PluginProbe
Sessions / 2.3.1
Sessions v2.3.1
2.1.0 2.10.0 2.11.0 2.12.0 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.2.0 2.3.0 2.3.1 2.4.0 2.4.1 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.1.0 3.1.1 All 39 releases
sessions / includes / system / class-session.php

class-session.php in Sessions 2.3.1, at includes/system/class-session.php

1,203 lines 35.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Session handling
4 *
5 * Handles all session operations and detection.
6 *
7 * @package System
8 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
9 * @since 1.0.0
10 */
11
12 namespace POSessions\System;
13
14 use POSessions\System\Environment;
15 use POSessions\System\Role;
16 use POSessions\System\Option;
17
18 use POSessions\System\Hash;
19 use POSessions\System\User;
20 use POSessions\System\GeoIP;
21 use POSessions\System\UserAgent;
22 use POSessions\Plugin\Feature\Schema;
23 use POSessions\Plugin\Feature\Capture;
24 use POSessions\Plugin\Feature\LimiterTypes;
25 use POSessions\System\IP;
26
27 /**
28 * Define the session functionality.
29 *
30 * Handles all session operations and detection.
31 *
32 * @package System
33 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
34 * @since 1.0.0
35 */
36 class Session {
37
38 /**
39 * The current user ID.
40 *
41 * @since 1.0.0
42 * @var integer $user_id The current user ID.
43 */
44 private $user_id = 0;
45
46 /**
47 * The current user.
48 *
49 * @since 1.0.0
50 * @var \WP_User $user The current user.
51 */
52 private $user = null;
53
54 /**
55 * The user's sessions.
56 *
57 * @since 1.0.0
58 * @var array $sessions The user's sessions.
59 */
60 private $sessions = [];
61
62 /**
63 * The user's distinct sessions IP.
64 *
65 * @since 1.1.0
66 * @var array $ip The user's distinct sessions IP.
67 */
68 private $ip = [];
69
70 /**
71 * The current token.
72 *
73 * @since 1.0.0
74 * @var string $token The current token.
75 */
76 private $token = '';
77
78 /**
79 * The class instance.
80 *
81 * @since 1.0.0
82 * @var $object $instance The class instance.
83 */
84 private static $instance = null;
85
86 /**
87 * Create an instance.
88 *
89 * @param mixed $user Optional, the user or user ID.
90 * @since 1.0.0
91 */
92 public function __construct( $user = null ) {
93 $this->load_user( $user );
94 }
95
96 /**
97 * Create an instance.
98 *
99 * @param mixed $user Optional, the user or user ID.
100 * @since 1.0.0
101 */
102 private function load_user( $user = null ) {
103 if ( ! isset( $user ) ) {
104 $this->user_id = get_current_user_id();
105 } else {
106 if ( $user instanceof \WP_User ) {
107 $this->user_id = $user->ID;
108 } elseif ( is_int( $user ) ) {
109 $this->user_id = $user;
110 } else {
111 $this->user_id = 0;
112 }
113 }
114 $this->sessions = self::get_user_sessions( $this->user_id );
115 if ( $this->is_needed() ) {
116 $this->user = get_user_by( 'id', $this->user_id );
117 if ( ! $this->user ) {
118 $this->user = null;
119 }
120 }
121 }
122
123 /**
124 * Verify if the instance is needed.
125 *
126 * @return boolean True if the features are needed, false otherwise.
127 * @since 1.0.0
128 */
129 public function is_needed() {
130 return is_int( $this->user_id ) && 0 < $this->user_id;
131 }
132
133 /**
134 * Get the number of active sessions.
135 *
136 * @return integer The number of sessions.
137 * @since 1.0.0
138 */
139 public function get_sessions_count() {
140 if ( isset( $this->sessions ) ) {
141 return count( $this->sessions );
142 }
143 return 0;
144 }
145
146 /**
147 * Get the user id.
148 *
149 * @return integer The user id.
150 * @since 1.0.0
151 */
152 public function get_user_id() {
153 return $this->user_id;
154 }
155
156 /**
157 * Modifies cookies durations.
158 *
159 * @param int $expiration Duration of the expiration period in seconds.
160 * @param int $user_id User ID.
161 * @param bool $remember Whether to remember the user login. Default false.
162 * @return int New duration of the expiration period in seconds.
163 * @since 1.0.0
164 */
165 public function cookie_expiration( $expiration, $user_id = null, $remember = false ) {
166 if ( ! isset( $this->user ) || ( isset( $user_id ) && $user_id !== $this->user_id ) ) {
167 return $expiration;
168 }
169 return (int) $this->get_privileges_for_user()['modes'][ $remember ? 'rttl' : 'ttl' ] * HOUR_IN_SECONDS;
170 }
171
172 /**
173 * Verify if the ip range is allowed.
174 *
175 * @param string $block The ip block ode.
176 * @return string 'allow' or 'disallow'.
177 * @since 1.0.0
178 */
179 private function verify_ip_range( $block ) {
180 if ( ! in_array( $block, [ 'none', 'external', 'local', 'all' ], true ) ) {
181 \DecaLog\Engine::eventsLogger( POSE_SLUG )->warning( 'IP range limitation set to "Allow For All".', [ 'code' => 202 ] );
182 return 'allow';
183 }
184 if ( 'none' === $block ) {
185 return 'allow';
186 }
187 if ( 'external' === $block && IP::is_current_private() ) {
188 return 'allow';
189 }
190 if ( 'local' === $block && IP::is_current_public() ) {
191 return 'allow';
192 }
193 return 'disallow';
194 }
195
196 /**
197 * Verify if the max number of ip.
198 *
199 * @param integer $maxip The ip max number.
200 * @return string 'allow' or 'disallow'.
201 * @since 1.1.0
202 */
203 private function verify_ip_max( $maxip ) {
204 if ( 0 === $maxip || in_array( IP::get_current(), $this->ip, true ) ) {
205 return 'allow';
206 }
207 if ( $maxip > count( $this->ip ) ) {
208 return 'allow';
209 }
210 return 'disallow';
211 }
212
213 /**
214 * Verify if the maximum allowed is reached.
215 *
216 * @param integer $limit The maximum allowed.
217 * @return string 'allow' or the token of the overridable if maximum is reached.
218 * @since 1.0.0
219 */
220 private function verify_per_user_limit( $limit ) {
221 if ( 0 === $limit ) {
222 return 'allow';
223 }
224 if ( is_array( $this->sessions ) && $limit > count( $this->sessions ) ) {
225 return 'allow';
226 }
227 if ( ! is_array( $this->sessions ) ) {
228 return 'allow';
229 }
230 uasort(
231 $this->sessions,
232 function ( $a, $b ) {
233 if ( $a['login'] === $b['login'] ) {
234 return 0;
235 } return ( $a['login'] < $b['login'] ) ? -1 : 1;
236 }
237 );
238 if ( $limit < count( $this->sessions ) ) {
239 $this->sessions = array_slice( $this->sessions, 1 );
240 do_action( 'sessions_force_terminate', $this->user_id );
241 self::set_user_sessions( $this->sessions, $this->user_id );
242 return $this->verify_per_user_limit( $limit );
243 }
244 return array_key_first( $this->sessions );
245 }
246
247 /**
248 * Verify if the maximum allowed is reached.
249 *
250 * @param integer $limit The maximum allowed.
251 * @return string 'allow' or the token of the overridable if maximum is reached.
252 * @since 1.0.0
253 */
254 private function verify_per_ip_limit( $limit ) {
255 if ( 0 === $limit ) {
256 return 'allow';
257 }
258 if ( ! is_array( $this->sessions ) ) {
259 return 'allow';
260 }
261 $ip = IP::get_current();
262 $compare = [];
263 $buffer = [];
264 foreach ( $this->sessions as $token => $session ) {
265 if ( IP::expand( $session['ip'] ) === $ip ) {
266 $compare[ $token ] = $session;
267 } else {
268 $buffer[ $token ] = $session;
269 }
270 }
271 if ( $limit > count( $compare ) ) {
272 return 'allow';
273 }
274 uasort(
275 $compare,
276 function ( $a, $b ) {
277 if ( $a['login'] === $b['login'] ) {
278 return 0;
279 } return ( $a['login'] < $b['login'] ) ? -1 : 1;
280 }
281 );
282 if ( $limit < count( $compare ) ) {
283 $compare = array_slice( $compare, 1 );
284 do_action( 'sessions_force_terminate', $this->user_id );
285 $this->sessions = array_merge( $compare, $buffer );
286 self::set_user_sessions( $this->sessions, $this->user_id );
287 return $this->verify_per_user_limit( $limit );
288 }
289 return array_key_first( $compare );
290 }
291
292 /**
293 * Verify if the maximum allowed is reached.
294 *
295 * @param integer $limit The maximum allowed.
296 * @return string 'allow' or the token of the overridable if maximum is reached.
297 * @since 1.0.0
298 */
299 private function verify_per_country_limit( $limit ) {
300 if ( 0 === $limit ) {
301 return 'allow';
302 }
303 if ( ! is_array( $this->sessions ) ) {
304 return 'allow';
305 }
306 $ip = IP::get_current();
307 $geo = new GeoIP();
308 $country = $geo->get_iso3166_alpha2( $ip );
309 $compare = [];
310 $buffer = [];
311 foreach ( $this->sessions as $token => $session ) {
312 if ( $country === $geo->get_iso3166_alpha2( $session['ip'] ) ) {
313 $compare[ $token ] = $session;
314 } else {
315 $buffer[ $token ] = $session;
316 }
317 }
318 if ( $limit > count( $compare ) ) {
319 return 'allow';
320 }
321 uasort(
322 $compare,
323 function ( $a, $b ) {
324 if ( $a['login'] === $b['login'] ) {
325 return 0;
326 } return ( $a['login'] < $b['login'] ) ? -1 : 1;
327 }
328 );
329 if ( $limit < count( $compare ) ) {
330 $compare = array_slice( $compare, 1 );
331 do_action( 'sessions_force_terminate', $this->user_id );
332 $this->sessions = array_merge( $compare, $buffer );
333 self::set_user_sessions( $this->sessions, $this->user_id );
334 return $this->verify_per_user_limit( $limit );
335 }
336 return array_key_first( $compare );
337 }
338
339 /**
340 * Verify if the maximum allowed is reached.
341 *
342 * @param string $ua The user agent.
343 * @param string $selector The selector ('device-class', 'device-type', 'device-client',...).
344 * @return string The requested ID.
345 * @since 1.0.0
346 */
347 private function get_device_id( $ua, $selector ) {
348 $device = UserAgent::get( $ua );
349 switch ( $selector ) {
350 case 'device-class':
351 if ( $device->class_is_bot ) {
352 return 'bot';
353 }
354 if ( $device->class_is_mobile ) {
355 return 'mobile';
356 }
357 if ( $device->class_is_desktop ) {
358 return 'desktop';
359 }
360 return 'other';
361 case 'device-type':
362 if ( $device->device_is_smartphone ) {
363 return 'smartphone';
364 }
365 if ( $device->device_is_featurephone ) {
366 return 'featurephone';
367 }
368 if ( $device->device_is_tablet ) {
369 return 'tablet';
370 }
371 if ( $device->device_is_phablet ) {
372 return 'phablet';
373 }
374 if ( $device->device_is_console ) {
375 return 'console';
376 }
377 if ( $device->device_is_portable_media_player ) {
378 return 'portable-media-player';
379 }
380 if ( $device->device_is_car_browser ) {
381 return 'car-browser';
382 }
383 if ( $device->device_is_tv ) {
384 return 'tv';
385 }
386 if ( $device->device_is_smart_display ) {
387 return 'smart-display';
388 }
389 if ( $device->device_is_camera ) {
390 return 'camera';
391 }
392 return 'other';
393 case 'device-client':
394 if ( $device->client_is_browser ) {
395 return 'browser';
396 }
397 if ( $device->client_is_feed_reader ) {
398 return 'feed-reader';
399 }
400 if ( $device->client_is_mobile_app ) {
401 return 'mobile-app';
402 }
403 if ( $device->client_is_pim ) {
404 return 'pim';
405 }
406 if ( $device->client_is_library ) {
407 return 'library';
408 }
409 if ( $device->client_is_media_player ) {
410 return 'media-payer';
411 }
412 return 'other';
413 case 'device-browser':
414 return $device->client_short_name;
415 case 'device-os':
416 return $device->os_short_name;
417 }
418 return '';
419 }
420
421 /**
422 * Verify if the maximum allowed is reached.
423 *
424 * @param string $selector The selector ('device-class', 'device-type', 'device-client',...).
425 * @param integer $limit The maximum allowed.
426 * @return string 'allow' or the token of the overridable if maximum is reached.
427 * @since 1.0.0
428 */
429 private function verify_per_device_limit( $selector, $limit ) {
430 if ( 0 === $limit ) {
431 return 'allow';
432 }
433 if ( ! is_array( $this->sessions ) ) {
434 return 'allow';
435 }
436 $device = $this->get_device_id( '', $selector );
437 $compare = [];
438 $buffer = [];
439 foreach ( $this->sessions as $token => $session ) {
440 if ( $device === $this->get_device_id( $session['ua'], $selector ) ) {
441 $compare[ $token ] = $session;
442 } else {
443 $buffer[ $token ] = $session;
444 }
445 }
446 if ( $limit > count( $compare ) ) {
447 return 'allow';
448 }
449 uasort(
450 $compare,
451 function ( $a, $b ) {
452 if ( $a['login'] === $b['login'] ) {
453 return 0;
454 } return ( $a['login'] < $b['login'] ) ? -1 : 1;
455 }
456 );
457 if ( $limit < count( $compare ) ) {
458 $compare = array_slice( $compare, 1 );
459 do_action( 'sessions_force_terminate', $this->user_id );
460 $this->sessions = array_merge( $compare, $buffer );
461 self::set_user_sessions( $this->sessions, $this->user_id );
462 return $this->verify_per_user_limit( $limit );
463 }
464 return array_key_first( $compare );
465 }
466
467 /**
468 * Enforce sessions limitation if needed.
469 *
470 * @param string $message The error message.
471 * @param integer $error The error code.
472 * @since 1.0.0
473 */
474 private function die( $message, $error ) {
475 Capture::login_block( $this->user_id );
476 wp_die( $message, $error );
477 }
478
479 /**
480 * Enforce sessions limitation if needed.
481 *
482 * @param \WP_User|false|null $user Local User information.
483 * @param object $user_data WordPress.com User Login information.
484 * @since 1.0.0
485 */
486 public function jetpack_sso_handle_login( $user, $user_data ) {
487 $this->load_user( $user );
488 $this->init_if_needed();
489 $this->limit_logins( $user, '', '', true );
490 }
491
492 /**
493 * Computes privileges for a set of roles.
494 *
495 * @param array $roles The set of roles for which the privileges must be computed.
496 * @return array The privileges.
497 * @since 2.0.0
498 */
499 private function get_privileges_for_roles( $roles ) {
500 $result = [];
501 $settings = Option::roles_get();
502 $methods = [ 'block', 'default', 'override' ];
503 $block_none = false;
504 $block_external = false;
505 $block_local = false;
506 $limits = [];
507 if ( 0 === (int) Option::network_get( 'rolemode' ) ) { // Cumulative privileges.
508 $idle = -1;
509 $maxip = -1;
510 $ttl = 0;
511 $rttl = 0;
512 $method = '';
513 foreach ( $roles as $role ) {
514 // Blocked IP ranges
515 switch ( $settings[ $role ]['block'] ) {
516 case 'none':
517 $block_none = true;
518 break;
519 case 'external':
520 $block_external = true;
521 break;
522 case 'local':
523 $block_local = true;
524 break;
525 }
526 // Limits
527 if ( 'none' === $settings[ $role ]['limit'] ) {
528 $limits['none'] = true;
529 } else {
530 foreach ( [ 'user', 'country', 'ip', 'device-class', 'device-type', 'device-client', 'device-browser', 'device-os' ] as $type ) {
531 if ( 0 === strpos( $settings[ $role ]['limit'], $type . '-' ) ) {
532 $value = (int) substr( $settings[ $role ]['limit'], strlen( $type ) + 1 );
533 if ( array_key_exists( $type, $limits ) ) {
534 if ( $limits[ $type ] < $value ) {
535 $limits[ $type ] = $value;
536 }
537 } else {
538 $limits[ $type ] = $value;
539 }
540 }
541 }
542 }
543 // Method
544 if ( '' === $method ) {
545 $method = $settings[ $role ]['method'];
546 } else {
547 $current = array_search( $method, $methods, true );
548 $new = array_search( $settings[ $role ]['method'], $methods, true );
549 if ( false !== $new && false !== $current && $new > $current ) {
550 $method = $settings[ $role ]['method'];
551 }
552 }
553 // Max idle days
554 if ( 0 === $settings[ $role ]['idle'] ) {
555 $idle = 0;
556 } elseif ( $settings[ $role ]['idle'] > $idle && 0 !== $idle ) {
557 $idle = $settings[ $role ]['idle'];
558 }
559 // Max number of IPs
560 if ( 0 !== $maxip ) {
561 if ( 0 === $settings[ $role ]['maxip'] ) {
562 $maxip = 0;
563 } elseif ( $settings[ $role ]['maxip'] > $maxip ) {
564 $maxip = $settings[ $role ]['maxip'];
565 }
566 }
567 // Cookie TTL
568 if ( $settings[ $role ]['cookie-ttl'] > $ttl ) {
569 $ttl = $settings[ $role ]['cookie-ttl'];
570 }
571 // Cookie R-TTL
572 if ( $settings[ $role ]['cookie-rttl'] > $rttl ) {
573 $rttl = $settings[ $role ]['cookie-rttl'];
574 }
575 }
576 } else { // Least privileges.
577 $idle = PHP_INT_MAX;
578 $maxip = PHP_INT_MAX;
579 $ttl = PHP_INT_MAX;
580 $rttl = PHP_INT_MAX;
581 $method = '';
582 foreach ( $roles as $role ) {
583 // Blocked IP ranges
584 switch ( $settings[ $role ]['block'] ) {
585 case 'none':
586 $block_none = true;
587 break;
588 case 'external':
589 $block_external = true;
590 break;
591 case 'local':
592 $block_local = true;
593 break;
594 }
595 // Limits
596 if ( 'none' === $settings[ $role ]['limit'] ) {
597 $limits['none'] = true;
598 } else {
599 foreach ( [ 'user', 'country', 'ip', 'device-class', 'device-type', 'device-client', 'device-browser', 'device-os' ] as $type ) {
600 if ( 0 === strpos( $settings[ $role ]['limit'], $type . '-' ) ) {
601 $value = (int) substr( $settings[ $role ]['limit'], strlen( $type ) + 1 );
602 if ( array_key_exists( $type, $limits ) ) {
603 if ( $limits[ $type ] > $value ) {
604 $limits[ $type ] = $value;
605 }
606 } else {
607 $limits[ $type ] = $value;
608 }
609 }
610 }
611 }
612 // Method
613 if ( '' === $method ) {
614 $method = $settings[ $role ]['method'];
615 } else {
616 $current = array_search( $method, $methods, true );
617 $new = array_search( $settings[ $role ]['method'], $methods, true );
618 if ( false !== $new && false !== $current && $new < $current ) {
619 $method = $settings[ $role ]['method'];
620 }
621 }
622 // Max idle days
623 if ( $settings[ $role ]['idle'] < $idle && 0 !== $settings[ $role ]['idle'] ) {
624 $idle = $settings[ $role ]['idle'];
625 } elseif ( 0 === $settings[ $role ]['idle'] && PHP_INT_MAX === $idle ) {
626 $idle = 0;
627 }
628 // Max number of IPs
629 if ( 0 === $settings[ $role ]['maxip'] ) {
630 $settings[ $role ]['maxip'] = PHP_INT_MAX;
631 }
632 if ( $settings[ $role ]['maxip'] < $maxip ) {
633 $maxip = $settings[ $role ]['maxip'];
634 }
635 // Cookie TTL
636 if ( $settings[ $role ]['cookie-ttl'] < $ttl ) {
637 $ttl = $settings[ $role ]['cookie-ttl'];
638 }
639 // Cookie R-TTL
640 if ( $settings[ $role ]['cookie-rttl'] < $rttl ) {
641 $rttl = $settings[ $role ]['cookie-rttl'];
642 }
643 }
644 }
645 // Blocked IP range computation
646 if ( 0 === (int) Option::network_get( 'rolemode' ) ) { // Cumulative privileges.
647 if ( ( $block_external && $block_local ) || $block_none ) {
648 $block = 'none';
649 } elseif ( $block_external ) {
650 $block = 'external';
651 }
652 elseif ( $block_local ) {
653 $block = 'local';
654 } else {
655 $block = 'none';
656 }
657 } else { // Least privileges.
658 if ( $block_external && $block_local ) {
659 $block = 'all';
660 } elseif ( $block_external ) {
661 $block = 'external';
662 }
663 elseif ( $block_local ) {
664 $block = 'local';
665 } else {
666 $block = 'none';
667 }
668 }
669 // Limits computation
670 if ( 0 === (int) Option::network_get( 'rolemode' ) ) { // Cumulative privileges.
671 if ( array_key_exists( 'none', $limits ) && $limits['none'] ) {
672 $limits = [];
673 }
674 } else { // Least privileges.
675 if ( array_key_exists( 'none', $limits ) && $limits['none'] && 1 === count( $limits ) ) {
676 $limits = [];
677 }
678 }
679 if ( array_key_exists( 'none', $limits ) ) {
680 unset( $limits['none'] );
681 }
682 // Max number of IPs
683 if ( PHP_INT_MAX !== $maxip && -1 !== $maxip ) {
684 $limits['ip'] = $maxip;
685 }
686 $modes['block'] = $block;
687 $modes['limits'] = $limits;
688 $modes['method'] = $method;
689 $modes['idle'] = $idle;
690 $modes['ttl'] = $ttl;
691 $modes['rttl'] = $rttl;
692 $result['roles'] = $roles;
693 $result['modes'] = $modes;
694 return $result;
695 }
696
697 /**
698 * Computes privileges for a user.
699 *
700 * @return array The privileges.
701 * @since 2.0.0
702 */
703 public function get_privileges_for_user() {
704 if ( Role::SUPER_ADMIN === Role::admin_type( $this->user_id ) || Role::SINGLE_ADMIN === Role::admin_type( $this->user_id ) || Role::LOCAL_ADMIN === Role::admin_type( $this->user_id ) ) {
705 $roles[] = 'administrator';
706 } else {
707 foreach ( Role::get_all() as $key => $detail ) {
708 if ( in_array( $key, $this->user->roles, true ) ) {
709 $roles[] = $key;
710 break;
711 }
712 }
713 }
714 return $this->get_privileges_for_roles( $roles );
715 }
716
717 /**
718 * Enforce sessions limitation if needed.
719 *
720 * @param mixed $user WP_User if the user is authenticated, WP_Error or null otherwise.
721 * @param string $username Username or email address.
722 * @param string $password User password.
723 * @param boolean $force_403 Optional. Force a 403 error if needed (in place of 'default' method).
724 * @return mixed WP_User if the user is allowed, WP_Error or null otherwise.
725 * @since 1.0.0
726 */
727 public function limit_logins( $user, $username, $password, $force_403 = false ) {
728 if ( -1 === (int) Option::network_get( 'rolemode' ) ) {
729 return $user;
730 }
731 if ( $user instanceof \WP_User ) {
732 $this->user_id = $user->ID;
733 $this->user = $user;
734 $this->sessions = self::get_user_sessions( $this->user_id );
735 $role = '';
736 $this->ip = [];
737 foreach ( $this->sessions as $session ) {
738 $ip = IP::expand( $session['ip'] );
739 if ( ! in_array( $ip, $this->ip, true ) ) {
740 $this->ip[] = $ip;
741 }
742 }
743 $privileges = $this->get_privileges_for_user()['modes'];
744 $result = $this->verify_ip_range( $privileges['block'] );
745 $mode = 'unknown';
746 if ( 'allow' === $result ) {
747 foreach ( $privileges['limits'] as $key => $limit ) {
748 $limit = (int) $limit;
749 if ( 0 < $limit ) {
750 switch ( $key ) {
751 case 'user':
752 $result = $this->verify_per_user_limit( $limit );
753 break;
754 case 'ip':
755 $result = $this->verify_ip_max( $limit );
756 break;
757 case 'country':
758 $result = $this->verify_per_country_limit( $limit );
759 break;
760 case 'device-class':
761 case 'device-type':
762 case 'device-client':
763 case 'device-browser':
764 case 'device-os':
765 $result = $this->verify_per_device_limit( $key, $limit );
766 break;
767 }
768 }
769 if ( 'allow' !== $result ) {
770 $mode = $key;
771 break;
772 }
773 }
774 } else {
775 \DecaLog\Engine::eventsLogger( POSE_SLUG )->warning( sprintf( 'New session not allowed on this IP range for %s.', User::get_user_string( $this->user_id ) ), [ 'code' => 403 ] );
776 $this->die( __( '<strong>FORBIDDEN</strong>: ', 'sessions' ) . apply_filters( 'sessions_bad_ip_message', __( 'You\'re not allowed to initiate a new session from your current IP address.', 'sessions' ) ), 403 );
777 }
778 if ( 'allow' !== $result ) {
779 $method = $privileges['method'];
780 if ( $force_403 && 'default' === $method ) {
781 $method = 'forced_403';
782 }
783 switch ( $method ) {
784 case 'override':
785 if ( '' !== $result ) {
786 if ( array_key_exists( $result, $this->sessions ) ) {
787 unset( $this->sessions[ $result ] );
788 do_action( 'sessions_force_terminate', $this->user_id );
789 self::set_user_sessions( $this->sessions, $this->user_id );
790 \DecaLog\Engine::eventsLogger( POSE_SLUG )->notice( sprintf( 'Session overridden for %s. Reason: %s.', User::get_user_string( $this->user_id ), $mode ) );
791 }
792 }
793 break;
794 case 'default':
795 \DecaLog\Engine::eventsLogger( POSE_SLUG )->warning( sprintf( 'New session not allowed for %s. Reason: %s.', User::get_user_string( $this->user_id ), $mode ), [ 'code' => 403 ] );
796 Capture::login_block( $this->user_id, true );
797 return new \WP_Error( '403', __( '<strong>ERROR</strong>: ', 'sessions' ) . apply_filters( 'sessions_blocked_message', __( 'You\'re not allowed to initiate a new session because your maximum number of active sessions has been reached.', 'sessions' ) ) );
798 default:
799 \DecaLog\Engine::eventsLogger( POSE_SLUG )->warning( sprintf( 'New session not allowed for %s. Reason: %s.', User::get_user_string( $this->user_id ), $mode ), [ 'code' => 403 ] );
800 $this->die( __( '<strong>FORBIDDEN</strong>: ', 'sessions' ) . apply_filters( 'sessions_blocked_message', __( 'You\'re not allowed to initiate a new session because your maximum number of active sessions has been reached.', 'sessions' ) ), 403 );
801 }
802 } else {
803 \DecaLog\Engine::eventsLogger( POSE_SLUG )->debug( sprintf( 'New session allowed for %s.', User::get_user_string( $this->user_id ) ), [ 'code' => 200 ] );
804 }
805 }
806 return $user;
807 }
808
809 /**
810 * Set the idle field if needed.
811 *
812 * @return boolean True if the features are needed, false otherwise.
813 * @since 1.0.0
814 */
815 private function set_idle() {
816 if ( ! $this->is_needed() || ! isset( $this->user ) ) {
817 return false;
818 }
819 if ( ! array_key_exists( $this->token, $this->sessions ) ) {
820 return false;
821 }
822 $privileges = $this->get_privileges_for_user()['modes'];
823 if ( 0 === (int) $privileges['idle'] ) {
824 if ( array_key_exists( 'session_idle', $this->sessions[ $this->token ] ) ) {
825 unset( $this->sessions[ $this->token ]['session_idle'] );
826 self::set_user_sessions( $this->sessions, $this->user_id );
827 }
828 return false;
829 }
830 if ( 100 < (int) $privileges['idle'] ) {
831 $this->sessions[ $this->token ]['session_idle'] = time() + (int) ( ( $privileges['idle'] - 100 ) * MINUTE_IN_SECONDS );
832 } else {
833 $this->sessions[ $this->token ]['session_idle'] = time() + (int) ( $privileges['idle'] * HOUR_IN_SECONDS );
834 }
835 self::set_user_sessions( $this->sessions, $this->user_id );
836 return true;
837 }
838
839 /**
840 * Set the ip field if needed.
841 *
842 * @return boolean True if the features are needed, false otherwise.
843 * @since 1.0.0
844 */
845 private function set_ip() {
846 if ( ! Option::network_get( 'followip' ) ) {
847 return false;
848 }
849 if ( ! $this->is_needed() || ! isset( $this->user ) ) {
850 return false;
851 }
852 if ( ! array_key_exists( $this->token, $this->sessions ) ) {
853 return false;
854 }
855 $this->sessions[ $this->token ]['ip'] = IP::expand( $_SERVER['REMOTE_ADDR'] );
856 self::set_user_sessions( $this->sessions, $this->user_id );
857 return true;
858 }
859
860 /**
861 * Get the limits as printable text.
862 *
863 * @return string The limits, ready to print.
864 * @since 1.0.0
865 */
866 public function get_limits_as_text() {
867 $privileges = $this->get_privileges_for_user()['modes'];
868 $result = '';
869 $restrict = [];
870 switch ( $privileges['block'] ) {
871 case 'external':
872 $result .= esc_html__( 'Login allowed only from private IP ranges.', 'sessions' ) . ' ';
873 break;
874 case 'local':
875 $result .= esc_html__( 'Login allowed only from public IP ranges.', 'sessions' ) . ' ';
876 break;
877 case 'all':
878 return esc_html__( 'Login is not allowed.', 'sessions' );
879 }
880 foreach ( $privileges['limits'] as $key => $limit ) {
881 $limit = (int) $limit;
882 if ( 0 < $limit ) {
883 switch ( $key ) {
884 case 'user':
885 $restrict[] = esc_html( sprintf( _n( '%d concurrent session.', '%d concurrent sessions.', $limit, 'sessions' ), $limit ) );
886 break;
887 case 'ip':
888 case 'country':
889 case 'device-class':
890 case 'device-type':
891 case 'device-client':
892 case 'device-browser':
893 case 'device-os':
894 $restrict[] = esc_html( sprintf( _n( '%d concurrent session per %s.', '%d concurrent sessions per %s.', $limit, 'sessions' ), $limit, LimiterTypes::$selector_names[ $key ] ) );
895 break;
896 }
897 }
898 }
899 if ( 0 < count( $restrict ) ) {
900 $result .= implode( ' ', $restrict ) . ' ';
901 }
902 if ( 100 < (int) $privileges['idle'] ) {
903 $result .= esc_html( sprintf( _n( 'Sessions expire after %d minute of inactivity.', 'Sessions expire after %d minutes of inactivity.', $privileges['idle'] - 100, 'sessions' ), $privileges['idle'] - 100 ) ) . ' ';
904 } elseif ( 0 !== (int) $privileges['idle'] ) {
905 $result .= esc_html( sprintf( _n( 'Sessions expire after %d hour of inactivity.', 'Sessions expire after %d hours of inactivity.', $privileges['idle'], 'sessions' ), $privileges['idle'] ) ) . ' ';
906 }
907 if ( '' === $result ) {
908 $result = esc_html__( 'No restrictions.', 'sessions' );
909 }
910 return $result;
911 }
912
913 /**
914 * Initialize hooks.
915 *
916 * @since 1.0.0
917 */
918 public static function init() {
919 if ( Option::network_get( 'forceip' ) ) {
920 $_SERVER['REMOTE_ADDR'] = IP::get_current();
921 }
922 add_action( 'init', [ self::class, 'initialize' ], PHP_INT_MAX );
923 add_action( 'set_current_user', [ self::class, 'initialize' ], PHP_INT_MAX );
924 }
925
926 /**
927 * Initialize properties if needed.
928 *
929 * @since 1.0.0
930 */
931 public function init_if_needed() {
932 if ( $this->is_needed() ) {
933 $this->token = Hash::simple_hash( wp_get_session_token(), false );
934 $this->set_idle();
935 $this->set_ip();
936 }
937 }
938
939 /**
940 * Initialize static properties.
941 *
942 * @since 1.0.0
943 */
944 public static function initialize() {
945 if ( ! isset( self::$instance ) ) {
946 self::$instance = new static();
947 self::$instance->init_if_needed();
948 add_filter( 'auth_cookie_expiration', [ self::$instance, 'cookie_expiration' ], PHP_INT_MAX, 3 );
949 add_filter( 'authenticate', [ self::$instance, 'limit_logins' ], PHP_INT_MAX, 3 );
950 add_filter( 'jetpack_sso_handle_login', [ self::$instance, 'jetpack_sso_handle_login' ], PHP_INT_MAX, 2 );
951 }
952 }
953
954 /**
955 * Get an element in a cookie.
956 *
957 * @param string $scheme The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
958 * @param string $element The element to retrieve.
959 * @return string The element.
960 * @since 1.0.0
961 */
962 public static function get_cookie_element( $scheme, $element ) {
963 $cookie_elements = wp_parse_auth_cookie( '', $scheme );
964 if ( ! $cookie_elements ) {
965 return '';
966 }
967 if ( array_key_exists( $element, $cookie_elements ) ) {
968 return (string) $cookie_elements[ $element ];
969 }
970 return '';
971 }
972
973 /**
974 * Get sessions.
975 *
976 * @param mixed $user_id Optional. The user ID.
977 * @return array The list of sessions.
978 * @since 1.0.0
979 */
980 public static function get_user_sessions( $user_id = false ) {
981 $result = [];
982 if ( ! $user_id ) {
983 $user_id = get_current_user_id();
984 }
985 if ( ! $user_id || ! is_int( $user_id ) ) {
986 return $result;
987 }
988 $result = get_user_meta( $user_id, 'session_tokens', true );
989 if ( ! is_array( $result ) && is_string( $result ) ) {
990 $result = maybe_unserialize( $result );
991 }
992 if ( ! is_array( $result ) ) {
993 $result = [];
994 }
995 return $result;
996 }
997
998 /**
999 * Get all sessions.
1000 *
1001 * @return array The details of sessions.
1002 * @since 1.0.0
1003 */
1004 public static function get_all_sessions() {
1005 global $wpdb;
1006 $sql = 'SELECT * FROM ' . $wpdb->usermeta . " WHERE meta_key = 'session_tokens' ORDER BY user_id DESC LIMIT " . (int) Option::network_get( 'buffer_limit' );
1007 // phpcs:ignore
1008 $result = $wpdb->get_results( $sql, ARRAY_A );
1009 foreach ( $result as &$record ) {
1010 if ( ! is_array( $record['meta_value'] ) && is_string( $record['meta_value'] ) ) {
1011 $record['meta_value'] = maybe_unserialize( $record['meta_value'] );
1012 }
1013 }
1014 return $result;
1015 }
1016
1017 /**
1018 * Set sessions.
1019 *
1020 * @param array $sessions The sessions records.
1021 * @param mixed $user_id Optional. The user ID.
1022 * @return boolean True if the operation was successful, false otherwise.
1023 * @since 1.0.0
1024 */
1025 public static function set_user_sessions( $sessions, $user_id = false ) {
1026 $result = false;
1027 if ( ! $user_id ) {
1028 $user_id = get_current_user_id();
1029 }
1030 if ( ! $user_id || ! is_int( $user_id ) ) {
1031 return $result;
1032 }
1033 return (bool) update_user_meta( $user_id, 'session_tokens', $sessions );
1034 }
1035
1036 /**
1037 * Terminate sessions needing to be terminated.
1038 *
1039 * @param array $sessions The sessions records.
1040 * @param integer $user_id The user ID.
1041 * @return integer Number of terminated sessions.
1042 * @since 1.0.0
1043 */
1044 public static function auto_terminate_session( $sessions, $user_id ) {
1045 $span = \DecaLog\Engine::tracesLogger( POSE_SLUG )->startSpan( 'Sessions auto-terminating', DECALOG_SPAN_SHUTDOWN );
1046 $idle = [];
1047 $exp = [];
1048 foreach ( $sessions as $token => $session ) {
1049 if ( array_key_exists( 'session_idle', $session ) && time() > $session['session_idle'] ) {
1050 $idle[] = $token;
1051 } elseif ( array_key_exists( 'expiration', $session ) && time() > $session['expiration'] ) {
1052 $exp[] = $token;
1053 }
1054 }
1055 foreach ( $idle as $token ) {
1056 unset( $sessions[ $token ] );
1057 do_action( 'sessions_after_idle_terminate', $user_id );
1058 }
1059 foreach ( $exp as $token ) {
1060 unset( $sessions[ $token ] );
1061 do_action( 'sessions_after_expired_terminate', $user_id );
1062 }
1063 self::set_user_sessions( $sessions, $user_id );
1064 \DecaLog\Engine::tracesLogger( POSE_SLUG )->endSpan( $span );
1065 return count( $idle ) + count( $exp );
1066 }
1067
1068
1069
1070 /**
1071 * Delete all sessions.
1072 *
1073 * @param integer $user_id Optional. Delete only for this user.
1074 * @return int|bool False if it was not possible, otherwise the number of deleted meta.
1075 * @since 1.0.0
1076 */
1077 public static function delete_all_sessions( $user_id = null) {
1078 if ( Role::SUPER_ADMIN === Role::admin_type() || Role::SINGLE_ADMIN === Role::admin_type() || 1 === Environment::exec_mode() ) {
1079 $id = get_current_user_id();
1080 if ( ( isset( $id ) && is_integer( $id ) && 0 < $id ) || 1 === Environment::exec_mode() ) {
1081 $span = \DecaLog\Engine::tracesLogger( POSE_SLUG )->startSpan( 'Sessions deleting', DECALOG_SPAN_MAIN_RUN );
1082 if ( isset( $user_id ) && is_integer( $user_id ) && 0 < $user_id ) {
1083 $criteria = " AND user_id = '" . $user_id . "'";
1084 } else {
1085 $criteria = '';
1086 }
1087 $users = 0;
1088 $sessions = 0;
1089 global $wpdb;
1090 $sql = "SELECT COUNT(*) AS users, SUM( CAST( SUBSTRING(`meta_value`,3,POSITION('{' IN `meta_value`) - 4) AS UNSIGNED)) AS sessions FROM " . $wpdb->usermeta . " WHERE `meta_key`='session_tokens' AND `meta_value`<>'' AND `meta_value`<>'a:0:{}' AND user_id <> '" . $id . "'" . $criteria;
1091 // phpcs:ignore
1092 $query = $wpdb->get_results( $sql, ARRAY_A );
1093 if ( is_array( $query ) && 0 < count( $query ) ) {
1094 $users = $query[0]['users'];
1095 $sessions = $query[0]['sessions'];
1096 }
1097 $count = $wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key='session_tokens' AND user_id <> '" . $id . "'" . $criteria );
1098 if ( false === $count ) {
1099 \DecaLog\Engine::eventsLogger( POSE_SLUG )->warning( 'Unable to delete all sessions.' );
1100 \DecaLog\Engine::tracesLogger( POSE_SLUG )->endSpan( $span );
1101 return $count;
1102 } else {
1103 if ( isset( $user_id ) && is_integer( $user_id ) && 0 < $user_id ) {
1104 $cpt = 0;
1105 } else {
1106 $cpt = self::delete_remaining_sessions();
1107 }
1108 if ( 0 < $cpt ) {
1109 $sessions += $cpt;
1110 }
1111 if ( 0 === $sessions ) {
1112 \DecaLog\Engine::eventsLogger( POSE_SLUG )->notice( 'No sessions to delete.' );
1113 } else {
1114 do_action( 'sessions_force_admin_terminate', $sessions );
1115 \DecaLog\Engine::eventsLogger( POSE_SLUG )->notice( sprintf( 'All sessions have been deleted (%d deleted meta).', $sessions ) );
1116 }
1117 \DecaLog\Engine::tracesLogger( POSE_SLUG )->endSpan( $span );
1118 return $sessions;
1119 }
1120 } else {
1121 \DecaLog\Engine::eventsLogger( POSE_SLUG )->alert( 'An unknown user attempted to delete all active sessions.' );
1122 return false;
1123 }
1124 } else {
1125 \DecaLog\Engine::eventsLogger( POSE_SLUG )->alert( 'A non authorized user attempted to delete all active sessions.' );
1126 return false;
1127 }
1128 }
1129
1130 /**
1131 * Delete remaining sessions.
1132 *
1133 * @return int|bool False if it was not possible, otherwise the number of deleted sessions.
1134 * @since 1.0.0
1135 */
1136 public static function delete_remaining_sessions() {
1137 if ( Role::SUPER_ADMIN === Role::admin_type() || Role::SINGLE_ADMIN === Role::admin_type() ) {
1138 $user_id = get_current_user_id();
1139 $selftoken = Hash::simple_hash( wp_get_session_token(), false );
1140 if ( isset( $user_id ) && is_integer( $user_id ) && 0 < $user_id ) {
1141 $sessions = self::get_user_sessions( $user_id );
1142 $cpt = count( $sessions ) - 1;
1143 if ( is_array( $sessions ) ) {
1144 foreach ( array_diff_key( array_keys( $sessions ), [ $selftoken ] ) as $key ) {
1145 unset( $sessions[ $key ] );
1146 }
1147 self::set_user_sessions( $sessions, $user_id );
1148 return $cpt;
1149 } else {
1150 return 0;
1151 }
1152 } else {
1153 \DecaLog\Engine::eventsLogger( POSE_SLUG )->alert( 'An unknown user attempted to delete all active sessions.' );
1154 return false;
1155 }
1156 } else {
1157 \DecaLog\Engine::eventsLogger( POSE_SLUG )->alert( 'A non authorized user attempted to delete all active sessions.' );
1158 return false;
1159 }
1160 }
1161
1162 /**
1163 * Delete selected sessions.
1164 *
1165 * @param array $bulk The sessions to delete.
1166 * @return int|bool False if it was not possible, otherwise the number of deleted meta.
1167 * @since 1.0.0
1168 */
1169 public static function delete_selected_sessions( $bulk ) {
1170 if ( Role::SUPER_ADMIN === Role::admin_type() || Role::SINGLE_ADMIN === Role::admin_type() ) {
1171 $span = \DecaLog\Engine::tracesLogger( POSE_SLUG )->startSpan( 'Sessions deleting', DECALOG_SPAN_MAIN_RUN );
1172 $selftoken = Hash::simple_hash( wp_get_session_token(), false );
1173 $count = 0;
1174 foreach ( $bulk as $id ) {
1175 $val = explode( ':', $id );
1176 if ( 2 === count( $val ) ) {
1177 $token = (string) $val[1];
1178 $user_id = (int) $val[0];
1179 $sessions = self::get_user_sessions( $user_id );
1180 if ( $selftoken !== $token ) {
1181 unset( $sessions[ $token ] );
1182 if ( self::set_user_sessions( $sessions, $user_id ) ) {
1183 ++$count;
1184 }
1185 }
1186 }
1187 }
1188 if ( 0 === $count ) {
1189 \DecaLog\Engine::eventsLogger( POSE_SLUG )->notice( 'No sessions to delete.' );
1190 } else {
1191 do_action( 'sessions_force_admin_terminate', $count );
1192 \DecaLog\Engine::eventsLogger( POSE_SLUG )->notice( sprintf( 'All selected sessions have been deleted (%d deleted sessions).', $count ) );
1193 }
1194 \DecaLog\Engine::tracesLogger( POSE_SLUG )->endSpan( $span );
1195 return $count;
1196 } else {
1197 \DecaLog\Engine::eventsLogger( POSE_SLUG )->alert( 'A non authorized user attempted to delete some active sessions.' );
1198 return false;
1199 }
1200 }
1201
1202 }
1203