PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.19
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.19
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 +109 -311 1.10.21.10.19 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,12 +249,27 @@
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;
266 + } catch ( \WCPOS\Vendor\Firebase\JWT\ExpiredException $e ) {
267 + return new WP_Error(
268 + 'woocommerce_pos_auth_token_expired',
269 + 'Token expired',
270 + array( 'status' => 403 )
271 + );
219 272 } catch ( Exception $e ) {
220 273 // Something is wrong trying to decode the token, send back the error.
221 274 return new WP_Error(
222 275 'woocommmerce_pos_auth_invalid_token',
@@ -314,9 +367,9 @@
314 367 $access_jti = null === $access_jti ? $jti : (string) $access_jti;
315 368
316 369 if ( null !== $linked_refresh_jti ) {
317 370 $linked_refresh_jti = (string) $linked_refresh_jti;
318 - $this->store_access_token_expiry( $user->ID, $linked_refresh_jti, $expires_at );
371 + $this->sessions->record_access_expiry( $user->ID, $linked_refresh_jti, $expires_at );
319 372 }
320 373
321 374 return array(
322 375 'token' => $token,
@@ -379,9 +432,26 @@
379 432 */
380 433 $token = JWT::encode( apply_filters( 'woocommerce_pos_jwt_refresh_token_before_sign', $token, $user ), $this->get_refresh_secret_key(), 'HS256' );
381 434
382 435 // Store refresh token JTI for potential revocation.
383 - $this->store_refresh_token_jti( $user->ID, $jti, $expire );
436 + $evicted = $this->sessions->record( $user->ID, $jti, $expire, Session_Context::from_request() );
437 + $issued_at = time();
438 + foreach ( $evicted as $evicted_jti => $token_data ) {
439 + /*
440 + * Blacklist ONLY a session that can still hold a live access token. An eviction
441 + * is not a revoke: clearing a bloated row can drop thousands of long-dead
442 + * sessions at once, and a transient for each would guard nothing — an expired
443 + * access token is already rejected on its own `exp` claim, and the refresh token
444 + * dies with the meta entry (`is_live()` requires the entry). This
445 + * also bounds each transient this path writes to one access-token lifetime,
446 + * rather than the refresh-token expiry `get_access_token_blacklist_ttl()` falls
447 + * back to for a session with no recorded access-token expiry.
448 + */
449 + $horizon = $this->access_token_horizon( $token_data );
450 + if ( $horizon > $issued_at ) {
451 + $this->blacklist_token( $evicted_jti, $horizon - $issued_at );
452 + }
453 + }
384 454
385 455 return $token;
386 456 }
387 457
@@ -467,14 +537,10 @@
467 537 'last_name' => $user->user_lastname,
468 538 'nice_name' => $user->user_nicename,
469 539 'display_name' => $user->display_name,
470 540 '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 - ),
541 + // The helper reports effective grants, including role-editor denies.
542 + 'capabilities' => Access_Section::effective_capabilities( $user ),
477 543 'avatar_url' => get_avatar_url( $user->ID ),
478 544 // Token data.
479 545 'access_token' => $tokens['access_token'],
480 546 'refresh_token' => $tokens['refresh_token'],
@@ -521,10 +587,18 @@
521 587 if ( is_wp_error( $decoded ) ) {
522 588 return $decoded;
523 589 }
524 590
591 + /*
592 + * Before the first row read on this path. A refresh loads the whole session row —
593 + * `is_live()` below, then `refresh_activity()` — so it needs
594 + * the same protection a login has against a row too large to read (#1776).
595 + * Validating an ACCESS token needs no such guard: it no longer touches the row.
596 + */
597 + $this->sessions->guard_row( absint( $decoded->data->user->id ) );
598 +
525 599 // Check if refresh token is still valid (not revoked).
526 - if ( ! $this->is_refresh_token_valid( $decoded->data->user->id, $decoded->jti ?? '' ) ) {
600 + if ( ! $this->sessions->is_live( $decoded->data->user->id, $decoded->jti ?? '' ) ) {
527 601 return new WP_Error(
528 602 'woocommerce_pos_auth_refresh_token_revoked',
529 603 'Refresh token has been revoked',
530 604 array( 'status' => 403 )
@@ -564,21 +638,9 @@
564 638 *
565 639 * @return bool
566 640 */
567 641 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;
642 + return $this->sessions->revoke( $user_id, $jti );
581 643 }
582 644
583 645 /**
584 646 * Revoke all refresh tokens for a user.
@@ -594,12 +656,13 @@
594 656 *
595 657 * @return bool
596 658 */
597 659 public function revoke_all_refresh_tokens( int $user_id ): bool {
598 - $refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
660 + $refresh_tokens = $this->sessions->entries( $user_id );
599 661
600 - // Blacklist all sessions for instant access token invalidation.
601 - if ( \is_array( $refresh_tokens ) ) {
662 + // Blacklist all sessions for instant access token invalidation. The expiry
663 + // policy is only consulted when there is something to blacklist.
664 + if ( array() !== $refresh_tokens ) {
602 665 $issued_at = time();
603 666 $access_expire = $this->get_access_token_expire( $issued_at );
604 667
605 668 foreach ( $refresh_tokens as $jti => $token_data ) {
@@ -607,9 +670,9 @@
607 670 $this->blacklist_token( $jti, $ttl );
608 671 }
609 672 }
610 673
611 - return delete_user_meta( $user_id, '_woocommerce_pos_refresh_tokens' );
674 + return $this->sessions->revoke_all( $user_id );
612 675 }
613 676
614 677 /**
615 678 * Get all active sessions for a user.
@@ -618,42 +681,9 @@
618 681 *
619 682 * @return array
620 683 */
621 684 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;
685 + return $this->sessions->list( $user_id );
656 686 }
657 687
658 688 /**
659 689 * Revoke a specific session by JTI (alias for revoke_refresh_token for clarity).
@@ -683,10 +713,11 @@
683 713 *
684 714 * @return bool
685 715 */
686 716 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 ) ) {
717 + $refresh_tokens = $this->sessions->entries( $user_id );
718 + if ( array() === $refresh_tokens ) {
719 + // No row (or nothing in it): nothing to blacklist, nothing to rewrite.
689 720 return false;
690 721 }
691 722
692 723 // Blacklist all sessions except current for instant access token invalidation.
@@ -699,18 +730,9 @@
699 730 $this->blacklist_token( $jti, $ttl );
700 731 }
701 732 }
702 733
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 );
734 + return $this->sessions->keep_only( $user_id, $current_jti );
713 735 }
714 736
715 737 /**
716 738 * Update last_active timestamp for a session.
@@ -720,16 +742,9 @@
720 742 *
721 743 * @return bool
722 744 */
723 745 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 );
746 + return $this->sessions->refresh_activity( $user_id, $jti );
732 747 }
733 748
734 749 /**
735 750 * Check if the current user can manage sessions for the target user.
@@ -791,11 +806,10 @@
791 806 *
792 807 * @return bool
793 808 */
794 809 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 );
810 + $session_data = $this->sessions->entry( $user_id, $refresh_jti );
811 + $ttl = $this->get_access_token_blacklist_ttl( $session_data );
798 812
799 813 // Revoke the refresh token (session) from user meta.
800 814 $revoked = $this->revoke_session( $user_id, $refresh_jti );
801 815
@@ -808,81 +822,25 @@
808 822 return $revoked;
809 823 }
810 824
811 825 /**
812 - * Store refresh token JTI for tracking/revocation.
826 + * The last moment an access token minted against a session can still validate.
813 827 *
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.
828 + * @param array $token_data Stored session record.
829 + *
830 + * @return int Unix timestamp; 0 when the session carries no usable timestamp at all.
819 831 */
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();
832 + private function access_token_horizon( array $token_data ): int {
833 + if ( isset( $token_data['access_expires'] ) ) {
834 + return (int) $token_data['access_expires'];
826 835 }
827 836
828 - // Clean up expired tokens.
829 - $refresh_tokens = array_filter(
830 - $refresh_tokens,
831 - function ( $token ) {
832 - return $token['expires'] > time();
833 - }
834 - );
837 + // Rows written before `access_expires` was recorded. The newest access token such a
838 + // session can hold was minted no later than its last recorded activity, so one
839 + // access-token lifetime past that moment is the outside limit.
840 + $last_seen = (int) ( $token_data['last_active'] ?? $token_data['created'] ?? 0 );
835 841
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 );
842 + return $last_seen > 0 ? $this->get_access_token_expire( $last_seen ) : 0;
885 843 }
886 844
887 845 /**
888 846 * Filters the JWT access token expire time.
@@ -936,37 +894,8 @@
936 894 return null;
937 895 }
938 896
939 897 /**
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 898 * Calculate blacklist TTL for a session.
970 899 *
971 900 * @param array $session_data Session metadata.
972 901 * @param null|int $issued_at Current timestamp.
@@ -988,139 +917,8 @@
988 917 $access_expire = max( $access_expire, (int) $session_data['expires'] );
989 918 }
990 919
991 920 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 921 }
1124 922
1125 923 /**
1126 924 * Check if a token JTI is blacklisted.