PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
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 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
← All changes | includes/Services/Auth.php +103 -311 1.10.31.10.18 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 */
@@ -211,8 +249,17 @@
211 249 'Session has been revoked',
212 250 array( 'status' => 403 )
213 251 );
214 252 }
253 +
254 + // The session is live: record that, so eviction can tell a device that is
255 + // working right now from one that has not been seen in a week.
256 + if ( isset( $decoded_token->refresh_jti ) ) {
257 + $this->sessions->touch(
258 + absint( $decoded_token->data->user->id ),
259 + (string) $decoded_token->refresh_jti
260 + );
261 + }
215 262 }
216 263
217 264 // Everything looks good return the decoded token.
218 265 return $decoded_token;
@@ -314,9 +361,9 @@
314 361 $access_jti = null === $access_jti ? $jti : (string) $access_jti;
315 362
316 363 if ( null !== $linked_refresh_jti ) {
317 364 $linked_refresh_jti = (string) $linked_refresh_jti;
318 - $this->store_access_token_expiry( $user->ID, $linked_refresh_jti, $expires_at );
365 + $this->sessions->record_access_expiry( $user->ID, $linked_refresh_jti, $expires_at );
319 366 }
320 367
321 368 return array(
322 369 'token' => $token,
@@ -379,9 +426,26 @@
379 426 */
380 427 $token = JWT::encode( apply_filters( 'woocommerce_pos_jwt_refresh_token_before_sign', $token, $user ), $this->get_refresh_secret_key(), 'HS256' );
381 428
382 429 // Store refresh token JTI for potential revocation.
383 - $this->store_refresh_token_jti( $user->ID, $jti, $expire );
430 + $evicted = $this->sessions->record( $user->ID, $jti, $expire, Session_Context::from_request() );
431 + $issued_at = time();
432 + foreach ( $evicted as $evicted_jti => $token_data ) {
433 + /*
434 + * Blacklist ONLY a session that can still hold a live access token. An eviction
435 + * is not a revoke: clearing a bloated row can drop thousands of long-dead
436 + * sessions at once, and a transient for each would guard nothing — an expired
437 + * access token is already rejected on its own `exp` claim, and the refresh token
438 + * dies with the meta entry (`is_live()` requires the entry). This
439 + * also bounds each transient this path writes to one access-token lifetime,
440 + * rather than the refresh-token expiry `get_access_token_blacklist_ttl()` falls
441 + * back to for a session with no recorded access-token expiry.
442 + */
443 + $horizon = $this->access_token_horizon( $token_data );
444 + if ( $horizon > $issued_at ) {
445 + $this->blacklist_token( $evicted_jti, $horizon - $issued_at );
446 + }
447 + }
384 448
385 449 return $token;
386 450 }
387 451
@@ -467,14 +531,10 @@
467 531 'last_name' => $user->user_lastname,
468 532 'nice_name' => $user->user_nicename,
469 533 'display_name' => $user->display_name,
470 534 '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 - ),
535 + // The helper reports effective grants, including role-editor denies.
536 + 'capabilities' => Access_Section::effective_capabilities( $user ),
477 537 'avatar_url' => get_avatar_url( $user->ID ),
478 538 // Token data.
479 539 'access_token' => $tokens['access_token'],
480 540 'refresh_token' => $tokens['refresh_token'],
@@ -521,10 +581,18 @@
521 581 if ( is_wp_error( $decoded ) ) {
522 582 return $decoded;
523 583 }
524 584
585 + /*
586 + * Before the first row read on this path. A refresh loads the whole session row —
587 + * `is_live()` below, then `refresh_activity()` — so it needs
588 + * the same protection a login has against a row too large to read (#1776).
589 + * Validating an ACCESS token needs no such guard: it no longer touches the row.
590 + */
591 + $this->sessions->guard_row( absint( $decoded->data->user->id ) );
592 +
525 593 // Check if refresh token is still valid (not revoked).
526 - if ( ! $this->is_refresh_token_valid( $decoded->data->user->id, $decoded->jti ?? '' ) ) {
594 + if ( ! $this->sessions->is_live( $decoded->data->user->id, $decoded->jti ?? '' ) ) {
527 595 return new WP_Error(
528 596 'woocommerce_pos_auth_refresh_token_revoked',
529 597 'Refresh token has been revoked',
530 598 array( 'status' => 403 )
@@ -564,21 +632,9 @@
564 632 *
565 633 * @return bool
566 634 */
567 635 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;
636 + return $this->sessions->revoke( $user_id, $jti );
581 637 }
582 638
583 639 /**
584 640 * Revoke all refresh tokens for a user.
@@ -594,12 +650,13 @@
594 650 *
595 651 * @return bool
596 652 */
597 653 public function revoke_all_refresh_tokens( int $user_id ): bool {
598 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
654 + $refresh_tokens = $this->sessions->entries( $user_id );
599 655
600 - // Blacklist all sessions for instant access token invalidation.
601 - if ( \is_array( $refresh_tokens ) ) {
656 + // Blacklist all sessions for instant access token invalidation. The expiry
657 + // policy is only consulted when there is something to blacklist.
658 + if ( array() !== $refresh_tokens ) {
602 659 $issued_at = time();
603 660 $access_expire = $this->get_access_token_expire( $issued_at );
604 661
605 662 foreach ( $refresh_tokens as $jti => $token_data ) {
@@ -607,9 +664,9 @@
607 664 $this->blacklist_token( $jti, $ttl );
608 665 }
609 666 }
610 667
611 - return delete_user_meta( $user_id, '_woocommerce_pos_refresh_tokens' );
668 + return $this->sessions->revoke_all( $user_id );
612 669 }
613 670
614 671 /**
615 672 * Get all active sessions for a user.
@@ -618,42 +675,9 @@
618 675 *
619 676 * @return array
620 677 */
621 678 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;
679 + return $this->sessions->list( $user_id );
656 680 }
657 681
658 682 /**
659 683 * Revoke a specific session by JTI (alias for revoke_refresh_token for clarity).
@@ -683,10 +707,11 @@
683 707 *
684 708 * @return bool
685 709 */
686 710 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 ) ) {
711 + $refresh_tokens = $this->sessions->entries( $user_id );
712 + if ( array() === $refresh_tokens ) {
713 + // No row (or nothing in it): nothing to blacklist, nothing to rewrite.
689 714 return false;
690 715 }
691 716
692 717 // Blacklist all sessions except current for instant access token invalidation.
@@ -699,18 +724,9 @@
699 724 $this->blacklist_token( $jti, $ttl );
700 725 }
701 726 }
702 727
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 );
728 + return $this->sessions->keep_only( $user_id, $current_jti );
713 729 }
714 730
715 731 /**
716 732 * Update last_active timestamp for a session.
@@ -720,16 +736,9 @@
720 736 *
721 737 * @return bool
722 738 */
723 739 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 );
740 + return $this->sessions->refresh_activity( $user_id, $jti );
732 741 }
733 742
734 743 /**
735 744 * Check if the current user can manage sessions for the target user.
@@ -791,11 +800,10 @@
791 800 *
792 801 * @return bool
793 802 */
794 803 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 );
804 + $session_data = $this->sessions->entry( $user_id, $refresh_jti );
805 + $ttl = $this->get_access_token_blacklist_ttl( $session_data );
798 806
799 807 // Revoke the refresh token (session) from user meta.
800 808 $revoked = $this->revoke_session( $user_id, $refresh_jti );
801 809
@@ -808,81 +816,25 @@
808 816 return $revoked;
809 817 }
810 818
811 819 /**
812 - * Store refresh token JTI for tracking/revocation.
820 + * The last moment an access token minted against a session can still validate.
813 821 *
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.
822 + * @param array $token_data Stored session record.
823 + *
824 + * @return int Unix timestamp; 0 when the session carries no usable timestamp at all.
819 825 */
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();
826 + private function access_token_horizon( array $token_data ): int {
827 + if ( isset( $token_data['access_expires'] ) ) {
828 + return (int) $token_data['access_expires'];
826 829 }
827 830
828 - // Clean up expired tokens.
829 - $refresh_tokens = array_filter(
830 - $refresh_tokens,
831 - function ( $token ) {
832 - return $token['expires'] > time();
833 - }
834 - );
831 + // Rows written before `access_expires` was recorded. The newest access token such a
832 + // session can hold was minted no later than its last recorded activity, so one
833 + // access-token lifetime past that moment is the outside limit.
834 + $last_seen = (int) ( $token_data['last_active'] ?? $token_data['created'] ?? 0 );
835 835
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 );
836 + return $last_seen > 0 ? $this->get_access_token_expire( $last_seen ) : 0;
885 837 }
886 838
887 839 /**
888 840 * Filters the JWT access token expire time.
@@ -936,37 +888,8 @@
936 888 return null;
937 889 }
938 890
939 891 /**
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 892 * Calculate blacklist TTL for a session.
970 893 *
971 894 * @param array $session_data Session metadata.
972 895 * @param null|int $issued_at Current timestamp.
@@ -988,139 +911,8 @@
988 911 $access_expire = max( $access_expire, (int) $session_data['expires'] );
989 912 }
990 913
991 914 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 915 }
1124 916
1125 917 /**
1126 918 * Check if a token JTI is blacklisted.