WC_Session_Handler::get_session()   B
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
dl 0
loc 22
rs 8.9197
c 0
b 0
f 0
eloc 11
nc 4
nop 2
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
if ( ! class_exists( 'WC_Session' ) ) {
7
	include_once( 'abstracts/abstract-wc-session.php' );
8
}
9
10
/**
11
 * Handle data for the current customers session.
12
 * Implements the WC_Session abstract class.
13
 *
14
 * From 2.5 this uses a custom table for session storage. Based on https://github.com/kloon/woocommerce-large-sessions.
15
 *
16
 * @class    WC_Session_Handler
17
 * @version  2.5.0
18
 * @package  WooCommerce/Classes
19
 * @category Class
20
 * @author   WooThemes
21
 */
22
class WC_Session_Handler extends WC_Session {
23
24
	/** @var string cookie name */
25
	private $_cookie;
26
27
	/** @var string session due to expire timestamp */
28
	private $_session_expiring;
29
30
	/** @var string session expiration timestamp */
31
	private $_session_expiration;
32
33
	/** $var bool Bool based on whether a cookie exists **/
34
	private $_has_cookie = false;
35
36
	/** @var string Custom session table name */
37
	private $_table;
38
39
	/**
40
	 * Constructor for the session class.
41
	 */
42
	public function __construct() {
43
		global $wpdb;
44
45
		$this->_cookie = 'wp_woocommerce_session_' . COOKIEHASH;
46
		$this->_table  = $wpdb->prefix . 'woocommerce_sessions';
47
48
		if ( $cookie = $this->get_session_cookie() ) {
49
			$this->_customer_id        = $cookie[0];
50
			$this->_session_expiration = $cookie[1];
51
			$this->_session_expiring   = $cookie[2];
52
			$this->_has_cookie         = true;
53
54
			// Update session if its close to expiring
55
			if ( time() > $this->_session_expiring ) {
56
				$this->set_session_expiration();
57
				$this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
58
			}
59
60
		} else {
61
			$this->set_session_expiration();
62
			$this->_customer_id = $this->generate_customer_id();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->generate_customer_id() can also be of type string. However, the property $_customer_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
63
		}
64
65
		$this->_data = $this->get_session_data();
66
67
		// Actions
68
		add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 );
69
		add_action( 'woocommerce_cleanup_sessions', array( $this, 'cleanup_sessions' ), 10 );
70
		add_action( 'shutdown', array( $this, 'save_data' ), 20 );
71
		add_action( 'wp_logout', array( $this, 'destroy_session' ) );
72
		if ( ! is_user_logged_in() ) {
73
			add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) );
74
		}
75
	}
76
77
	/**
78
	 * Sets the session cookie on-demand (usually after adding an item to the cart).
79
	 *
80
	 * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
81
	 *
82
	 * Warning: Cookies will only be set if this is called before the headers are sent.
83
	 */
84
	public function set_customer_session_cookie( $set ) {
85
		if ( $set ) {
86
			// Set/renew our cookie
87
			$to_hash           = $this->_customer_id . '|' . $this->_session_expiration;
88
			$cookie_hash       = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
89
			$cookie_value      = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
90
			$this->_has_cookie = true;
91
92
			// Set the cookie
93
			wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, apply_filters( 'wc_session_use_secure_cookie', false ) );
94
		}
95
	}
96
97
	/**
98
	 * Return true if the current user has an active session, i.e. a cookie to retrieve values.
99
	 *
100
	 * @return bool
101
	 */
102
	public function has_session() {
103
		return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in();
104
	}
105
106
	/**
107
	 * Set session expiration.
108
	 */
109
	public function set_session_expiration() {
110
		$this->_session_expiring   = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours.
0 ignored issues
show
Documentation Bug introduced by
The property $_session_expiring was declared of type string, but time() + intval(apply_fi...piring', 60 * 60 * 47)) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
111
		$this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours.
0 ignored issues
show
Documentation Bug introduced by
The property $_session_expiration was declared of type string, but time() + intval(apply_fi...ration', 60 * 60 * 48)) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
112
	}
113
114
	/**
115
	 * Generate a unique customer ID for guests, or return user ID if logged in.
116
	 *
117
	 * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
118
	 *
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.
133
	 *
134
	 * @return bool|array
135
	 */
136
	public function get_session_cookie() {
137
		if ( empty( $_COOKIE[ $this->_cookie ] ) || ! is_string( $_COOKIE[ $this->_cookie ] ) ) {
138
			return false;
139
		}
140
141
		list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $_COOKIE[ $this->_cookie ] );
142
143
		// Validate hash
144
		$to_hash = $customer_id . '|' . $session_expiration;
145
		$hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
146
147
		if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) {
148
			return false;
149
		}
150
151
		return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
152
	}
