Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.67% covered (warning)
86.67%
65 / 75
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Device_Detection
86.30% covered (warning)
86.30%
63 / 73
71.43% covered (warning)
71.43%
5 / 7
34.63
0.00% covered (danger)
0.00%
0 / 1
 get_info
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
6.03
 is_phone
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_smartphone
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_tablet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_desktop
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_handheld
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_mobile
80.95% covered (warning)
80.95%
34 / 42
0.00% covered (danger)
0.00%
0 / 1
24.05
1<?php
2/**
3 * Device detection for Jetpack.
4 *
5 * @package automattic/jetpack-device-detection
6 */
7
8namespace Automattic\Jetpack;
9
10require_once __DIR__ . '/functions.php';
11require_once __DIR__ . '/class-user-agent-info.php';
12
13use Automattic\Jetpack\Device_Detection\User_Agent_Info;
14use function Automattic\Jetpack\Device_Detection\wp_unslash;
15
16/**
17 * Class Device_Detection
18 *
19 * Determine if the current User Agent matches the passed $kind.
20 *
21 * Note: str_contains() and other PHP8+ functions that have a polyfill in core are not used here,
22 * as wp-includes/compat.php may not be loaded yet.
23 */
24class Device_Detection {
25
26    /**
27     * Memoization cache for get_info() results.
28     *
29     * @var array
30     */
31    private static $get_info_memo = array();
32
33    /**
34     * Maximum size of the memoization cache.
35     *
36     * @var int
37     */
38    private static $max_memo_size = 100;
39
40    /**
41     * Returns information about the current device accessing the page.
42     *
43     * @param string $ua (Optional) User-Agent string.
44     *
45     * @return array Device information.
46     *
47     * array(
48     *  'is_phone'            => (bool) Whether the current device is a mobile phone.
49     *  'is_smartphone'       => (bool) Whether the current device is a smartphone.
50     *  'is_tablet'           => (bool) Whether the current device is a tablet device.
51     *  'is_handheld'         => (bool) Whether the current device is a handheld device.
52     *  'is_desktop'          => (bool) Whether the current device is a laptop / desktop device.
53     *  'platform'            => (string) Detected platform.
54     *  'is_phone_matched_ua' => (string) Matched UA.
55     * );
56     */
57    public static function get_info( $ua = '' ) {
58        // Return memoized result if available.
59        // phpcs:disable WordPress.Security.ValidatedSanitizedInput
60        $memo_key = ! empty( $ua ) ? $ua : ( $_SERVER['HTTP_USER_AGENT'] ?? '' );
61        // Note: UA string used raw for compatibility reasons.
62        // No sanitization is needed as the value is never output or persisted, and is only used for memoization.
63        // phpcs:enable WordPress.Security.ValidatedSanitizedInput
64        if ( isset( self::$get_info_memo[ $memo_key ] ) ) {
65            return self::$get_info_memo[ $memo_key ];
66        }
67
68        $ua_info = new User_Agent_Info( $ua );
69
70        $info = array(
71            'is_phone'            => self::is_mobile( 'any', false, $ua_info ),
72            'is_phone_matched_ua' => self::is_mobile( 'any', true, $ua_info ),
73            'is_smartphone'       => self::is_mobile( 'smart', false, $ua_info ),
74            'is_tablet'           => $ua_info->is_tablet(),
75            'platform'            => $ua_info->get_platform(),
76            'desktop_platform'    => $ua_info->get_desktop_platform(),
77            'browser'             => $ua_info->get_browser(),
78        );
79
80        $info['is_handheld'] = $info['is_phone'] || $info['is_tablet'];
81        $info['is_desktop']  = ! $info['is_handheld'];
82
83        if ( function_exists( 'apply_filters' ) ) {
84            /**
85             * Filter the value of Device_Detection::get_info.
86             *
87             * @since 1.0.0
88             *
89             * @param array           $info    Array of device information.
90             * @param string          $ua      User agent string passed to Device_Detection::get_info.
91             * @param User_Agent_Info $ua_info Instance of Automattic\Jetpack\Device_Detection\User_Agent_Info.
92             */
93            $info = apply_filters( 'jetpack_device_detection_get_info', $info, $ua, $ua_info );
94        }
95
96        // Memoize the result.
97        self::$get_info_memo[ $memo_key ] = $info;
98        if ( count( self::$get_info_memo ) > self::$max_memo_size ) {
99            array_shift( self::$get_info_memo );
100        }
101
102        return $info;
103    }
104
105    /**
106     * Detects phone devices.
107     *
108     * @param string $ua User-Agent string.
109     *
110     * @return bool
111     */
112    public static function is_phone( $ua = '' ) {
113        $device_info = self::get_info( $ua );
114        return true === $device_info['is_phone'];
115    }
116
117    /**
118     * Detects smartphone devices.
119     *
120     * @param string $ua User-Agent string.
121     *
122     * @return bool
123     */
124    public static function is_smartphone( $ua = '' ) {
125        $device_info = self::get_info( $ua );
126        return true === $device_info['is_smartphone'];
127    }
128
129    /**
130     * Detects tablet devices.
131     *
132     * @param string $ua User-Agent string.
133     *
134     * @return bool
135     */
136    public static function is_tablet( $ua = '' ) {
137        $device_info = self::get_info( $ua );
138        return true === $device_info['is_tablet'];
139    }
140
141    /**
142     * Detects desktop devices.
143     *
144     * @param string $ua User-Agent string.
145     *
146     * @return bool
147     */
148    public static function is_desktop( $ua = '' ) {
149        $device_info = self::get_info( $ua );
150        return true === $device_info['is_desktop'];
151    }
152
153    /**
154     * Detects handheld (i.e. phone + tablet) devices.
155     *
156     * @param string $ua User-Agent string.
157     *
158     * @return bool
159     */
160    public static function is_handheld( $ua = '' ) {
161        $device_info = self::get_info( $ua );
162        return true === $device_info['is_handheld'];
163    }
164
165    /**
166     * Determine if the current User Agent matches the passed $kind.
167     *
168     * @param string          $kind                 Category of mobile device to check for. Either: any, dumb, smart.
169     * @param bool            $return_matched_agent Boolean indicating if the UA should be returned.
170     * @param User_Agent_Info $ua_info              Boolean indicating if the UA should be returned.
171     *
172     * @return bool|string Boolean indicating if current UA matches $kind. If `$return_matched_agent` is true, returns the UA string.
173     */
174    private static function is_mobile( $kind, $return_matched_agent, $ua_info ) {
175        $kinds         = array(
176            'smart' => false,
177            'dumb'  => false,
178            'any'   => false,
179        );
180        $matched_agent = '';
181
182        // If an invalid kind is passed in, reset it to default.
183        if ( ! isset( $kinds[ $kind ] ) ) {
184                $kind = 'any';
185        }
186
187        if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
188            return false;
189        }
190
191        $agent = strtolower( filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) );
192        if ( strpos( $agent, 'ipad' ) ) {
193            return false;
194        }
195
196        // Remove Samsung Galaxy tablets (SCH-I800) from being mobile devices.
197        if ( strpos( $agent, 'sch-i800' ) ) {
198            return false;
199        }
200
201        if ( $ua_info->is_android_tablet() && false === $ua_info->is_kindle_touch() ) {
202            return false;
203        }
204
205        if ( $ua_info->is_blackberry_tablet() ) {
206            return false;
207        }
208
209        // checks for iPhoneTier devices & RichCSS devices.
210        if ( $ua_info->isTierIphone() || $ua_info->isTierRichCSS() ) {
211            $kinds['smart'] = true;
212            $matched_agent  = $ua_info->matched_agent;
213        }
214
215        if ( ! $kinds['smart'] ) {
216            // if smart, we are not dumb so no need to check.
217            $dumb_agents = $ua_info->dumb_agents;
218
219            foreach ( $dumb_agents as $dumb_agent ) {
220                if ( false !== strpos( $agent, $dumb_agent ) ) {
221                    $kinds['dumb'] = true;
222                    $matched_agent = $dumb_agent;
223
224                    break;
225                }
226            }
227
228            if ( ! $kinds['dumb'] ) {
229                if ( isset( $_SERVER['HTTP_X_WAP_PROFILE'] ) ) {
230                    $kinds['dumb'] = true;
231                    $matched_agent = 'http_x_wap_profile';
232                } elseif ( isset( $_SERVER['HTTP_ACCEPT'] ) && ( preg_match( '/wap\.|\.wap/i', $_SERVER['HTTP_ACCEPT'] ) || false !== strpos( strtolower( $_SERVER['HTTP_ACCEPT'] ), 'application/vnd.wap.xhtml+xml' ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is doing the validating.
233                    $kinds['dumb'] = true;
234                    $matched_agent = 'vnd.wap.xhtml+xml';
235                }
236            }
237        }
238
239        if ( $kinds['dumb'] || $kinds['smart'] ) {
240            $kinds['any'] = true;
241        }
242
243        $value = $kinds[ $kind ];
244
245        if ( $return_matched_agent ) {
246            $value = $matched_agent;
247        }
248        return $value;
249    }
250}