1 <?php
  2 
  3 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
  4 
  5 /**
  6  * Handle data for the current customers session.
  7  * Implements the WC_Session abstract class
  8  *
  9  * Long term plan will be, if https://github.com/ericmann/wp-session-manager/ gains traction
 10  * in WP core, this will be switched out to use it and maintain backwards compatibility :)
 11  *
 12  * Partly based on WP SESSION by Eric Mann.
 13  *
 14  * @class       WC_Session_Handler
 15  * @version     2.0.0
 16  * @package     WooCommerce/Classes
 17  * @category    Class
 18  * @author      WooThemes
 19  */
 20 class WC_Session_Handler extends WC_Session {
 21 
 22     /** cookie name */
 23     private $_cookie;
 24 
 25     /** session due to expire timestamp */
 26     private $_session_expiring;
 27 
 28     /** session expiration timestamp */
 29     private $_session_expiration;
 30 
 31     /** Bool based on whether a cookie exists **/
 32     private $_has_cookie = false;
 33 
 34     /**
 35      * Constructor for the session class.
 36      *
 37      * @access public
 38      * @return void
 39      */
 40     public function __construct() {
 41         $this->_cookie = 'wp_woocommerce_session_' . COOKIEHASH;
 42 
 43         if ( $cookie = $this->get_session_cookie() ) {
 44             $this->_customer_id        = $cookie[0];
 45             $this->_session_expiration = $cookie[1];
 46             $this->_session_expiring   = $cookie[2];
 47             $this->_has_cookie         = true;
 48 
 49             // Update session if its close to expiring
 50             if ( time() > $this->_session_expiring ) {
 51                 $this->set_session_expiration();
 52                 $session_expiry_option = '_wc_session_expires_' . $this->_customer_id;
 53                 // Check if option exists first to avoid auloading cleaned up sessions
 54                 if ( false === get_option( $session_expiry_option ) ) {
 55                     add_option( $session_expiry_option, $this->_session_expiration, '', 'no' );
 56                 } else {
 57                     update_option( $session_expiry_option, $this->_session_expiration );
 58                 }
 59             }
 60 
 61         } else {
 62             $this->set_session_expiration();
 63             $this->_customer_id = $this->generate_customer_id();
 64         }
 65 
 66         $this->_data = $this->get_session_data();
 67 
 68         // Actions
 69         add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 );
 70         add_action( 'woocommerce_cleanup_sessions', array( $this, 'cleanup_sessions' ), 10 );
 71         add_action( 'shutdown', array( $this, 'save_data' ), 20 );
 72     }
 73 
 74     /**
 75      * Sets the session cookie on-demand (usually after adding an item to the cart).
 76      *
 77      * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
 78      *
 79      * Warning: Cookies will only be set if this is called before the headers are sent.
 80      */
 81     public function set_customer_session_cookie( $set ) {
 82         if ( $set ) {
 83             // Set/renew our cookie
 84             $to_hash           = $this->_customer_id . $this->_session_expiration;
 85             $cookie_hash       = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
 86             $cookie_value      = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
 87             $this->_has_cookie = true;
 88 
 89             // Set the cookie
 90             wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, apply_filters( 'wc_session_use_secure_cookie', false ) );
 91         }
 92     }
 93 
 94     /**
 95      * Return true if the current user has an active session, i.e. a cookie to retrieve values
 96      * @return boolean
 97      */
 98     public function has_session() {
 99         return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in();
100     }
101 
102     /**
103      * set_session_expiration function.
104      *
105      * @access public
106      * @return void
107      */
108     public function set_session_expiration() {
109         $this->_session_expiring    = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours
110         $this->_session_expiration  = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours
111     }
112 
113     /**
114      * Generate a unique customer ID for guests, or return user ID if logged in. 
115      * 
116      * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
117      *
118      * @access public
119      * @return int|string
120      */
121     public function generate_customer_id() {
122         if ( is_user_logged_in() ) {
123             return get_current_user_id();
124         } else {
125             require_once( ABSPATH . 'wp-includes/class-phpass.php');
126             $hasher = new PasswordHash( 8, false );
127             return md5( $hasher->get_random_bytes( 32 ) );
128         }
129     }
130 
131     /**
132      * get_session_cookie function.
133      *
134      * @access public
135      * @return mixed
136      */
137     public function get_session_cookie() {
138         if ( empty( $_COOKIE[ $this->_cookie ] ) ) {
139             return false;
140         }
141 
142         list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $_COOKIE[ $this->_cookie ] );
143 
144         // Validate hash
145         $to_hash = $customer_id . $session_expiration;
146         $hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
147 
148         if ( $hash != $cookie_hash ) {
149             return false;
150         }
151 
152         return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
153     }
154 
155     /**
156      * get_session_data function.
157      *
158      * @access public
159      * @return array
160      */
161     public function get_session_data() {
162         return (array) get_option( '_wc_session_' . $this->_customer_id, array() );
163     }
164 
165     /**
166      * save_data function.
167      *
168      * @access public
169      * @return void
170      */
171     public function save_data() {
172         // Dirty if something changed - prevents saving nothing new
173         if ( $this->_dirty && $this->has_session() ) {
174 
175             $session_option        = '_wc_session_' . $this->_customer_id;
176             $session_expiry_option = '_wc_session_expires_' . $this->_customer_id;
177 
178             if ( false === get_option( $session_option ) ) {
179                 add_option( $session_option, $this->_data, '', 'no' );
180                 add_option( $session_expiry_option, $this->_session_expiration, '', 'no' );
181             } else {
182                 update_option( $session_option, $this->_data );
183             }
184         }
185     }
186 
187     /**
188      * cleanup_sessions function.
189      *
190      * @access public
191      * @return void
192      */
193     public function cleanup_sessions() {
194         global $wpdb;
195 
196         if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) {
197             $now                = time();
198             $expired_sessions   = array();
199             $wc_session_expires = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE '_wc_session_expires_%'" );
200 
201             foreach ( $wc_session_expires as $wc_session_expire ) {
202                 if ( $now > intval( $wc_session_expire->option_value ) ) {
203                     $session_id         = substr( $wc_session_expire->option_name, 20 );
204                     $expired_sessions[] = $wc_session_expire->option_name;  // Expires key
205                     $expired_sessions[] = "_wc_session_$session_id"; // Session key
206                 }
207             }
208 
209             if ( ! empty( $expired_sessions ) ) {
210                 $expired_sessions_chunked = array_chunk( $expired_sessions, 100 );
211                 foreach ( $expired_sessions_chunked as $chunk ) {
212                     $option_names = implode( "','", $chunk );
213                     $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name IN ('$option_names')" );
214                 }
215             }
216         }
217     }
218 }
WooCommerce API documentation generated by ApiGen 2.8.0