Completed
Pull Request — master (#10412)
by Gerhard
08:41
created

WC_Session_Handler::generate_customer_id()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 9
rs 9.6666
cc 2
eloc 7
nc 2
nop 0
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
			$wpdb->replace(
177
				$this->_table,
178
				array(
179
					'session_key' => $this->_customer_id,
180
					'session_value' => maybe_serialize( $this->_data ),
181
					'session_expiry' => $this->_session_expiration
182
				),
183
				array(
184
					'%s',
185
					'%s',
186
					'%d'
187
				)
188
			);
189
190
			// Set cache
191
			wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
192
193
			// Mark session clean after saving
194
			$this->_dirty = false;
195
		}
196
	}
197
198
	/**
199
	 * Destroy all session data.
200
	 */
201
	public function destroy_session() {
202
		// Clear cookie
203
		wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, apply_filters( 'wc_session_use_secure_cookie', false ) );
204
205
		$this->delete_session( $this->_customer_id );
206
207
		// Clear cart
208
		wc_empty_cart();
209
210
		// Clear data
211
		$this->_data        = array();
212
		$this->_dirty       = false;
213
		$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...
214
	}
215
216
	/**
217
	 * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
218
	 *
219
	 * @return string
220
	 */
221
	public function nonce_user_logged_out( $uid ) {
222
		return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
223
	}
224
225
	/**
226
	 * Cleanup sessions.
227
	 */
228
	public function cleanup_sessions() {
229
		global $wpdb;
230
231
		if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) {
232
233
			// Delete expired sessions
234
			$wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) );
235
236
			// Invalidate cache
237
			WC_Cache_Helper::incr_cache_prefix( WC_SESSION_CACHE_GROUP );
238
		}
239
	}
240
241
	/**
242
	 * Returns the session.
243
	 *
244
	 * @param string $customer_id
245
	 * @param mixed $default
246
	 * @return string|array
247
	 */
248
	public function get_session( $customer_id, $default = false ) {
249
		global $wpdb;
250
251
		if ( defined( 'WP_SETUP_CONFIG' ) ) {
252
			return false;
253
		}
254
255
		// Try get it from the cache, it will return false if not present or if object cache not in use
256
		$value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
257
258
		if ( false === $value ) {
259
			$value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) );
260
261
			if ( is_null( $value ) ) {
262
				$value = $default;
263
			}
264
265
			wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
266
		}
267
268
		return maybe_unserialize( $value );
269
	}
270
271
	/**
272
	 * Delete the session from the cache and database.
273
	 *
274
	 * @param int $customer_id
275
	 */
276
	public function delete_session( $customer_id ) {
277
		global $wpdb;
278
279
		wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );
280
281
		$wpdb->delete(
282
			$this->_table,
283
			array(
284
				'session_key' => $customer_id
285
			)
286
		);
287
	}
288
289
	/**
290
	 * Update the session expiry timestamp.
291
	 *
292
	 * @param string $customer_id
293
	 * @param int $timestamp
294
	 */
295
	public function update_session_timestamp( $customer_id, $timestamp ) {
296
		global $wpdb;
297
298
		$wpdb->update(
299
			$this->_table,
300
			array(
301
				'session_expiry' => $timestamp
302
			),
303
			array(
304
				'session_key' => $customer_id
305
			),
306
			array(
307
				'%d'
308
			)
309
		);
310
	}
311
}
312