Completed
Push — master ( 424bc6...6c8709 )
by Mike
23:37
created

WC_Session_Handler   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2
Metric Value
wmc 36
lcom 1
cbo 2
dl 0
loc 314
rs 8.8

15 Methods

Rating   Name   Duplication   Size   Complexity  
A has_session() 0 3 3
A set_session_expiration() 0 4 1
A get_session_data() 0 3 2
A get_cache_prefix() 0 3 1
A nonce_user_logged_out() 0 3 3
B __construct() 0 34 4
A set_customer_session_cookie() 0 12 2
A generate_customer_id() 0 9 2
B save_data() 0 46 4
A destroy_session() 0 14 1
A cleanup_sessions() 0 12 3
B get_session() 0 22 4
A delete_session() 0 12 1
A update_session_timestamp() 0 16 1
A get_session_cookie() 0 17 4
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 18 and the first side effect is on line 3.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Handle data for the current customers session.
8
 * Implements the WC_Session abstract class.
9
 *
10
 * From 2.5 this uses a custom table for session storage. Based on https://github.com/kloon/woocommerce-large-sessions.
11
 *
12
 * @class    WC_Session_Handler
13
 * @version  2.5.0
14
 * @package  WooCommerce/Classes
15
 * @category Class
16
 * @author   WooThemes
17
 */
18
class WC_Session_Handler extends WC_Session {
19
20
	/** @var string cookie name */
21
	private $_cookie;
22
23
	/** @var string session due to expire timestamp */
24
	private $_session_expiring;
25
26
	/** @var string session expiration timestamp */
27
	private $_session_expiration;
28
29
	/** $var bool Bool based on whether a cookie exists **/
30
	private $_has_cookie = false;
31
32
	/** @var string Custom session table name */
33
	private $_table;
34
35
	/**
36
	 * Constructor for the session class.
37
	 */
38
	public function __construct() {
39
		global $wpdb;
40
41
		$this->_cookie = 'wp_woocommerce_session_' . COOKIEHASH;
42
		$this->_table  = $wpdb->prefix . 'woocommerce_sessions';
43
44
		if ( $cookie = $this->get_session_cookie() ) {
45
			$this->_customer_id        = $cookie[0];
46
			$this->_session_expiration = $cookie[1];
47
			$this->_session_expiring   = $cookie[2];
48
			$this->_has_cookie         = true;
49
50
			// Update session if its close to expiring
51
			if ( time() > $this->_session_expiring ) {
52
				$this->set_session_expiration();
53
				$this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
54
			}
55
56
		} else {
57
			$this->set_session_expiration();
58
			$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...
59
		}
60
61
		$this->_data = $this->get_session_data();
62
63
		// Actions
64
		add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 );
65
		add_action( 'woocommerce_cleanup_sessions', array( $this, 'cleanup_sessions' ), 10 );
66
		add_action( 'shutdown', array( $this, 'save_data' ), 20 );
67
		add_action( 'wp_logout', array( $this, 'destroy_session' ) );
68
		if ( ! is_user_logged_in() ) {
69
			add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) );
70
		}
71
	}
72
73
	/**
74
	 * Sets the session cookie on-demand (usually after adding an item to the cart).
75
	 *
76
	 * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
77
	 *
78
	 * Warning: Cookies will only be set if this is called before the headers are sent.
79
	 */
80
	public function set_customer_session_cookie( $set ) {
81
		if ( $set ) {
82
			// Set/renew our cookie
83
			$to_hash           = $this->_customer_id . '|' . $this->_session_expiration;
84
			$cookie_hash       = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
85
			$cookie_value      = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
86
			$this->_has_cookie = true;
87
88
			// Set the cookie
89
			wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, apply_filters( 'wc_session_use_secure_cookie', false ) );
90
		}
91
	}
92
93
	/**
94
	 * Return true if the current user has an active session, i.e. a cookie to retrieve values.
95
	 *
96
	 * @return bool
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.
104
	 */
105
	public function set_session_expiration() {
106
		$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...
107
		$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...
108
	}
109
110
	/**
111
	 * Generate a unique customer ID for guests, or return user ID if logged in.
112
	 *
113
	 * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
114
	 *
115
	 * @return int|string
116
	 */
117
	public function generate_customer_id() {
118
		if ( is_user_logged_in() ) {
119
			return get_current_user_id();
120
		} else {
121
			require_once( ABSPATH . 'wp-includes/class-phpass.php');
122
			$hasher = new PasswordHash( 8, false );
123
			return md5( $hasher->get_random_bytes( 32 ) );
124
		}
125
	}
126
127
	/**
128
	 * Get session cookie.
129
	 *
130
	 * @return bool|array
131
	 */
