Completed
Push — feature/jetpack-packages-2 ( a52b3c )
by
unknown
242:43 queued 236:19
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
class Jetpack_Data {
4
	/*
5
	 * Used internally when we want to look for the Normal Blog Token
6
	 * without knowing its token key ahead of time.
7
	 */
8
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
9
10
	/**
11
	 * Gets the requested token.
12
	 *
13
	 * Tokens are one of two types:
14
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
15
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
16
	 *    are not associated with a user account. They represent the site's connection with
17
	 *    the Jetpack servers.
18
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
19
	 *
20
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
21
	 * token, and $private is a secret that should never be displayed anywhere or sent
22
	 * over the network; it's used only for signing things.
23
	 *
24
	 * Blog Tokens can be "Normal" or "Special".
25
	 * * Normal: The result of a normal connection flow. They look like
26
	 *   "{$random_string_1}.{$random_string_2}"
27
	 *   That is, $token_key and $private are both random strings.
28
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
29
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
30
	 *   constant (rare).
31
	 * * Special: A connection token for sites that have gone through an alternative
32
	 *   connection flow. They look like:
33
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
34
	 *   That is, $private is a random string and $token_key has a special structure with
35
	 *   lots of semicolons.
36
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
37
	 *   JETPACK_BLOG_TOKEN constant.
38
	 *
39
	 * In particular, note that Normal Blog Tokens never start with ";" and that
40
	 * Special Blog Tokens always do.
41
	 *
42
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
43
	 * order:
44
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
45
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
46
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
47
	 *
48
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
49
	 * @param string|false $token_key If provided, check that the token matches the provided input.
50
	 *                                false                                : Use first token. Default.
51
	 *                                Jetpack_Data::MAGIC_NORMAL_TOKEN_KEY : Use first Normal Token.
52
	 *                                non-empty string                     : Use matching token
53
	 * @return object|false
54
	 */
55
	public static function get_access_token( $user_id = false, $token_key = false ) {
56
		$possible_special_tokens = array();
57
		$possible_normal_tokens  = array();
58
59
		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...
60
			if ( !$user_tokens = Jetpack_Options::get_option( 'user_tokens' ) ) {
61
				return false;
62
			}
63
			if ( $user_id === JETPACK_MASTER_USER ) {
64
				if ( !$user_id = Jetpack_Options::get_option( 'master_user' ) ) {
65
					return false;
66
				}
67
			}
68
			if ( !isset( $user_tokens[$user_id] ) || ! $user_tokens[$user_id] ) {
69
				return false;
70
			}
71
			$user_token_chunks = explode( '.', $user_tokens[$user_id] );
72
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
73
				return false;
74
			}
75
			if ( $user_id != $user_token_chunks[2] ) {
76
				return false;
77
			}
78
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
79
		} else {
80
			$stored_blog_token = Jetpack_Options::get_option( 'blog_token' );
81
			if ( $stored_blog_token ) {
82
				$possible_normal_tokens[] = $stored_blog_token;
83
			}
84
85
			$defined_tokens = Jetpack_Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
86
				? explode( ',', Jetpack_Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
87
				: array();
88
89
			foreach ( $defined_tokens as $defined_token ) {
90
				if ( ';' === $defined_token[0] ) {
91
					$possible_special_tokens[] = $defined_token;
92
				} else {
93
					$possible_normal_tokens[] = $defined_token;
94
				}
95
			}
96
		}
97
98
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
99
			$possible_tokens = $possible_normal_tokens;
100
		} else {
101
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
102
		}
103
104
		if ( ! $possible_tokens ) {
105
			return false;
106
		}
107
108
		$valid_token = false;
109
110
		if ( false === $token_key ) {
111
			// Use first token.
112
			$valid_token = $possible_tokens[0];
113
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
114
			// Use first normal token.
115
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
116
		} else {
117
			// Use the token matching $token_key or false if none.
118
			// Ensure we check the full key.
119
			$token_check = rtrim( $token_key, '.' ) . '.';
120
121
			foreach ( $possible_tokens as $possible_token ) {
122
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
123
					$valid_token = $possible_token;
124
					break;
125
				}
126
			}
127
		}
128
129
		if ( ! $valid_token ) {
130
			return false;
131
		}
132
133
		return (object) array(
134
			'secret' => $valid_token,
135
			'external_user_id' => (int) $user_id,
136
		);
137
	}
138
139
	/**
140
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
141
	 *
142
	 * @param $domain
143
	 * @param array $extra
144
	 *
145
	 * @return bool|WP_Error
146
	 */
147
	public static function is_usable_domain( $domain, $extra = array() ) {
148
149
		// If it's empty, just fail out.
150
		if ( ! $domain ) {
151
			return new WP_Error( 'fail_domain_empty', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) );
152
		}
153
154
		/**
155
		 * Skips the usuable domain check when connecting a site.
156
		 *
157
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
158
		 *
159
		 * @since 4.1.0
160
		 *
161
		 * @param bool If the check should be skipped. Default false.
162
		 */
163
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
164
			return true;
165
		}
166
167
		// None of the explicit localhosts.
168
		$forbidden_domains = array(
169
			'wordpress.com',
170
			'localhost',
171
			'localhost.localdomain',
172
			'127.0.0.1',
173
			'local.wordpress.test',         // VVV
174
			'local.wordpress-trunk.test',   // VVV
175
			'src.wordpress-develop.test',   // VVV
176
			'build.wordpress-develop.test', // VVV
177
		);
178 View Code Duplication
		if ( in_array( $domain, $forbidden_domains ) ) {
179
			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 ) );
180
		}
181
182
		// No .test or .local domains
183 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
184
			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 ) );
185
		}
186
187
		// No WPCOM subdomains
188 View Code Duplication
		if ( preg_match( '#\.wordpress\.com$#i', $domain ) ) {
189
			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 ) );
190
		}
191
192
		// If PHP was compiled without support for the Filter module (very edge case)
193
		if ( ! function_exists( 'filter_var' ) ) {
194
			// Just pass back true for now, and let wpcom sort it out.
195
			return true;
196
		}
197
198
		return true;
199
	}
200
201
	/**
202
	 * Returns true if the IP address passed in should not be in a reserved range, even if PHP says that it is.
203
	 * See: https://bugs.php.net/bug.php?id=66229 and https://github.com/php/php-src/commit/d1314893fd1325ca6aa0831101896e31135a2658
204
	 *
205
	 * This function mirrors Jetpack_Data::php_bug_66229_check() in the WPCOM codebase.
206
	 */
207
	public static function php_bug_66229_check( $ip ) {
208
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
209
			return false;
210
		}
211
212
		$ip_arr = array_map( 'intval', explode( '.', $ip ) );
213
214
		if ( 128 == $ip_arr[0] && 0 == $ip_arr[1] ) {
215
			return true;
216
		}
217
218
		if ( 191 == $ip_arr[0] && 255 == $ip_arr[1] ) {
219
			return true;
220
		}
221
222
		return false;
223
	}
224
}
225