Completed
Push — branch-testing-74-auto ( 8dccdd )
by Jeremy
34:20 queued 24:52
created

class.jetpack-data.php (2 issues)

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
	 * Blog Tokens can be "Normal" or "Special".
21
	 * * Normal: The result of a normal connection flow. They look like
22
	 *   "{$random_string_1}.{$random_string_2}"
23
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
24
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
25
	 *   constant (rare).
26
	 * * Special: A connection token for sites that have gone through an alternative
27
	 *   connection flow. They look like:
28
	 *   ";{$special_id};.{$random_string}"
29
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
30
	 *   JETPACK_BLOG_TOKEN constant.
31
	 *
32
	 * In particular, note that Normal Blog Tokens never start with ";" and that
33
	 * Special Blog Tokens always do.
34
	 *
35
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
36
	 * order:
37
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
38
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
39
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
40
	 *
41
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
42
	 * @param string|false $token_key If provided, check that the token matches the provided input.
43
	 *                                false                                : Use first token. Default.
44
	 *                                Jetpack_Data::MAGIC_NORMAL_TOKEN_KEY : Use first Normal Token.
45
	 *                                non-empty string                     : Use matching token
46
	 * @return object|false
47
	 */
48
	public static function get_access_token( $user_id = false, $token_key = false ) {
49
		$possible_special_tokens = array();
50
		$possible_normal_tokens  = array();
51
52
		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...
53
			if ( !$user_tokens = Jetpack_Options::get_option( 'user_tokens' ) ) {
54
				return false;
55
			}
56
			if ( $user_id === JETPACK_MASTER_USER ) {
57
				if ( !$user_id = Jetpack_Options::get_option( 'master_user' ) ) {
58
					return false;
59
				}
60
			}
61
			if ( !isset( $user_tokens[$user_id] ) || ! $user_tokens[$user_id] ) {
62
				return false;
63
			}
64
			$user_token_chunks = explode( '.', $user_tokens[$user_id] );
65
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
66
				return false;
67
			}
68
			if ( $user_id != $user_token_chunks[2] ) {
69
				return false;
70
			}
71
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
72
		} else {
73
			$stored_blog_token = Jetpack_Options::get_option( 'blog_token' );
74
			if ( $stored_blog_token ) {
75
				$possible_normal_tokens[] = $stored_blog_token;
76
			}
77
78
			$defined_tokens = Jetpack_Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
79
				? explode( ',', Jetpack_Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
80
				: array();
81
82
			foreach ( $defined_tokens as $defined_token ) {
83
				if ( ';' === $defined_token[0] ) {
84
					$possible_special_tokens[] = $defined_token;
85
				} else {
86
					$possible_normal_tokens[] = $defined_token;
87
				}
88
			}
89
		}
90
91
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
92
			$possible_tokens = $possible_normal_tokens;
93
		} else {
94
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
95
		}
96
97
		if ( ! $possible_tokens ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $possible_tokens of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
98
			return false;
99
		}
100
101
		$valid_token = false;
102
103
		if ( false === $token_key ) {
104
			// Use first token.
105
			$valid_token = $possible_tokens[0];
106
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
107
			// Use first normal token.
108
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
109
		} else {
110
			// Use the token matching $token_key or false if none.
111
			// Ensure we check the full key.
112
			$token_check = rtrim( $token_key, '.' ) . '.';
113
114
			foreach ( $possible_tokens as $possible_token ) {
115
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
116
					$valid_token = $possible_token;
117
					break;
118
				}
119
			}
120
		}
121
122
		if ( ! $valid_token ) {
123
			return false;
124
		}
125
126
		return (object) array(
127
			'secret' => $valid_token,
128
			'external_user_id' => (int) $user_id,
129
		);
130
	}
131
132
	/**
133
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
134
	 *
135
	 * @param $domain
136
	 * @param array $extra
137
	 *
138
	 * @return bool|WP_Error
139
	 */
140
	public static function is_usable_domain( $domain, $extra = array() ) {
141
142
		// If it's empty, just fail out.
143
		if ( ! $domain ) {
144
			return new WP_Error( 'fail_domain_empty', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) );
145
		}
146
147
		/**
148
		 * Skips the usuable domain check when connecting a site.
149
		 *
150
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
151
		 *
152
		 * @since 4.1.0
153
		 *
154
		 * @param bool If the check should be skipped. Default false.
155
		 */
156
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
157
			return true;
158
		}
159
160
		// None of the explicit localhosts.
161
		$forbidden_domains = array(
162
			'wordpress.com',
163
			'localhost',
164
			'localhost.localdomain',
165
			'127.0.0.1',
166
			'local.wordpress.test',         // VVV
167
			'local.wordpress-trunk.test',   // VVV
168
			'src.wordpress-develop.test',   // VVV
169
			'build.wordpress-develop.test', // VVV
170
		);
171 View Code Duplication
		if ( in_array( $domain, $forbidden_domains ) ) {
172
			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 ) );
173
		}
174
175
		// No .test or .local domains
176 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
177
			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 ) );
178
		}
179
180
		// No WPCOM subdomains
181 View Code Duplication
		if ( preg_match( '#\.wordpress\.com$#i', $domain ) ) {
182
			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 ) );
183
		}
184
185
		// If PHP was compiled without support for the Filter module (very edge case)
186
		if ( ! function_exists( 'filter_var' ) ) {
187
			// Just pass back true for now, and let wpcom sort it out.
188
			return true;
189
		}
190
191
		return true;
192
	}
193
194
	/**
195
	 * Returns true if the IP address passed in should not be in a reserved range, even if PHP says that it is.
196
	 * See: https://bugs.php.net/bug.php?id=66229 and https://github.com/php/php-src/commit/d1314893fd1325ca6aa0831101896e31135a2658
197
	 *
198
	 * This function mirrors Jetpack_Data::php_bug_66229_check() in the WPCOM codebase.
199
	 */
200
	public static function php_bug_66229_check( $ip ) {
201
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
202
			return false;
203
		}
204
205
		$ip_arr = array_map( 'intval', explode( '.', $ip ) );
206
207
		if ( 128 == $ip_arr[0] && 0 == $ip_arr[1] ) {
208
			return true;
209
		}
210
211
		if ( 191 == $ip_arr[0] && 255 == $ip_arr[1] ) {
212
			return true;
213
		}
214
215
		return false;
216
	}
217
}
218