132
	public function get_session_cookie() {
133
		if ( empty( $_COOKIE[ $this->_cookie ] ) ) {
134
			return false;
135
		}
136
137
		list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $_COOKIE[ $this->_cookie ] );
138
139
		// Validate hash
140
		$to_hash = $customer_id . '|' . $session_expiration;
141
		$hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
142
143
		if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) {
144
			return false;
145
		}
146
147
		return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
148
	}
149
150
	/**
151
	 * Get session data.
152
	 *
153
	 * @return array
154
	 */
155
	public function get_session_data() {
156
		return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array();
157
	}
158
159
	/**
160
	 * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
161
	 *
162
	 * @return string
163
	 */
164
	private function get_cache_prefix() {
165
		return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
166
	}
167
168
	/**
169
	 * Save data.
170
	 */
171
	public function save_data() {
172
		// Dirty if something changed - prevents saving nothing new
173
		if ( $this->_dirty && $this->has_session() ) {
174
			global $wpdb;
175
176
			$session_id = $wpdb->get_var( $wpdb->prepare( "SELECT session_id FROM $this->_table WHERE session_key = %s;", $this->_customer_id ) );
177
178
			if ( $session_id ) {
179
				$wpdb->update(
180
					$this->_table,
181
					array(
182
						'session_key'    => $this->_customer_id,
183
						'session_value'  => maybe_serialize( $this->_data ),
184
						'session_expiry' => $this->_session_expiration
185
					),
186
					array( 'session_id' => $session_id ),
187
					array(
188
						'%s',
189
						'%s',
190
						'%d'
191
					),
192
					array( '%d' )
193
				);
194
			} else {
195
				$wpdb->insert(
196
					$this->_table,
197
					array(
198
						'session_key'    => $this->_customer_id,
199
						'session_value'  => maybe_serialize( $this->_data ),
200
						'session_expiry' => $this->_session_expiration
201
					),
202
					array(
203
						'%s',
204
						'%s',
205
						'%d'
206
					)
207
				);
208
			}
209
210
			// Set cache
211
			wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
212
213
			// Mark session clean after saving
214
			$this->_dirty = false;
215
		}
216
	}
217
218
	/**
219
	 * Destroy all session data.
220
	 */
221
	public function destroy_session() {
222
		// Clear cookie
223
		wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, apply_filters( 'wc_session_use_secure_cookie', false ) );
224
225
		$this->delete_session( $this->_customer_id );
226
227
		// Clear cart
228
		wc_empty_cart();
229
230
		// Clear data
231
		$this->_data        = array();
232
		$this->_dirty       = false;
233
		$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...
234
	}
235
236
	/**
237
	 * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
238
	 *
239
	 * @return string
240
	 */
241
	public function nonce_user_logged_out( $uid ) {
242
		return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
243
	}
244
245
	/**
246
	 * Cleanup sessions.
247
	 */
248
	public function cleanup_sessions() {
249
		global $wpdb;
250
251
		if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) {
252
253
			// Delete expired sessions
254
			$wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) );
255
256
			// Invalidate cache
257
			WC_Cache_Helper::incr_cache_prefix( WC_SESSION_CACHE_GROUP );
258
		}
259
	}
260
261
	/**
262
	 * Returns the session.
263
	 *
264
	 * @param string $customer_id
265
	 * @param mixed $default
266
	 * @return string|array
267
	 */
268
	public function get_session( $customer_id, $default = false ) {
269
		global $wpdb;
270
271
		if ( defined( 'WP_SETUP_CONFIG' ) ) {
272
			return false;
273
		}
274
275
		// Try get it from the cache, it will return false if not present or if object cache not in use
276
		$value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
277
278
		if ( false === $value ) {
279
			$value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) );
280
281
			if ( is_null( $value ) ) {
282
				$value = $default;
283
			}
284
285
			wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
286
		}
287
288
		return maybe_unserialize( $value );
289
	}
290
291
	/**
292
	 * Delete the session from the cache and database.
293
	 *
294
	 * @param int $customer_id
295
	 */
296
	public function delete_session( $customer_id ) {
297
		global $wpdb;
298
299
		wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
300
301
		$wpdb->delete(
302
			$this->_table,
303
			array(
304
				'session_key' => $customer_id
305
			)
306
		);
307
	}
308
309
	/**
310
	 * Update the session expiry timestamp.
311
	 *
312
	 * @param string $customer_id
313
	 * @param int $timestamp
314
	 */
315
	public function update_session_timestamp( $customer_id, $timestamp ) {
316
		global $wpdb;
317
318
		$wpdb->update(
319
			$this->_table,
320
			array(
321
				'session_expiry' => $timestamp
322
			),
323
			array(
324
				'session_key' => $customer_id
325
			),
326
			array(
327
				'%d'
328
			)
329
		);
330
	}
331
}
332