153
154
	/**
155
	 * Get session data.
156
	 *
157
	 * @return array
158
	 */
159
	public function get_session_data() {
160
		return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array();
161
	}
162
163
	/**
164
	 * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
165
	 *
166
	 * @return string
167
	 */
168
	private function get_cache_prefix() {
169
		return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
170
	}
171
172
	/**
173
	 * Save data.
174
	 */
175
	public function save_data() {
176
		// Dirty if something changed - prevents saving nothing new
177
		if ( $this->_dirty && $this->has_session() ) {
178
			global $wpdb;
179
180
			$wpdb->replace(
181
				$this->_table,
182
				array(
183
					'session_key' => $this->_customer_id,
184
					'session_value' => maybe_serialize( $this->_data ),
185
					'session_expiry' => $this->_session_expiration
186
				),
187
				array(
188
					'%s',
189
					'%s',
190
					'%d'
191
				)
192
			);
193
194
			// Set cache
195
			wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
196
197
			// Mark session clean after saving
198
			$this->_dirty = false;
199
		}
200
	}
201
202
	/**
203
	 * Destroy all session data.
204
	 */
205
	public function destroy_session() {
206
		// Clear cookie
207
		wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, apply_filters( 'wc_session_use_secure_cookie', false ) );
208
209
		$this->delete_session( $this->_customer_id );
210
211
		// Clear cart
212
		wc_empty_cart();
213
214
		// Clear data
215
		$this->_data        = array();
216
		$this->_dirty       = false;
217
		$this->_customer_id = $this->generate_customer_id();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->generate_customer_id() can also be of type string. However, the property $_customer_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
218
	}
219
220
	/**
221
	 * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
222
	 *
223
	 * @return string
224
	 */
225
	public function nonce_user_logged_out( $uid ) {
226
		return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
227
	}
228
229
	/**
230
	 * Cleanup sessions.
231
	 */
232
	public function cleanup_sessions() {
233
		global $wpdb;
234
235
		if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) {
236
237
			// Delete expired sessions
238
			$wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) );
239
240
			// Invalidate cache
241
			WC_Cache_Helper::incr_cache_prefix( WC_SESSION_CACHE_GROUP );
242
		}
243
	}
244
245
	/**
246
	 * Returns the session.
247
	 *
248
	 * @param string $customer_id
249
	 * @param mixed $default
250
	 * @return string|array
251
	 */
252
	public function get_session( $customer_id, $default = false ) {
253
		global $wpdb;
254
255
		if ( defined( 'WP_SETUP_CONFIG' ) ) {
256
			return false;
257
		}
258
259
		// Try get it from the cache, it will return false if not present or if object cache not in use
260
		$value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
261
262
		if ( false === $value ) {
263
			$value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) );
264
265
			if ( is_null( $value ) ) {
266
				$value = $default;
267
			}
268
269
			wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
270
		}
271
272
		return maybe_unserialize( $value );
273
	}
274
275
	/**
276
	 * Delete the session from the cache and database.
277
	 *
278
	 * @param int $customer_id
279
	 */
280
	public function delete_session( $customer_id ) {
281
		global $wpdb;
282
283
		wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
284
285
		$wpdb->delete(
286
			$this->_table,
287
			array(
288
				'session_key' => $customer_id
289
			)
290
		);
291
	}
292
293
	/**
294
	 * Update the session expiry timestamp.
295
	 *
296
	 * @param string $customer_id
297
	 * @param int $timestamp
298
	 */
299
	public function update_session_timestamp( $customer_id, $timestamp ) {
300
		global $wpdb;
301
302
		$wpdb->update(
303
			$this->_table,
304
			array(
305
				'session_expiry' => $timestamp
306
			),
307
			array(
308
				'session_key' => $customer_id
309
			),
310
			array(
311
				'%d'
312
			)
313
		);
314
	}
315
}
316