class-wc-session-handler.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. defined( 'ABSPATH' ) || exit;
  13. /**
  14. * Session handler class.
  15. */
  16. class WC_Session_Handler extends WC_Session {
  17. /**
  18. * Cookie name used for the session.
  19. *
  20. * @var string cookie name
  21. */
  22. protected $_cookie;
  23. /**
  24. * Stores session expiry.
  25. *
  26. * @var string session due to expire timestamp
  27. */
  28. protected $_session_expiring;
  29. /**
  30. * Stores session due to expire timestamp.
  31. *
  32. * @var string session expiration timestamp
  33. */
  34. protected $_session_expiration;
  35. /**
  36. * True when the cookie exists.
  37. *
  38. * @var bool Based on whether a cookie exists.
  39. */
  40. protected $_has_cookie = false;
  41. /**
  42. * Table name for session data.
  43. *
  44. * @var string Custom session table name
  45. */
  46. protected $_table;
  47. /**
  48. * Constructor for the session class.
  49. */
  50. public function __construct() {
  51. $this->_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH );
  52. $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
  53. }
  54. /**
  55. * Init hooks and session data.
  56. *
  57. * @since 3.3.0
  58. */
  59. public function init() {
  60. $cookie = $this->get_session_cookie();
  61. if ( $cookie ) {
  62. $this->_customer_id = $cookie[0];
  63. $this->_session_expiration = $cookie[1];
  64. $this->_session_expiring = $cookie[2];
  65. $this->_has_cookie = true;
  66. // Update session if its close to expiring.
  67. if ( time() > $this->_session_expiring ) {
  68. $this->set_session_expiration();
  69. $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
  70. }
  71. } else {
  72. $this->set_session_expiration();
  73. $this->_customer_id = $this->generate_customer_id();
  74. }
  75. $this->_data = $this->get_session_data();
  76. add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 );
  77. add_action( 'shutdown', array( $this, 'save_data' ), 20 );
  78. add_action( 'wp_logout', array( $this, 'destroy_session' ) );
  79. if ( ! is_user_logged_in() ) {
  80. add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) );
  81. }
  82. }
  83. /**
  84. * Sets the session cookie on-demand (usually after adding an item to the cart).
  85. *
  86. * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
  87. *
  88. * Warning: Cookies will only be set if this is called before the headers are sent.
  89. *
  90. * @param bool $set Should the session cookie be set.
  91. */
  92. public function set_customer_session_cookie( $set ) {
  93. if ( $set ) {
  94. $to_hash = $this->_customer_id . '|' . $this->_session_expiration;
  95. $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
  96. $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
  97. $this->_has_cookie = true;
  98. wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, apply_filters( 'wc_session_use_secure_cookie', false ) );
  99. }
  100. }
  101. /**
  102. * Return true if the current user has an active session, i.e. a cookie to retrieve values.
  103. *
  104. * @return bool
  105. */
  106. public function has_session() {
  107. return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine.
  108. }
  109. /**
  110. * Set session expiration.
  111. */
  112. public function set_session_expiration() {
  113. $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours.
  114. $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours.
  115. }
  116. /**
  117. * Generate a unique customer ID for guests, or return user ID if logged in.
  118. *
  119. * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
  120. *
  121. * @return string
  122. */
  123. public function generate_customer_id() {
  124. $customer_id = '';
  125. if ( is_user_logged_in() ) {
  126. $customer_id = get_current_user_id();
  127. }
  128. if ( empty( $customer_id ) ) {
  129. require_once ABSPATH . 'wp-includes/class-phpass.php';
  130. $hasher = new PasswordHash( 8, false );
  131. $customer_id = md5( $hasher->get_random_bytes( 32 ) );
  132. }
  133. return $customer_id;
  134. }
  135. /**
  136. * Get the session cookie, if set. Otherwise return false.
  137. *
  138. * Session cookies without a customer ID are invalid.
  139. *
  140. * @return bool|array
  141. */
  142. public function get_session_cookie() {
  143. $cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine.
  144. if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) {
  145. return false;
  146. }
  147. list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value );
  148. if ( empty( $customer_id ) ) {
  149. return false;
  150. }
  151. // Validate hash.
  152. $to_hash = $customer_id . '|' . $session_expiration;
  153. $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
  154. if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) {
  155. return false;
  156. }
  157. return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
  158. }
  159. /**
  160. * Get session data.
  161. *
  162. * @return array
  163. */
  164. public function get_session_data() {
  165. return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array();
  166. }
  167. /**
  168. * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
  169. *
  170. * @return string
  171. */
  172. private function get_cache_prefix() {
  173. return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
  174. }
  175. /**
  176. * Save data.
  177. */
  178. public function save_data() {
  179. // Dirty if something changed - prevents saving nothing new.
  180. if ( $this->_dirty && $this->has_session() ) {
  181. global $wpdb;
  182. $wpdb->replace( // @codingStandardsIgnoreLine.
  183. $this->_table,
  184. array(
  185. 'session_key' => $this->_customer_id,
  186. 'session_value' => maybe_serialize( $this->_data ),
  187. 'session_expiry' => $this->_session_expiration,
  188. ),
  189. array(
  190. '%s',
  191. '%s',
  192. '%d',
  193. )
  194. );
  195. wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
  196. $this->_dirty = false;
  197. }
  198. }
  199. /**
  200. * Destroy all session data.
  201. */
  202. public function destroy_session() {
  203. wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, apply_filters( 'wc_session_use_secure_cookie', false ) );
  204. $this->delete_session( $this->_customer_id );
  205. wc_empty_cart();
  206. $this->_data = array();
  207. $this->_dirty = false;
  208. $this->_customer_id = $this->generate_customer_id();
  209. }
  210. /**
  211. * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
  212. *
  213. * @param int $uid User ID.
  214. * @return string
  215. */
  216. public function nonce_user_logged_out( $uid ) {
  217. return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
  218. }
  219. /**
  220. * Cleanup session data from the database and clear caches.
  221. */
  222. public function cleanup_sessions() {
  223. global $wpdb;
  224. $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine.
  225. if ( class_exists( 'WC_Cache_Helper' ) ) {
  226. WC_Cache_Helper::incr_cache_prefix( WC_SESSION_CACHE_GROUP );
  227. }
  228. }
  229. /**
  230. * Returns the session.
  231. *
  232. * @param string $customer_id Custo ID.
  233. * @param mixed $default Default session value.
  234. * @return string|array
  235. */
  236. public function get_session( $customer_id, $default = false ) {
  237. global $wpdb;
  238. if ( defined( 'WP_SETUP_CONFIG' ) ) {
  239. return false;
  240. }
  241. // Try to get it from the cache, it will return false if not present or if object cache not in use.
  242. $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
  243. if ( false === $value ) {
  244. $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine.
  245. if ( is_null( $value ) ) {
  246. $value = $default;
  247. }
  248. wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
  249. }
  250. return maybe_unserialize( $value );
  251. }
  252. /**
  253. * Delete the session from the cache and database.
  254. *
  255. * @param int $customer_id Customer ID.
  256. */
  257. public function delete_session( $customer_id ) {
  258. global $wpdb;
  259. wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
  260. $wpdb->delete( // @codingStandardsIgnoreLine.
  261. $this->_table,
  262. array(
  263. 'session_key' => $customer_id,
  264. )
  265. );
  266. }
  267. /**
  268. * Update the session expiry timestamp.
  269. *
  270. * @param string $customer_id Customer ID.
  271. * @param int $timestamp Timestamp to expire the cookie.
  272. */
  273. public function update_session_timestamp( $customer_id, $timestamp ) {
  274. global $wpdb;
  275. // @codingStandardsIgnoreStart.
  276. $wpdb->update(
  277. $this->_table,
  278. array(
  279. 'session_expiry' => $timestamp,
  280. ),
  281. array(
  282. 'session_key' => $customer_id,
  283. ),
  284. array(
  285. '%d'
  286. )
  287. );
  288. // @codingStandardsIgnoreEnd.
  289. }
  290. }