Passed
Pull Request — master (#217)
by Patrik
03:30
created

WPInv_Session_Handler   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 323
rs 7.44
c 0
b 0
f 0
wmc 52
lcom 1
cbo 1

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A init() 0 11 2
A init_session_cookie() 0 29 5
A set_customer_session_cookie() 0 12 4
A setcookie() 0 8 5
A use_secure_cookie() 0 4 2
A has_session() 0 3 3
A set_session_expiration() 0 4 1
A generate_customer_id() 0 13 3
B get_session_cookie() 0 23 7
A get_session_data() 0 3 2
A generate_key() 0 7 2
A save_data() 0 9 3
A destroy_session() 0 4 1
A forget_session() 0 9 1
A nonce_user_logged_out() 0 3 3
A get_session() 0 21 5
A delete_session() 0 6 1
A update_session_timestamp() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like WPInv_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 WPInv_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 WPInv_Session abstract class.
5
 *
6
 */
7
8
defined( 'ABSPATH' ) || exit;
9
10
/**
11
 * Session handler class.
12
 */
13
class WPInv_Session_Handler extends WPInv_Session {
14
15
	/**
16
	 * Cookie name used for the session.
17
	 *
18
	 * @var string cookie name
19
	 */
20
	protected $_cookie;
21
22
	/**
23
	 * Stores session expiry.
24
	 *
25
	 * @var string session due to expire timestamp
26
	 */
27
	protected $_session_expiring;
28
29
	/**
30
	 * Stores session due to expire timestamp.
31
	 *
32
	 * @var string session expiration timestamp
33
	 */
34
	protected $_session_expiration;
35
36
	/**
37
	 * True when the cookie exists.
38
	 *
39
	 * @var bool Based on whether a cookie exists.
40
	 */
41
	protected $_has_cookie = false;
42
43
	/**
44
	 * Table name for session data.
45
	 *
46
	 * @var string Custom session table name
47
	 */
48
	protected $_table;
49
50
	/**
51
	 * Constructor for the session class.
52
	 */
53
	public function __construct() {
54
55
	    $this->_cookie = apply_filters( 'wpinv_cookie', 'wpinv_session_' . COOKIEHASH );
56
        add_action( 'init', array( $this, 'init' ), -1 );
57
	}
58
59
	/**
60
	 * Init hooks and session data.
61
	 *
62
	 * @since 3.3.0
63
	 */
64
	public function init() {
65
		$this->init_session_cookie();
66
67
		add_action( 'wp', array( $this, 'set_customer_session_cookie' ), 10 );
68
		add_action( 'shutdown', array( $this, 'save_data' ), 20 );
69
		add_action( 'wp_logout', array( $this, 'destroy_session' ) );
70
71
		if ( ! is_user_logged_in() ) {
72
			add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) );
73
		}
74
	}
75
76
	/**
77
	 * Setup cookie and customer ID.
78
	 *
79
	 * @since 3.6.0
80
	 */
81
	public function init_session_cookie() {
82
		$cookie = $this->get_session_cookie();
83
84
		if ( $cookie ) {
85
			$this->_customer_id        = $cookie[0];
86
			$this->_session_expiration = $cookie[1];
87
			$this->_session_expiring   = $cookie[2];
88
			$this->_has_cookie         = true;
89
			$this->_data               = $this->get_session_data();
90
91
			// If the user logs in, update session.
92
			if ( is_user_logged_in() && get_current_user_id() != $this->_customer_id ) {
93
				$this->_customer_id = get_current_user_id();
94
				$this->_dirty       = true;
95
				$this->save_data();
96
				$this->set_customer_session_cookie( true );
97
			}
98
99
			// Update session if its close to expiring.
100
			if ( time() > $this->_session_expiring ) {
101
				$this->set_session_expiration();
102
				$this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
103
			}
104
		} else {
105
			$this->set_session_expiration();
106
			$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...
107
			$this->_data        = $this->get_session_data();
108
		}
109
	}
110
111
	/**
112
	 * Sets the session cookie on-demand (usually after adding an item to the cart).
113
	 *
114
	 * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
115
	 *
116
	 * Warning: Cookies will only be set if this is called before the headers are sent.
117
	 *
118
	 * @param bool $set Should the session cookie be set.
119
	 */
120
	public function set_customer_session_cookie( $set ) {
121
		if ( $set ) {
122
			$to_hash           = $this->_customer_id . '|' . $this->_session_expiration;
123
			$cookie_hash       = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
124
			$cookie_value      = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
125
			$this->_has_cookie = true;
126
127
			if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) {
128
				$this->setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true );
129
			}
130
		}
131
	}
132
133
	public function setcookie($name, $value, $expire = 0, $secure = false, $httponly = false){
134
        if ( ! headers_sent() ) {
135
            setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure, apply_filters( 'wpinv_cookie_httponly', $httponly, $name, $value, $expire, $secure ) );
136
        } elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
137
            headers_sent( $file, $line );
138
            trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine
139
        }
140
    }
141
142
	/**
143
	 * Should the session cookie be secure?
144
	 *
145
	 * @since 3.6.0
146
	 * @return bool
147
	 */
