Completed
Push — master ( fb5f9c...377d79 )
by Mike
64:31 queued 55:52
created

WC_Session_Handler   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 65.69%

Importance

Changes 0
Metric Value
dl 0
loc 337
ccs 67
cts 102
cp 0.6569
rs 8.96
c 0
b 0
f 0
wmc 43
lcom 1
cbo 2

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A init() 0 12 2
A init_session_cookie() 0 19 3
A set_customer_session_cookie() 0 12 4
A has_session() 0 3 3
A set_session_expiration() 0 4 1
A generate_customer_id() 0 15 3
B get_session_cookie() 0 23 7
A get_session_data() 0 3 2
A get_cache_prefix() 0 3 1
A save_data() 0 19 3
A destroy_session() 0 4 1
A forget_session() 0 9 1
A nonce_user_logged_out() 0 3 3
A cleanup_sessions() 0 9 2
A get_session() 0 22 4
A delete_session() 0 12 1
A update_session_timestamp() 0 16 1

How to fix   Complexity   

Complex Class

Complex classes like WC_Session_Handler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Session_Handler, and based on these observations, apply Extract Interface, too.

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