No Description

class-wc-session-handler.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. /**
  3. * Handle data for the current customers session.
  4. * Implements the WC_Session abstract class.
  5. *
  6. * From 2.5 this uses a custom table for session storage. Based on https://github.com/kloon/woocommerce-large-sessions.
  7. *
  8. * @class WC_Session_Handler
  9. * @version 2.5.0
  10. * @package WooCommerce\Classes
  11. */
  12. use Automattic\Jetpack\Constants;
  13. defined( 'ABSPATH' ) || exit;
  14. /**
  15. * Session handler class.
  16. */
  17. class WC_Session_Handler extends WC_Session {
  18. /**
  19. * Cookie name used for the session.
  20. *
  21. * @var string cookie name
  22. */
  23. protected $_cookie;
  24. /**
  25. * Stores session expiry.
  26. *
  27. * @var string session due to expire timestamp
  28. */
  29. protected $_session_expiring;
  30. /**
  31. * Stores session due to expire timestamp.
  32. *
  33. * @var string session expiration timestamp
  34. */
  35. protected $_session_expiration;
  36. /**
  37. * True when the cookie exists.
  38. *
  39. * @var bool Based on whether a cookie exists.
  40. */
  41. protected $_has_cookie = false;
  42. /**
  43. * Table name for session data.
  44. *
  45. * @var string Custom session table name
  46. */
  47. protected $_table;
  48. /**
  49. * Constructor for the session class.
  50. */
  51. public function __construct() {
  52. $this->_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH );
  53. $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
  54. }
  55. /**
  56. * Init hooks and session data.
  57. *
  58. * @since 3.3.0
  59. */
  60. public function init() {
  61. $this->init_session_cookie();
  62. add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 );
  63. add_action( 'shutdown', array( $this, 'save_data' ), 20 );
  64. add_action( 'wp_logout', array( $this, 'destroy_session' ) );
  65. if ( ! is_user_logged_in() ) {
  66. add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 );
  67. }
  68. }
  69. /**
  70. * Setup cookie and customer ID.
  71. *
  72. * @since 3.6.0
  73. */
  74. public function init_session_cookie() {
  75. $cookie = $this->get_session_cookie();
  76. if ( $cookie ) {
  77. $this->_customer_id = $cookie[0];
  78. $this->_session_expiration = $cookie[1];
  79. $this->_session_expiring = $cookie[2];
  80. $this->_has_cookie = true;
  81. $this->_data = $this->get_session_data();
  82. // If the user logs in, update session.
  83. if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
  84. $guest_session_id = $this->_customer_id;
  85. $this->_customer_id = strval( get_current_user_id() );
  86. $this->_dirty = true;
  87. $this->save_data( $guest_session_id );
  88. $this->set_customer_session_cookie( true );
  89. }
  90. // Update session if its close to expiring.
  91. if ( time() > $this->_session_expiring ) {
  92. $this->set_session_expiration();
  93. $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
  94. }
  95. } else {
  96. $this->set_session_expiration();
  97. $this->_customer_id = $this->generate_customer_id();
  98. $this->_data = $this->get_session_data();
  99. }
  100. }
  101. /**
  102. * Sets the session cookie on-demand (usually after adding an item to the cart).
  103. *
  104. * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
  105. *
  106. * Warning: Cookies will only be set if this is called before the headers are sent.
  107. *
  108. * @param bool $set Should the session cookie be set.
  109. */
  110. public function set_customer_session_cookie( $set ) {
  111. if ( $set ) {
  112. $to_hash = $this->_customer_id . '|' . $this->_session_expiration;
  113. $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
  114. $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
  115. $this->_has_cookie = true;
  116. if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) {
  117. wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true );
  118. }
  119. }
  120. }
  121. /**
  122. * Should the session cookie be secure?
  123. *
  124. * @since 3.6.0
  125. * @return bool
  126. */
  127. protected function use_secure_cookie() {
  128. return apply_filters( 'wc_session_use_secure_cookie', wc_site_is_https() && is_ssl() );
  129. }
  130. /**
  131. * Return true if the current user has an active session, i.e. a cookie to retrieve values.
  132. *
  133. * @return bool
  134. */
  135. public function has_session() {
  136. return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine.
  137. }
  138. /**
  139. * Set session expiration.
  140. */
  141. public function set_session_expiration() {
  142. $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours.
  143. $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours.
  144. }
  145. /**
  146. * Generate a unique customer ID for guests, or return user ID if logged in.
  147. *
  148. * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
  149. *
  150. * @return string
  151. */
  152. public function generate_customer_id() {
  153. $customer_id = '';
  154. if ( is_user_logged_in() ) {
  155. $customer_id = strval( get_current_user_id() );
  156. }
  157. if ( empty( $customer_id ) ) {
  158. require_once ABSPATH . 'wp-includes/class-phpass.php';
  159. $hasher = new PasswordHash( 8, false );
  160. $customer_id = md5( $hasher->get_random_bytes( 32 ) );
  161. }
  162. return $customer_id;
  163. }
  164. /**
  165. * Get session unique ID for requests if session is initialized or user ID if logged in.
  166. * Introduced to help with unit tests.
  167. *
  168. * @since 5.3.0
  169. * @return string
  170. */
  171. public function get_customer_unique_id() {
  172. $customer_id = '';
  173. if ( $this->has_session() && $this->_customer_id ) {
  174. $customer_id = $this->_customer_id;
  175. } elseif ( is_user_logged_in() ) {
  176. $customer_id = (string) get_current_user_id();
  177. }
  178. return $customer_id;
  179. }
  180. /**
  181. * Get the session cookie, if set. Otherwise return false.
  182. *
  183. * Session cookies without a customer ID are invalid.
  184. *
  185. * @return bool|array
  186. */
  187. public function get_session_cookie() {
  188. $cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine.
  189. if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) {
  190. return false;
  191. }
  192. list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value );
  193. if ( empty( $customer_id ) ) {
  194. return false;
  195. }
  196. // Validate hash.
  197. $to_hash = $customer_id . '|' . $session_expiration;
  198. $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
  199. if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) {
  200. return false;
  201. }
  202. return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
  203. }
  204. /**
  205. * Get session data.
  206. *
  207. * @return array
  208. */
  209. public function get_session_data() {
  210. return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array();
  211. }
  212. /**
  213. * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
  214. *
  215. * @return string
  216. */
  217. private function get_cache_prefix() {
  218. return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
  219. }
  220. /**
  221. * Save data and delete guest session.
  222. *
  223. * @param int $old_session_key session ID before user logs in.
  224. */
  225. public function save_data( $old_session_key = 0 ) {
  226. // Dirty if something changed - prevents saving nothing new.
  227. if ( $this->_dirty && $this->has_session() ) {
  228. global $wpdb;
  229. $wpdb->query(
  230. $wpdb->prepare(
  231. "INSERT INTO {$wpdb->prefix}woocommerce_sessions (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d)
  232. ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)",
  233. $this->_customer_id,
  234. maybe_serialize( $this->_data ),
  235. $this->_session_expiration
  236. )
  237. );
  238. wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
  239. $this->_dirty = false;
  240. if ( get_current_user_id() != $old_session_key && ! is_object( get_user_by( 'id', $old_session_key ) ) ) {
  241. $this->delete_session( $old_session_key );
  242. }
  243. }
  244. }
  245. /**
  246. * Destroy all session data.
  247. */
  248. public function destroy_session() {
  249. $this->delete_session( $this->_customer_id );
  250. $this->forget_session();
  251. }
  252. /**
  253. * Forget all session data without destroying it.
  254. */
  255. public function forget_session() {
  256. wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true );
  257. wc_empty_cart();
  258. $this->_data = array();
  259. $this->_dirty = false;
  260. $this->_customer_id = $this->generate_customer_id();
  261. }
  262. /**
  263. * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
  264. *
  265. * @deprecated 5.3.0
  266. * @param int $uid User ID.
  267. * @return int|string
  268. */
  269. public function nonce_user_logged_out( $uid ) {
  270. wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' );
  271. return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
  272. }
  273. /**
  274. * When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID.
  275. * This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called.
  276. *
  277. * @since 5.3.0
  278. * @param int $uid User ID.
  279. * @param string $action The nonce action.
  280. * @return int|string
  281. */
  282. public function maybe_update_nonce_user_logged_out( $uid, $action ) {
  283. if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) {
  284. return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
  285. }
  286. return $uid;
  287. }
  288. /**
  289. * Cleanup session data from the database and clear caches.
  290. */
  291. public function cleanup_sessions() {
  292. global $wpdb;
  293. $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine.
  294. if ( class_exists( 'WC_Cache_Helper' ) ) {
  295. WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
  296. }
  297. }
  298. /**
  299. * Returns the session.
  300. *
  301. * @param string $customer_id Custo ID.
  302. * @param mixed $default Default session value.
  303. * @return string|array
  304. */
  305. public function get_session( $customer_id, $default = false ) {
  306. global $wpdb;
  307. if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) {
  308. return false;
  309. }
  310. // Try to get it from the cache, it will return false if not present or if object cache not in use.
  311. $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
  312. if ( false === $value ) {
  313. $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine.
  314. if ( is_null( $value ) ) {
  315. $value = $default;
  316. }
  317. $cache_duration = $this->_session_expiration - time();
  318. if ( 0 < $cache_duration ) {
  319. wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $cache_duration );
  320. }
  321. }
  322. return maybe_unserialize( $value );
  323. }
  324. /**
  325. * Delete the session from the cache and database.
  326. *
  327. * @param int $customer_id Customer ID.
  328. */
  329. public function delete_session( $customer_id ) {
  330. global $wpdb;
  331. wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
  332. $wpdb->delete(
  333. $this->_table,
  334. array(
  335. 'session_key' => $customer_id,
  336. )
  337. );
  338. }
  339. /**
  340. * Update the session expiry timestamp.
  341. *
  342. * @param string $customer_id Customer ID.
  343. * @param int $timestamp Timestamp to expire the cookie.
  344. */
  345. public function update_session_timestamp( $customer_id, $timestamp ) {
  346. global $wpdb;
  347. $wpdb->update(
  348. $this->_table,
  349. array(
  350. 'session_expiry' => $timestamp,
  351. ),
  352. array(
  353. 'session_key' => $customer_id,
  354. ),
  355. array(
  356. '%d',
  357. )
  358. );
  359. }
  360. }