Completed
Push — feature/jetpack-packages ( 199f42...b13b58 )
by
unknown
17:26 queued 10:03
created

class.jetpack-data.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use Automattic\Jetpack\Constants\Manager as Constants_Manager;
4
5
class Jetpack_Data {
6
	/*
7
	 * Used internally when we want to look for the Normal Blog Token
8
	 * without knowing its token key ahead of time.
9
	 */
10
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
11
12
	/**
13
	 * Gets the requested token.
14
	 *
15
	 * Tokens are one of two types:
16
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
17
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
18
	 *    are not associated with a user account. They represent the site's connection with
19
	 *    the Jetpack servers.
20
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
21
	 *
22
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
23
	 * token, and $private is a secret that should never be displayed anywhere or sent
24
	 * over the network; it's used only for signing things.
25
	 *
26
	 * Blog Tokens can be "Normal" or "Special".
27
	 * * Normal: The result of a normal connection flow. They look like
28
	 *   "{$random_string_1}.{$random_string_2}"
29
	 *   That is, $token_key and $private are both random strings.
30
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
31
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
32
	 *   constant (rare).
33
	 * * Special: A connection token for sites that have gone through an alternative
34
	 *   connection flow. They look like:
35
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
36
	 *   That is, $private is a random string and $token_key has a special structure with
37
	 *   lots of semicolons.
38
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
39
	 *   JETPACK_BLOG_TOKEN constant.
40
	 *
41
	 * In particular, note that Normal Blog Tokens never start with ";" and that
42
	 * Special Blog Tokens always do.
43
	 *
44
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
45
	 * order:
46
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
47
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
48
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
49
	 *
50
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
51
	 * @param string|false $token_key If provided, check that the token matches the provided input.
52
	 *                                false                                : Use first token. Default.
53
	 *                                Jetpack_Data::MAGIC_NORMAL_TOKEN_KEY : Use first Normal Token.
54
	 *                                non-empty string                     : Use matching token
55
	 * @return object|false
56
	 */
57
	public static function get_access_token( $user_id = false, $token_key = false ) {
58
		$possible_special_tokens = array();
59
		$possible_normal_tokens  = array();
60
61
		if ( $user_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user_id of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
62
			if ( !$user_tokens = Jetpack_Options::get_option( 'user_tokens' ) ) {
63
				return false;
64
			}
65
			if ( $user_id === JETPACK_MASTER_USER ) {
66
				if ( !$user_id = Jetpack_Options::get_option( 'master_user' ) ) {
67
					return false;
68
				}
69
			}
70
			if ( !isset( $user_tokens[$user_id] ) || ! $user_tokens[$user_id] ) {
71
				return false;
72
			}
73
			$user_token_chunks = explode( '.', $user_tokens[$user_id] );
74
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
75
				return false;
76
			}
77
			if ( $user_id != $user_token_chunks[2] ) {
78
				return false;
79
			}
80
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
81
		} else {
82
			$stored_blog_token = Jetpack_Options::get_option( 'blog_token' );
83
			if ( $stored_blog_token ) {
84
				$possible_normal_tokens[] = $stored_blog_token;
85
			}
86
87
			$defined_tokens = Constants_Manager::is_defined( 'JETPACK_BLOG_TOKEN' )
88
				? explode( ',', Constants_Manager::get_constant( 'JETPACK_BLOG_TOKEN' ) )
89
				: array();
90
91
			foreach ( $defined_tokens as $defined_token ) {
92
				if ( ';' === $defined_token[0] ) {
93
					$possible_special_tokens[] = $defined_token;
94
				} else {
95
					$possible_normal_tokens[] = $defined_token;
96
				}
97
			}
98
		}
99
100
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
101
			$possible_tokens = $possible_normal_tokens;
102
		} else {
103
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
104
		}
105
106
		if ( ! $possible_tokens ) {
107
			return false;
108
		}
109
110
		$valid_token = false;
111
112
		if ( false === $token_key ) {
113
			// Use first token.
114
			$valid_token = $possible_tokens[0];
115
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
116
			// Use first normal token.
117
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
118
		} else {
119
			// Use the token matching $token_key or false if none.
120
			// Ensure we check the full key.
121
			$token_check = rtrim( $token_key, '.' ) . '.';
122
123
			foreach ( $possible_tokens as $possible_token ) {
124
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
125
					$valid_token = $possible_token;
126
					break;
127
				}
128
			}
129
		}
130
131
		if ( ! $valid_token ) {
132
			return false;
133
		}
134
135
		return (object) array(
136
			'secret' => $valid_token,
137
			'external_user_id' => (int) $user_id,
138
		);
139
	}
140
141
	/**
142
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
143
	 *
144
	 * @param $domain
145
	 * @param array $extra
146
	 *
147
	 * @return bool|WP_Error
148
	 */
149
	public static function is_usable_domain( $domain, $extra = array() ) {
150
151
		// If it's empty, just fail out.
152
		if ( ! $domain ) {
153
			return new WP_Error( 'fail_domain_empty', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) );
154
		}
155
156
		/**
157
		 * Skips the usuable domain check when connecting a site.
158
		 *
159
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
160
		 *
161
		 * @since 4.1.0
162
		 *
163
		 * @param bool If the check should be skipped. Default false.
164
		 */
165
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
166
			return true;
167
		}
168
169
		// None of the explicit localhosts.
170
		$forbidden_domains = array(
171
			'wordpress.com',
172
			'localhost',
173
			'localhost.localdomain',
174
			'127.0.0.1',
175
			'local.wordpress.test',         // VVV
176
			'local.wordpress-trunk.test',   // VVV
177
			'src.wordpress-develop.test',   // VVV
178
			'build.wordpress-develop.test', // VVV
179
		);
180 View Code Duplication
		if ( in_array( $domain, $forbidden_domains ) ) {
181
			return new WP_Error( 'fail_domain_forbidden', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', 'jetpack' ), $domain ) );
182
		}
183
184
		// No .test or .local domains
185 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
186
			return new WP_Error( 'fail_domain_tld', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', 'jetpack' ), $domain ) );
187
		}
188
189
		// No WPCOM subdomains
190 View Code Duplication
		if ( preg_match( '#\.wordpress\.com$#i', $domain ) ) {
191
			return new WP_Error( 'fail_subdomain_wpcom', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', 'jetpack' ), $domain ) );
192
		}
193
194
		// If PHP was compiled without support for the Filter module (very edge case)
195
		if ( ! function_exists( 'filter_var' ) ) {
196
			// Just pass back true for now, and let wpcom sort it out.
197
			return true;
198
		}
199
200
		return true;
201
	}
202
203
	/**
204
	 * Returns true if the IP address passed in should not be in a reserved range, even if PHP says that it is.
205
	 * See: https://bugs.php.net/bug.php?id=66229 and https://github.com/php/php-src/commit/d1314893fd1325ca6aa0831101896e31135a2658
206
	 *
207
	 * This function mirrors Jetpack_Data::php_bug_66229_check() in the WPCOM codebase.
208
	 */
209
	public static function php_bug_66229_check( $ip ) {
210
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
211
			return false;
212
		}
213
214
		$ip_arr = array_map( 'intval', explode( '.', $ip ) );
215
216
		if ( 128 == $ip_arr[0] && 0 == $ip_arr[1] ) {
217
			return true;
218
		}
219
220
		if ( 191 == $ip_arr[0] && 255 == $ip_arr[1] ) {
221
			return true;
222
		}
223
224
		return false;
225
	}
226
}
227