PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.21
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.21
1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 All 167 releases
← All changes | includes/Services/Auth.php +158 -311 1.10.3 → 1.10.21 View file →
@@ -20,8 +20,29 @@
20 20 * Auth Service class.
21 21 */
22 22 class Auth {
23 23 /**
24 + * Maximum retained idle sessions.
25 + *
26 + * @deprecated Use Session_Registry::MAX_SESSIONS_PER_USER.
27 + */
28 + public const MAX_SESSIONS_PER_USER = Session_Registry::MAX_SESSIONS_PER_USER;
29 +
30 + /**
31 + * Minimum idle time before eviction.
32 + *
33 + * @deprecated Use Session_Registry::SESSION_EVICTION_IDLE_SECONDS.
34 + */
35 + public const SESSION_EVICTION_IDLE_SECONDS = Session_Registry::SESSION_EVICTION_IDLE_SECONDS;
36 +
37 + /**
38 + * Session row byte ceiling.
39 + *
40 + * @deprecated Use Session_Registry::MAX_SESSIONS_ROW_BYTES.
41 + */
42 + public const MAX_SESSIONS_ROW_BYTES = Session_Registry::MAX_SESSIONS_ROW_BYTES;
43 +
44 + /**
24 45 * The single instance of the class.
25 46 *
26 47 * @var null|Auth
27 48 */
@@ -27,15 +48,32 @@
27 48 */
28 49 private static $instance = null;
29 50
30 51 /**
52 + * Session storage.
53 + *
54 + * @var Session_Registry
55 + */
56 + private $sessions;
57 +
58 + /**
31 59 * Constructor is private to prevent direct instantiation.
32 60 * Or Auth::instance() instead.
33 61 */
34 62 public function __construct() {
63 + $this->sessions = new Session_Registry();
35 64 }
36 65
37 66 /**
67 + * Get the session registry.
68 + *
69 + * @return Session_Registry
70 + */
71 + public function sessions(): Session_Registry {
72 + return $this->sessions;
73 + }
74 +
75 + /**
38 76 * Gets the singleton instance.
39 77 *
40 78 * @return Auth
41 79 */
@@ -47,8 +85,46 @@
47 85 return self::$instance;
48 86 }
49 87
50 88 /**
89 + * On `wp_logout`: end the web POS session named by this browser's cookie.
90 + *
91 + * Sessions of native apps and other browsers stay live. The cookie itself is left alone.
92 + *
93 + * @param mixed $user_id ID of the user logging out.
94 + */
95 + public static function revoke_web_session_on_logout( $user_id ): void {
96 + if ( absint( $user_id ) > 0 ) {
97 + self::instance()->cleanup_previous_web_session( absint( $user_id ) );
98 + }
99 + }
100 +
101 + /**
102 + * On `password_reset`: end every POS session of the user.
103 + *
104 + * @param mixed $user User whose password is being reset.
105 + */
106 + public static function revoke_sessions_on_password_reset( $user ): void {
107 + if ( $user instanceof WP_User ) {
108 + self::instance()->revoke_all_refresh_tokens( $user->ID );
109 + }
110 + }
111 +
112 + /**
113 + * On `profile_update`: end every POS session of the user when the password changed.
114 + *
115 + * @param mixed $user_id ID of the updated user.
116 + * @param mixed $old_user_data User data before the update.
117 + */
118 + public static function revoke_sessions_on_password_change( $user_id, $old_user_data = null ): void {
119 + $user = get_userdata( absint( $user_id ) );
120 +
121 + if ( $old_user_data instanceof WP_User && $user instanceof WP_User && $old_user_data->user_pass !== $user->user_pass ) {
122 + self::instance()->revoke_all_refresh_tokens( $user->ID );
123 + }
124 + }
125 +
126 + /**
51 127 * Extract a WCPOS token from an authorization value.
52 128 *
53 129 * @param mixed $auth_value Authorization value.
54 130 *
@@ -211,12 +287,36 @@
211 287 'Session has been revoked',
212 288 array( 'status' => 403 )
213 289 );
214 290 }
291 +
292 + // The session registry is authoritative; the blacklist transient above is only a
293 + // fast path that can be evicted or purged. Once the session is live, record that,
294 + // so eviction can tell a device working right now from one unseen for a week.
295 + if ( isset( $decoded_token->refresh_jti ) ) {
296 + $user_id = absint( $decoded_token->data->user->id );
297 + $refresh_jti = (string) $decoded_token->refresh_jti;
298 +
299 + if ( ! $this->sessions->is_live( $user_id, $refresh_jti ) ) {
300 + return new WP_Error(
301 + 'woocommerce_pos_auth_session_revoked',
302 + 'Session has been revoked',
303 + array( 'status' => 403 )
304 + );
305 + }
306 +
307 + $this->sessions->touch( $user_id, $refresh_jti );
308 + }
215 309 }
216 310
217 311 // Everything looks good return the decoded token.
218 312 return $decoded_token;
313 + } catch ( \WCPOS\Vendor\Firebase\JWT\ExpiredException $e ) {
314 + return new WP_Error(
315 + 'woocommerce_pos_auth_token_expired',
316 + 'Token expired',
317 + array( 'status' => 403 )
318 + );
219 319 } catch ( Exception $e ) {
220 320 // Something is wrong trying to decode the token, send back the error.
221 321 return new WP_Error(
222 322 'woocommmerce_pos_auth_invalid_token',
@@ -314,9 +414,9 @@
314 414 $access_jti = null === $access_jti ? $jti : (string) $access_jti;
315 415
316 416 if ( null !== $linked_refresh_jti ) {
317 417 $linked_refresh_jti = (string) $linked_refresh_jti;
318 - $this->store_access_token_expiry( $user->ID, $linked_refresh_jti, $expires_at );
418 + $this->sessions->record_access_expiry( $user->ID, $linked_refresh_jti, $expires_at );
319 419 }
320 420
321 421 return array(
322 422 'token' => $token,
@@ -379,9 +479,26 @@
379 479 */
380 480 $token = JWT::encode( apply_filters( 'woocommerce_pos_jwt_refresh_token_before_sign', $token, $user ), $this->get_refresh_secret_key(), 'HS256' );
381 481
382 482 // Store refresh token JTI for potential revocation.
383 - $this->store_refresh_token_jti( $user->ID, $jti, $expire );
483 + $evicted = $this->sessions->record( $user->ID, $jti, $expire, Session_Context::from_request() );
484 + $issued_at = time();
485 + foreach ( $evicted as $evicted_jti => $token_data ) {
486 + /*
487 + * Blacklist ONLY a session that can still hold a live access token. An eviction
488 + * is not a revoke: clearing a bloated row can drop thousands of long-dead
489 + * sessions at once, and a transient for each would guard nothing — an expired
490 + * access token is already rejected on its own `exp` claim, and the refresh token
491 + * dies with the meta entry (`is_live()` requires the entry). This
492 + * also bounds each transient this path writes to one access-token lifetime,
493 + * rather than the refresh-token expiry `get_access_token_blacklist_ttl()` falls
494 + * back to for a session with no recorded access-token expiry.
495 + */
496 + $horizon = $this->access_token_horizon( $token_data );
497 + if ( $horizon > $issued_at ) {
498 + $this->blacklist_token( $evicted_jti, $horizon - $issued_at );
499 + }
500 + }
384 501
385 502 return $token;
386 503 }
387 504
@@ -467,14 +584,10 @@
467 584 'last_name' => $user->user_lastname,
468 585 'nice_name' => $user->user_nicename,
469 586 'display_name' => $user->display_name,
470 587 'roles' => array_values( $user->roles ),
471 - // Raw grants (role + user), the same vocabulary the POS Access settings
472 - // screen reads and writes. user_can() is wrong here: the singular meta
473 - // caps (edit_product, delete_product) cannot be checked without a post.
474 - 'capabilities' => array_values(
475 - array_filter( Access_Section::capability_names(), fn( $cap ) => ! empty( $user->allcaps[ $cap ] ) )
476 - ),
588 + // The helper reports effective grants, including role-editor denies.
589 + 'capabilities' => Access_Section::effective_capabilities( $user ),
477 590 'avatar_url' => get_avatar_url( $user->ID ),
478 591 // Token data.
479 592 'access_token' => $tokens['access_token'],
480 593 'refresh_token' => $tokens['refresh_token'],
@@ -521,10 +634,20 @@
521 634 if ( is_wp_error( $decoded ) ) {
522 635 return $decoded;
523 636 }
524 637
638 + /*
639 + * Before the first row read on this path. A refresh loads the whole session row —
640 + * `is_live()` below, then `refresh_activity()` — so it needs
641 + * the same protection a login has against a row too large to read (#1776).
642 + * Validating an ACCESS token READS the row through `is_live()` but never writes it,
643 + * and runs no guard because `guard_row()` can write; the read primes the user meta
644 + * cache that WordPress fills anyway to read the user's capabilities, so it adds no query.
645 + */
646 + $this->sessions->guard_row( absint( $decoded->data->user->id ) );
647 +
525 648 // Check if refresh token is still valid (not revoked).
526 - if ( ! $this->is_refresh_token_valid( $decoded->data->user->id, $decoded->jti ?? '' ) ) {
649 + if ( ! $this->sessions->is_live( $decoded->data->user->id, $decoded->jti ?? '' ) ) {
527 650 return new WP_Error(
528 651 'woocommerce_pos_auth_refresh_token_revoked',
529 652 'Refresh token has been revoked',
530 653 array( 'status' => 403 )
@@ -564,21 +687,9 @@
564 687 *
565 688 * @return bool
566 689 */
567 690 public function revoke_refresh_token( int $user_id, string $jti ): bool {
568 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
569 - if ( ! \is_array( $refresh_tokens ) ) {
570 - return false;
571 - }
572 -
573 - if ( isset( $refresh_tokens[ $jti ] ) ) {
574 - unset( $refresh_tokens[ $jti ] );
575 - update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
576 -
577 - return true;
578 - }
579 -
580 - return false;
691 + return $this->sessions->revoke( $user_id, $jti );
581 692 }
582 693
583 694 /**
584 695 * Revoke all refresh tokens for a user.
@@ -594,12 +705,13 @@
594 705 *
595 706 * @return bool
596 707 */
597 708 public function revoke_all_refresh_tokens( int $user_id ): bool {
598 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
709 + $refresh_tokens = $this->sessions->entries( $user_id );
599 710
600 - // Blacklist all sessions for instant access token invalidation.
601 - if ( \is_array( $refresh_tokens ) ) {
711 + // Blacklist all sessions for instant access token invalidation. The expiry
712 + // policy is only consulted when there is something to blacklist.
713 + if ( array() !== $refresh_tokens ) {
602 714 $issued_at = time();
603 715 $access_expire = $this->get_access_token_expire( $issued_at );
604 716
605 717 foreach ( $refresh_tokens as $jti => $token_data ) {
@@ -607,9 +719,9 @@
607 719 $this->blacklist_token( $jti, $ttl );
608 720 }
609 721 }
610 722
611 - return delete_user_meta( $user_id, '_woocommerce_pos_refresh_tokens' );
723 + return $this->sessions->revoke_all( $user_id );
612 724 }
613 725
614 726 /**
615 727 * Get all active sessions for a user.
@@ -618,42 +730,9 @@
618 730 *
619 731 * @return array
620 732 */
621 733 public function get_user_sessions( int $user_id ): array {
622 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
623 - if ( ! \is_array( $refresh_tokens ) ) {
624 - return array();
625 - }
626 -
627 - $sessions = array();
628 - $current_time = time();
629 -
630 - foreach ( $refresh_tokens as $jti => $token_data ) {
631 - // Skip expired sessions.
632 - if ( $token_data['expires'] <= $current_time ) {
633 - continue;
634 - }
635 -
636 - $sessions[] = array(
637 - 'jti' => $jti,
638 - 'created' => $token_data['created'] ?? $current_time,
639 - 'last_active' => $token_data['last_active'] ?? $token_data['created'] ?? $current_time,
640 - 'expires' => $token_data['expires'],
641 - 'ip_address' => $token_data['ip_address'] ?? '',
642 - 'user_agent' => $token_data['user_agent'] ?? '',
643 - 'device_info' => $token_data['device_info'] ?? array(),
644 - );
645 - }
646 -
647 - // Sort by last_active descending (most recent first).
648 - usort(
649 - $sessions,
650 - function ( $a, $b ) {
651 - return $b['last_active'] - $a['last_active'];
652 - }
653 - );
654 -
655 - return $sessions;
734 + return $this->sessions->list( $user_id );
656 735 }
657 736
658 737 /**
659 738 * Revoke a specific session by JTI (alias for revoke_refresh_token for clarity).
@@ -683,10 +762,11 @@
683 762 *
684 763 * @return bool
685 764 */
686 765 public function revoke_all_sessions_except( int $user_id, string $current_jti ): bool {
687 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
688 - if ( ! \is_array( $refresh_tokens ) ) {
766 + $refresh_tokens = $this->sessions->entries( $user_id );
767 + if ( array() === $refresh_tokens ) {
768 + // No row (or nothing in it): nothing to blacklist, nothing to rewrite.
689 769 return false;
690 770 }
691 771
692 772 // Blacklist all sessions except current for instant access token invalidation.
@@ -699,18 +779,9 @@
699 779 $this->blacklist_token( $jti, $ttl );
700 780 }
701 781 }
702 782
703 - // Keep only the current session in user meta.
704 - $refresh_tokens = array_filter(
705 - $refresh_tokens,
706 - function ( $_token, $jti ) use ( $current_jti ) {
707 - return $jti === $current_jti;
708 - },
709 - ARRAY_FILTER_USE_BOTH
710 - );
711 -
712 - return update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
783 + return $this->sessions->keep_only( $user_id, $current_jti );
713 784 }
714 785
715 786 /**
716 787 * Update last_active timestamp for a session.
@@ -720,16 +791,9 @@
720 791 *
721 792 * @return bool
722 793 */
723 794 public function update_session_activity( int $user_id, string $jti ): bool {
724 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
725 - if ( ! \is_array( $refresh_tokens ) || ! isset( $refresh_tokens[ $jti ] ) ) {
726 - return false;
727 - }
728 -
729 - $refresh_tokens[ $jti ]['last_active'] = time();
730 -
731 - return update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
795 + return $this->sessions->refresh_activity( $user_id, $jti );
732 796 }
733 797
734 798 /**
735 799 * Check if the current user can manage sessions for the target user.
@@ -791,11 +855,10 @@
791 855 *
792 856 * @return bool
793 857 */
794 858 public function revoke_session_with_blacklist( int $user_id, string $refresh_jti ): bool {
795 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
796 - $session_data = \is_array( $refresh_tokens ) && isset( $refresh_tokens[ $refresh_jti ] ) ? $refresh_tokens[ $refresh_jti ] : array();
797 - $ttl = $this->get_access_token_blacklist_ttl( $session_data );
859 + $session_data = $this->sessions->entry( $user_id, $refresh_jti );
860 + $ttl = $this->get_access_token_blacklist_ttl( $session_data );
798 861
799 862 // Revoke the refresh token (session) from user meta.
800 863 $revoked = $this->revoke_session( $user_id, $refresh_jti );
801 864
@@ -808,81 +871,25 @@
808 871 return $revoked;
809 872 }
810 873
811 874 /**
812 - * Store refresh token JTI for tracking/revocation.
875 + * The last moment an access token minted against a session can still validate.
813 876 *
814 - * @param int $user_id The user ID.
815 - * @param string $jti The token JTI.
816 - * @param int $expires The expiration timestamp.
817 - * @param null|Session_Context $context Request state the session is recorded
818 - * against. Defaults to the current request.
877 + * @param array $token_data Stored session record.
878 + *
879 + * @return int Unix timestamp; 0 when the session carries no usable timestamp at all.
819 880 */
820 - private function store_refresh_token_jti( int $user_id, string $jti, int $expires, ?Session_Context $context = null ): void {
821 - $context = null === $context ? Session_Context::from_request() : $context;
822 -
823 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
824 - if ( ! \is_array( $refresh_tokens ) ) {
825 - $refresh_tokens = array();
881 + private function access_token_horizon( array $token_data ): int {
882 + if ( isset( $token_data['access_expires'] ) ) {
883 + return (int) $token_data['access_expires'];
826 884 }
827 885
828 - // Clean up expired tokens.
829 - $refresh_tokens = array_filter(
830 - $refresh_tokens,
831 - function ( $token ) {
832 - return $token['expires'] > time();
833 - }
834 - );
886 + // Rows written before `access_expires` was recorded. The newest access token such a
887 + // session can hold was minted no later than its last recorded activity, so one
888 + // access-token lifetime past that moment is the outside limit.
889 + $last_seen = (int) ( $token_data['last_active'] ?? $token_data['created'] ?? 0 );
835 890
836 - // Capture session metadata.
837 - $current_time = time();
838 - $ip_address = $context->get_ip();
839 - $user_agent = $context->get_user_agent();
840 - $device_info = $this->parse_user_agent( $user_agent );
841 -
842 - // Check for explicit platform declaration from native apps (passed as a param in the auth request).
843 - $platform = $context->get_platform();
844 - $version = $context->get_version();
845 - $build = $context->get_build();
846 -
847 - // Override app_type if platform was explicitly provided by the client.
848 - if ( \in_array( $platform, array( 'ios', 'android', 'electron', 'web' ), true ) ) {
849 - $device_info['app_type'] = 'web' === $platform ? 'web' : $platform . '_app';
850 -
851 - // Set appropriate device type based on platform.
852 - if ( 'ios' === $platform || 'android' === $platform ) {
853 - $device_info['device_type'] = 'tablet'; // Default to tablet for mobile apps.
854 - } elseif ( 'electron' === $platform ) {
855 - $device_info['device_type'] = 'desktop';
856 - }
857 -
858 - // Use version from param if provided.
859 - if ( ! empty( $version ) ) {
860 - $device_info['browser_version'] = $version;
861 - }
862 -
863 - // Store build number if provided.
864 - if ( ! empty( $build ) ) {
865 - $device_info['build'] = $build;
866 - }
867 -
868 - // Set browser to WooCommerce POS for native apps.
869 - if ( 'web' !== $platform ) {
870 - $device_info['browser'] = 'WooCommerce POS';
871 - }
872 - }
873 -
874 - // Add new token with metadata.
875 - $refresh_tokens[ $jti ] = array(
876 - 'expires' => $expires,
877 - 'created' => $current_time,
878 - 'last_active' => $current_time,
879 - 'ip_address' => $ip_address,
880 - 'user_agent' => $user_agent,
881 - 'device_info' => $device_info,
882 - );
883 -
884 - update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
891 + return $last_seen > 0 ? $this->get_access_token_expire( $last_seen ) : 0;
885 892 }
886 893
887 894 /**
888 895 * Filters the JWT access token expire time.
@@ -936,37 +943,8 @@
936 943 return null;
937 944 }
938 945
939 946 /**
940 - * Record the latest access token expiry linked to a refresh-token session.
941 - *
942 - * @param int $user_id The user ID.
943 - * @param string $refresh_jti Refresh token JTI.
944 - * @param int $access_expires Access token expiry timestamp.
945 - *
946 - * @return bool
947 - */
948 - private function store_access_token_expiry( int $user_id, string $refresh_jti, int $access_expires ): bool {
949 - if ( empty( $refresh_jti ) || $access_expires <= 0 ) {
950 - return false;
951 - }
952 -
953 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
954 - if ( ! \is_array( $refresh_tokens ) || ! isset( $refresh_tokens[ $refresh_jti ] ) ) {
955 - return false;
956 - }
957 -
958 - $current_access_expires = isset( $refresh_tokens[ $refresh_jti ]['access_expires'] ) ? (int) $refresh_tokens[ $refresh_jti ]['access_expires'] : 0;
959 - if ( $access_expires <= $current_access_expires ) {
960 - return true;
961 - }
962 -
963 - $refresh_tokens[ $refresh_jti ]['access_expires'] = $access_expires;
964 -
965 - return update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
966 - }
967 -
968 - /**
969 947 * Calculate blacklist TTL for a session.
970 948 *
971 949 * @param array $session_data Session metadata.
972 950 * @param null|int $issued_at Current timestamp.
@@ -988,139 +966,8 @@
988 966 $access_expire = max( $access_expire, (int) $session_data['expires'] );
989 967 }
990 968
991 969 return max( 0, $access_expire - $issued_at );
992 - }
993 -
994 - /**
995 - * Check if refresh token is still valid (not revoked).
996 - *
997 - * @param int $user_id The user ID.
998 - * @param string $jti The token JTI.
999 - *
1000 - * @return bool
1001 - */
1002 - private function is_refresh_token_valid( int $user_id, string $jti ): bool {
1003 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
1004 - if ( ! \is_array( $refresh_tokens ) ) {
1005 - return false;
1006 - }
1007 -
1008 - return isset( $refresh_tokens[ $jti ] ) && $refresh_tokens[ $jti ]['expires'] > time();
1009 - }
1010 -
1011 - /**
1012 - * Parse user agent string to extract device information.
1013 - *
1014 - * @param string $user_agent The user agent string.
1015 - *
1016 - * @return array
1017 - */
1018 - private function parse_user_agent( string $user_agent ): array {
1019 - $device_info = array(
1020 - 'device_type' => 'unknown',
1021 - 'browser' => 'unknown',
1022 - 'browser_version' => '',
1023 - 'os' => 'unknown',
1024 - 'app_type' => 'web', // web, ios_app, android_app, electron_app.
1025 - );
1026 -
1027 - if ( empty( $user_agent ) ) {
1028 - return $device_info;
1029 - }
1030 -
1031 - // Detect WooCommerce POS apps first (custom identifiers)
1032 - // Check for Electron app (including just "WooCommercePOS" in user agent with Electron).
1033 - if ( preg_match( '/Electron/i', $user_agent ) && preg_match( '/WooCommercePOS|WCPOS/i', $user_agent ) ) {
1034 - $device_info['app_type'] = 'electron_app';
1035 - $device_info['browser'] = 'WooCommerce POS';
1036 - $device_info['device_type'] = 'desktop';
1037 - // Try to extract WooCommercePOS version.
1038 - if ( preg_match( '/WooCommercePOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
1039 - $device_info['browser_version'] = $matches[1];
1040 - } elseif ( preg_match( '/WCPOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
1041 - $device_info['browser_version'] = $matches[1];
1042 - }
1043 - } elseif ( preg_match( '/WCPOS[-_]?iOS|WooCommercePOS[-_]?iOS/i', $user_agent ) ) {
1044 - $device_info['app_type'] = 'ios_app';
1045 - $device_info['browser'] = 'WooCommerce POS';
1046 - // Default to tablet unless explicitly detected as phone.
1047 - $device_info['device_type'] = preg_match( '/iphone|ipod/i', $user_agent ) ? 'mobile' : 'tablet';
1048 - if ( preg_match( '/WCPOS[-_]?iOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
1049 - $device_info['browser_version'] = $matches[1];
1050 - } elseif ( preg_match( '/WooCommercePOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
1051 - $device_info['browser_version'] = $matches[1];
1052 - }
1053 - } elseif ( preg_match( '/WCPOS[-_]?Android|WooCommercePOS[-_]?Android/i', $user_agent ) ) {
1054 - $device_info['app_type'] = 'android_app';
1055 - $device_info['browser'] = 'WooCommerce POS';
1056 - // Default to tablet unless explicitly detected as mobile.
1057 - $device_info['device_type'] = preg_match( '/mobile/i', $user_agent ) && ! preg_match( '/tablet/i', $user_agent ) ? 'mobile' : 'tablet';
1058 - if ( preg_match( '/WCPOS[-_]?Android[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
1059 - $device_info['browser_version'] = $matches[1];
1060 - } elseif ( preg_match( '/WooCommercePOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
1061 - $device_info['browser_version'] = $matches[1];
1062 - }
1063 - }
1064 -
1065 - // Detect standard device type (if not already set by app detection).
1066 - if ( 'web' === $device_info['app_type'] ) {
1067 - if ( preg_match( '/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/i', $user_agent ) ) {
1068 - $device_info['device_type'] = 'mobile';
1069 - } elseif ( preg_match( '/tablet|ipad|playbook|silk/i', $user_agent ) ) {
1070 - $device_info['device_type'] = 'tablet';
1071 - } else {
1072 - $device_info['device_type'] = 'desktop';
1073 - }
1074 - }
1075 -
1076 - // Detect browser (skip if we already detected a WCPOS app).
1077 - if ( 'WooCommerce POS' !== $device_info['browser'] ) {
1078 - if ( preg_match( '/MSIE|Trident/i', $user_agent ) ) {
1079 - $device_info['browser'] = 'Internet Explorer';
1080 - if ( preg_match( '/MSIE ([0-9.]+)/', $user_agent, $matches ) ) {
1081 - $device_info['browser_version'] = $matches[1];
1082 - }
1083 - } elseif ( preg_match( '/Edge\/([0-9.]+)/i', $user_agent, $matches ) ) {
1084 - $device_info['browser'] = 'Edge';
1085 - $device_info['browser_version'] = $matches[1];
1086 - } elseif ( preg_match( '/Edg\/([0-9.]+)/i', $user_agent, $matches ) ) {
1087 - $device_info['browser'] = 'Edge';
1088 - $device_info['browser_version'] = $matches[1];
1089 - } elseif ( preg_match( '/Firefox\/([0-9.]+)/i', $user_agent, $matches ) ) {
1090 - $device_info['browser'] = 'Firefox';
1091 - $device_info['browser_version'] = $matches[1];
1092 - } elseif ( preg_match( '/Chrome\/([0-9.]+)/i', $user_agent, $matches ) ) {
1093 - $device_info['browser'] = 'Chrome';
1094 - $device_info['browser_version'] = $matches[1];
1095 - } elseif ( preg_match( '/Safari\/([0-9.]+)/i', $user_agent, $matches ) ) {
1096 - // Safari should be checked after Chrome because Chrome also contains Safari.
1097 - if ( ! preg_match( '/Chrome/i', $user_agent ) ) {
1098 - $device_info['browser'] = 'Safari';
1099 - $device_info['browser_version'] = $matches[1];
1100 - }
1101 - } elseif ( preg_match( '/Opera\/([0-9.]+)/i', $user_agent, $matches ) ) {
1102 - $device_info['browser'] = 'Opera';
1103 - $device_info['browser_version'] = $matches[1];
1104 - }
1105 - }
1106 -
1107 - // Detect OS.
1108 - if ( preg_match( '/Windows NT ([0-9.]+)/i', $user_agent, $matches ) ) {
1109 - $device_info['os'] = 'Windows';
1110 - } elseif ( preg_match( '/Mac OS X ([0-9_]+)/i', $user_agent, $matches ) ) {
1111 - $device_info['os'] = 'macOS';
1112 - } elseif ( preg_match( '/Android ([0-9.]+)/i', $user_agent, $matches ) ) {
1113 - $device_info['os'] = 'Android';
1114 - } elseif ( preg_match( '/iPhone OS ([0-9_]+)/i', $user_agent, $matches ) ) {
1115 - $device_info['os'] = 'iOS';
1116 - } elseif ( preg_match( '/iPad.*OS ([0-9_]+)/i', $user_agent, $matches ) ) {
1117 - $device_info['os'] = 'iPadOS';
1118 - } elseif ( preg_match( '/Linux/i', $user_agent ) ) {
1119 - $device_info['os'] = 'Linux';
1120 - }
1121 -
1122 - return $device_info;
1123 970 }
1124 971
1125 972 /**
1126 973 * Check if a token JTI is blacklisted.