148
	protected function use_secure_cookie() {
149
        $is_https = false !== strstr( get_option( 'home' ), 'https:' );
150
		return apply_filters( 'wpinv_session_use_secure_cookie', $is_https && is_ssl() );
151
	}
152
153
	/**
154
	 * Return true if the current user has an active session, i.e. a cookie to retrieve values.
155
	 *
156
	 * @return bool
157
	 */
158
	public function has_session() {
159
		return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine.
160
	}
161
162
	/**
163
	 * Set session expiration.
164
	 */
165
	public function set_session_expiration() {
166
		$this->_session_expiring   = time() + intval( apply_filters( 'wpinv_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...
167
		$this->_session_expiration = time() + intval( apply_filters( 'wpinv_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...
168
	}
169
170
	/**
171
	 * Generate a unique customer ID for guests, or return user ID if logged in.
172
	 *
173
	 * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
174
	 *
175
	 * @return string
176
	 */
177
	public function generate_customer_id() {
178
		$customer_id = '';
179
180
		if ( is_user_logged_in() ) {
181
			$customer_id = get_current_user_id();
182
		}
183
184
		if ( empty( $customer_id ) ) {
185
            $customer_id = wp_create_nonce('wpinv-session-customer-id');
186
		}
187
188
		return $customer_id;
189
	}
190
191
	/**
192
	 * Get the session cookie, if set. Otherwise return false.
193
	 *
194
	 * Session cookies without a customer ID are invalid.
195
	 *
196
	 * @return bool|array
197
	 */
198
	public function get_session_cookie() {
199
		$cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine.
200
201
		if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) {
202
			return false;
203
		}
204
205
		list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value );
206
207
		if ( empty( $customer_id ) ) {
208
			return false;
209
		}
210
211
		// Validate hash.
212
		$to_hash = $customer_id . '|' . $session_expiration;
213
		$hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
214
215
		if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) {
216
			return false;
217
		}
218
219
		return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
220
	}
221
222
	/**
223
	 * Get session data.
224
	 *
225
	 * @return array
226
	 */
227
	public function get_session_data() {
228
		return $this->has_session() ? (array) $this->get_session( $this->_customer_id ) : array();
229
	}
230
231
	public function generate_key($customer_id){
232
        if(!$customer_id){
233
            return;
234
        }
235
236
        return 'wpi_trans_'.$customer_id;
237
    }
238
239
	/**
240
	 * Save data.
241
	 */
242
	public function save_data() {
243
		// Dirty if something changed - prevents saving nothing new.
244
		if ( $this->_dirty && $this->has_session() ) {
245
246
            set_transient( $this->generate_key($this->_customer_id), $this->_data, $this->_session_expiration);
247
248
			$this->_dirty = false;
249
		}
250
	}
251
252
	/**
253
	 * Destroy all session data.
254
	 */
255
	public function destroy_session() {
256
		$this->delete_session( $this->_customer_id );
257
		$this->forget_session();
258
	}
259
260
	/**
261
	 * Forget all session data without destroying it.
262
	 */
263
	public function forget_session() {
264
		$this->setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true );
265
266
		wpinv_empty_cart();
267
268
		$this->_data        = array();
269
		$this->_dirty       = false;
270
		$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...
271
	}
272
273
	/**
274
	 * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
275
	 *
276
	 * @param int $uid User ID.
277
	 * @return string
278
	 */
279
	public function nonce_user_logged_out( $uid ) {
280
		return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
281
	}
282
283
	/**
284
	 * Returns the session.
285
	 *
286
	 * @param string $customer_id Customer ID.
287
	 * @param mixed  $default Default session value.
288
	 * @return string|array
289
	 */
290
	public function get_session( $customer_id, $default = false ) {
291
292
		if ( defined( 'WP_SETUP_CONFIG' ) ) {
293
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by WPInv_Session_Handler::get_session of type string|array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
294
		}
295
296
        if ( !is_user_logged_in() ) {
297
            if(!wp_verify_nonce( $customer_id, 'wpinv-session-customer-id' )){
298
                return array();
299
            }
300
        }
301
302
        $key = $this->generate_key($customer_id);
303
        $value = get_transient($key);
304
305
        if ( !$value ) {
306
            $value = $default;
307
        }
308
309
		return maybe_unserialize( $value );
310
	}
311
312
	/**
313
	 * Delete the session from the cache and database.
314
	 *
315
	 * @param int $customer_id Customer ID.
316
	 */
317
	public function delete_session( $customer_id ) {
318
319
        $key = $this->generate_key($customer_id);
320
321
		delete_transient($key);
322
	}
323
324
	/**
325
	 * Update the session expiry timestamp.
326
	 *
327
	 * @param string $customer_id Customer ID.
328
	 * @param int    $timestamp Timestamp to expire the cookie.
329
	 */
330
	public function update_session_timestamp( $customer_id, $timestamp ) {
331
332
        set_transient( $this->generate_key($customer_id), maybe_serialize( $this->_data ), $timestamp);
333
334
	}
335
}
336
337
global $wpi_session;
338
$wpi_session = new WPInv_Session_Handler();