| 1 |
<?php |
| 2 |
/** |
| 3 |
* Session Context. |
| 4 |
* |
| 5 |
* @package WCPOS\WooCommercePOS |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace WCPOS\WooCommercePOS\Services; |
| 9 |
|
| 10 |
/** |
| 11 |
* Immutable snapshot of the request state a login session is recorded against. |
| 12 |
* |
| 13 |
* The Auth service used to read `$_SERVER` and `$_REQUEST` inline while storing |
| 14 |
* a refresh token, which made the device-metadata rules impossible to test |
| 15 |
* without mutating superglobals. This value object is the single place those |
| 16 |
* reads happen: `from_request()` captures them, everything downstream takes a |
| 17 |
* `Session_Context` and can be handed a hand-built one in tests. |
| 18 |
* |
| 19 |
* The reads in `from_request()` are a faithful port of the previous inline |
| 20 |
* code, superglobal for superglobal, including the use of `$_REQUEST` rather |
| 21 |
* than `$_GET` — the native apps post their device metadata as part of the auth |
| 22 |
* flow, so narrowing the source would change behaviour. |
| 23 |
*/ |
| 24 |
class Session_Context { |
| 25 |
/** |
| 26 |
* The raw (sanitized) user agent string. |
| 27 |
* |
| 28 |
* @var string |
| 29 |
*/ |
| 30 |
private $user_agent; |
| 31 |
|
| 32 |
/** |
| 33 |
* Explicit platform declaration from a native app: ios, android, electron or web. |
| 34 |
* |
| 35 |
* @var string |
| 36 |
*/ |
| 37 |
private $platform; |
| 38 |
|
| 39 |
/** |
| 40 |
* Explicit app version declaration from a native app. |
| 41 |
* |
| 42 |
* @var string |
| 43 |
*/ |
| 44 |
private $version; |
| 45 |
|
| 46 |
/** |
| 47 |
* Explicit build number declaration from a native app. |
| 48 |
* |
| 49 |
* @var string |
| 50 |
*/ |
| 51 |
private $build; |
| 52 |
|
| 53 |
/** |
| 54 |
* The client IP address, or an empty string when it could not be determined. |
| 55 |
* |
| 56 |
* @var string |
| 57 |
*/ |
| 58 |
private $ip; |
| 59 |
|
| 60 |
/** |
| 61 |
* Constructor. |
| 62 |
* |
| 63 |
* @param string $user_agent The user agent string. |
| 64 |
* @param string $platform Explicit platform declaration. |
| 65 |
* @param string $version Explicit app version declaration. |
| 66 |
* @param string $build Explicit build number declaration. |
| 67 |
* @param string $ip The client IP address. |
| 68 |
*/ |
| 69 |
public function __construct( |
| 70 |
string $user_agent = '', |
| 71 |
string $platform = '', |
| 72 |
string $version = '', |
| 73 |
string $build = '', |
| 74 |
string $ip = '' |
| 75 |
) { |
| 76 |
$this->user_agent = $user_agent; |
| 77 |
$this->platform = $platform; |
| 78 |
$this->version = $version; |
| 79 |
$this->build = $build; |
| 80 |
$this->ip = $ip; |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Capture the session context from the current request. |
| 85 |
* |
| 86 |
* Both arrays default to the corresponding superglobal. They are only |
| 87 |
* parameters so tests can supply request state without mutating globals; |
| 88 |
* production callers pass nothing. |
| 89 |
* |
| 90 |
* @param null|array $server Server/header state, defaults to $_SERVER. |
| 91 |
* @param null|array $request Request parameters, defaults to $_REQUEST. |
| 92 |
* |
| 93 |
* @return self |
| 94 |
*/ |
| 95 |
public static function from_request( ?array $server = null, ?array $request = null ): self { |
| 96 |
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only capture of client-declared device metadata; the request itself is authenticated by the auth flow. |
| 97 |
$server = null === $server ? $_SERVER : $server; |
| 98 |
$request = null === $request ? $_REQUEST : $request; |
| 99 |
// phpcs:enable WordPress.Security.NonceVerification.Recommended |
| 100 |
|
| 101 |
$user_agent = isset( $server['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $server['HTTP_USER_AGENT'] ) ) : ''; |
| 102 |
|
| 103 |
// Explicit platform declaration from native apps (passed as a param in the auth request). |
| 104 |
$platform = isset( $request['platform'] ) ? sanitize_text_field( wp_unslash( $request['platform'] ) ) : ''; |
| 105 |
$version = isset( $request['version'] ) ? sanitize_text_field( wp_unslash( $request['version'] ) ) : ''; |
| 106 |
$build = isset( $request['build'] ) ? sanitize_text_field( wp_unslash( $request['build'] ) ) : ''; |
| 107 |
|
| 108 |
return new self( $user_agent, $platform, $version, $build, self::client_ip_from_request( $server ) ); |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Read the client IP address from the current request. |
| 113 |
* |
| 114 |
* Proxy headers are checked in a fixed precedence: Cloudflare, then |
| 115 |
* X-Forwarded-For, then X-Real-IP, then the raw remote address. The first |
| 116 |
* header that is present wins, and a comma-separated list is reduced to its |
| 117 |
* first entry. Anything that does not validate as an IP becomes ''. |
| 118 |
* |
| 119 |
* @param null|array $server Server/header state, defaults to $_SERVER. |
| 120 |
* |
| 121 |
* @return string |
| 122 |
*/ |
| 123 |
public static function client_ip_from_request( ?array $server = null ): string { |
| 124 |
$server = null === $server ? $_SERVER : $server; |
| 125 |
$ip_address = ''; |
| 126 |
|
| 127 |
// Check for various proxy headers. |
| 128 |
$headers = array( |
| 129 |
'HTTP_CF_CONNECTING_IP', // Cloudflare. |
| 130 |
'HTTP_X_FORWARDED_FOR', |
| 131 |
'HTTP_X_REAL_IP', |
| 132 |
'REMOTE_ADDR', |
| 133 |
); |
| 134 |
|
| 135 |
foreach ( $headers as $header ) { |
| 136 |
if ( ! empty( $server[ $header ] ) ) { |
| 137 |
$ip_address = sanitize_text_field( wp_unslash( $server[ $header ] ) ); |
| 138 |
// Handle comma-separated IPs (X-Forwarded-For can contain multiple IPs). |
| 139 |
if ( false !== strpos( $ip_address, ',' ) ) { |
| 140 |
$ip_parts = explode( ',', $ip_address ); |
| 141 |
$ip_address = trim( $ip_parts[0] ); |
| 142 |
} |
| 143 |
|
| 144 |
break; |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
// Validate and sanitize IP. |
| 149 |
if ( filter_var( $ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 ) ) { |
| 150 |
return $ip_address; |
| 151 |
} |
| 152 |
|
| 153 |
return ''; |
| 154 |
} |
| 155 |
|
| 156 |
/** |
| 157 |
* Get the user agent string. |
| 158 |
* |
| 159 |
* @return string |
| 160 |
*/ |
| 161 |
public function get_user_agent(): string { |
| 162 |
return $this->user_agent; |
| 163 |
} |
| 164 |
|
| 165 |
/** |
| 166 |
* Get the explicit platform declaration. |
| 167 |
* |
| 168 |
* @return string |
| 169 |
*/ |
| 170 |
public function get_platform(): string { |
| 171 |
return $this->platform; |
| 172 |
} |
| 173 |
|
| 174 |
/** |
| 175 |
* Get the explicit app version declaration. |
| 176 |
* |
| 177 |
* @return string |
| 178 |
*/ |
| 179 |
public function get_version(): string { |
| 180 |
return $this->version; |
| 181 |
} |
| 182 |
|
| 183 |
/** |
| 184 |
* Get the explicit build number declaration. |
| 185 |
* |
| 186 |
* @return string |
| 187 |
*/ |
| 188 |
public function get_build(): string { |
| 189 |
return $this->build; |
| 190 |
} |
| 191 |
|
| 192 |
/** |
| 193 |
* Get the client IP address. |
| 194 |
* |
| 195 |
* @return string |
| 196 |
*/ |
| 197 |
public function get_ip(): string { |
| 198 |
return $this->ip; |
| 199 |
} |
| 200 |
} |
| 201 |
|