1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* The Jetpack Connection Secrets class file. |
4
|
|
|
* |
5
|
|
|
* @package automattic/jetpack-connection |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Automattic\Jetpack\Connection; |
9
|
|
|
|
10
|
|
|
use Jetpack_Options; |
11
|
|
|
use WP_Error; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* The Jetpack Connection Secrets class that is used to manage secrets. |
15
|
|
|
*/ |
16
|
|
|
class Secrets { |
17
|
|
|
|
18
|
|
|
const SECRETS_MISSING = 'secrets_missing'; |
19
|
|
|
const SECRETS_EXPIRED = 'secrets_expired'; |
20
|
|
|
const LEGACY_SECRETS_OPTION_NAME = 'jetpack_secrets'; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Deletes all connection secrets from the local Jetpack site. |
24
|
|
|
*/ |
25
|
|
|
public function delete_all() { |
26
|
|
|
Jetpack_Options::delete_raw_option( 'jetpack_secrets' ); |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Runs the wp_generate_password function with the required parameters. This is the |
31
|
|
|
* default implementation of the secret callable, can be overridden using the |
32
|
|
|
* jetpack_connection_secret_generator filter. |
33
|
|
|
* |
34
|
|
|
* @return String $secret value. |
35
|
|
|
*/ |
36
|
|
|
private function secret_callable_method() { |
37
|
|
|
return wp_generate_password( 32, false ); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Generates two secret tokens and the end of life timestamp for them. |
42
|
|
|
* |
43
|
|
|
* @param String $action The action name. |
44
|
|
|
* @param Integer|bool $user_id The user identifier. Defaults to `false`. |
45
|
|
|
* @param Integer $exp Expiration time in seconds. |
46
|
|
|
*/ |
47
|
|
|
public function generate( $action, $user_id = false, $exp = 600 ) { |
48
|
|
|
if ( false === $user_id ) { |
49
|
|
|
$user_id = get_current_user_id(); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
$callable = apply_filters( 'jetpack_connection_secret_generator', array( get_called_class(), 'secret_callable_method' ) ); |
53
|
|
|
|
54
|
|
|
$secrets = Jetpack_Options::get_raw_option( |
55
|
|
|
self::LEGACY_SECRETS_OPTION_NAME, |
56
|
|
|
array() |
57
|
|
|
); |
58
|
|
|
|
59
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
60
|
|
|
|
61
|
|
|
if ( |
62
|
|
|
isset( $secrets[ $secret_name ] ) && |
63
|
|
|
$secrets[ $secret_name ]['exp'] > time() |
64
|
|
|
) { |
65
|
|
|
return $secrets[ $secret_name ]; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
$secret_value = array( |
69
|
|
|
'secret_1' => call_user_func( $callable ), |
70
|
|
|
'secret_2' => call_user_func( $callable ), |
71
|
|
|
'exp' => time() + $exp, |
72
|
|
|
); |
73
|
|
|
|
74
|
|
|
$secrets[ $secret_name ] = $secret_value; |
75
|
|
|
|
76
|
|
|
$res = Jetpack_Options::update_raw_option( self::LEGACY_SECRETS_OPTION_NAME, $secrets ); |
77
|
|
|
return $res ? $secrets[ $secret_name ] : false; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Returns two secret tokens and the end of life timestamp for them. |
82
|
|
|
* |
83
|
|
|
* @param String $action The action name. |
84
|
|
|
* @param Integer $user_id The user identifier. |
85
|
|
|
* @return string|array an array of secrets or an error string. |
86
|
|
|
*/ |
87
|
|
|
public function get( $action, $user_id ) { |
88
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
89
|
|
|
$secrets = Jetpack_Options::get_raw_option( |
90
|
|
|
self::LEGACY_SECRETS_OPTION_NAME, |
91
|
|
|
array() |
92
|
|
|
); |
93
|
|
|
|
94
|
|
|
if ( ! isset( $secrets[ $secret_name ] ) ) { |
95
|
|
|
return self::SECRETS_MISSING; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
if ( $secrets[ $secret_name ]['exp'] < time() ) { |
99
|
|
|
$this->delete( $action, $user_id ); |
100
|
|
|
return self::SECRETS_EXPIRED; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
return $secrets[ $secret_name ]; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Deletes secret tokens in case they, for example, have expired. |
108
|
|
|
* |
109
|
|
|
* @param String $action The action name. |
110
|
|
|
* @param Integer $user_id The user identifier. |
111
|
|
|
*/ |
112
|
|
|
public function delete( $action, $user_id ) { |
113
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
114
|
|
|
$secrets = Jetpack_Options::get_raw_option( |
115
|
|
|
self::LEGACY_SECRETS_OPTION_NAME, |
116
|
|
|
array() |
117
|
|
|
); |
118
|
|
|
if ( isset( $secrets[ $secret_name ] ) ) { |
119
|
|
|
unset( $secrets[ $secret_name ] ); |
120
|
|
|
Jetpack_Options::update_raw_option( self::LEGACY_SECRETS_OPTION_NAME, $secrets ); |
121
|
|
|
} |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Verify a Previously Generated Secret. |
126
|
|
|
* |
127
|
|
|
* @param string $action The type of secret to verify. |
128
|
|
|
* @param string $secret_1 The secret string to compare to what is stored. |
129
|
|
|
* @param int $user_id The user ID of the owner of the secret. |
130
|
|
|
* @return WP_Error|string WP_Error on failure, secret_2 on success. |
131
|
|
|
*/ |
132
|
|
|
public function verify( $action, $secret_1, $user_id ) { |
133
|
|
|
$allowed_actions = array( 'register', 'authorize', 'publicize' ); |
134
|
|
|
if ( ! in_array( $action, $allowed_actions, true ) ) { |
135
|
|
|
return new WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 ); |
|
|
|
|
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
$user = get_user_by( 'id', $user_id ); |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* We've begun verifying the previously generated secret. |
142
|
|
|
* |
143
|
|
|
* @since 7.5.0 |
144
|
|
|
* |
145
|
|
|
* @param string $action The type of secret to verify. |
146
|
|
|
* @param \WP_User $user The user object. |
147
|
|
|
*/ |
148
|
|
|
do_action( 'jetpack_verify_secrets_begin', $action, $user ); |
149
|
|
|
|
150
|
|
|
$return_error = function ( WP_Error $error ) use ( $action, $user ) { |
151
|
|
|
/** |
152
|
|
|
* Verifying of the previously generated secret has failed. |
153
|
|
|
* |
154
|
|
|
* @since 7.5.0 |
155
|
|
|
* |
156
|
|
|
* @param string $action The type of secret to verify. |
157
|
|
|
* @param \WP_User $user The user object. |
158
|
|
|
* @param WP_Error $error The error object. |
159
|
|
|
*/ |
160
|
|
|
do_action( 'jetpack_verify_secrets_fail', $action, $user, $error ); |
161
|
|
|
|
162
|
|
|
return $error; |
163
|
|
|
}; |
164
|
|
|
|
165
|
|
|
$stored_secrets = $this->get( $action, $user_id ); |
166
|
|
|
$this->delete( $action, $user_id ); |
167
|
|
|
|
168
|
|
|
$error = null; |
169
|
|
|
if ( empty( $secret_1 ) ) { |
170
|
|
|
$error = $return_error( |
171
|
|
|
new WP_Error( |
172
|
|
|
'verify_secret_1_missing', |
|
|
|
|
173
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
174
|
|
|
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ), |
175
|
|
|
400 |
176
|
|
|
) |
177
|
|
|
); |
178
|
|
|
} elseif ( ! is_string( $secret_1 ) ) { |
179
|
|
|
$error = $return_error( |
180
|
|
|
new WP_Error( |
181
|
|
|
'verify_secret_1_malformed', |
|
|
|
|
182
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
183
|
|
|
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ), |
184
|
|
|
400 |
185
|
|
|
) |
186
|
|
|
); |
187
|
|
|
} elseif ( empty( $user_id ) ) { |
188
|
|
|
// $user_id is passed around during registration as "state". |
189
|
|
|
$error = $return_error( |
190
|
|
|
new WP_Error( |
191
|
|
|
'state_missing', |
|
|
|
|
192
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
193
|
|
|
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ), |
194
|
|
|
400 |
195
|
|
|
) |
196
|
|
|
); |
197
|
|
|
} elseif ( ! ctype_digit( (string) $user_id ) ) { |
198
|
|
|
$error = $return_error( |
199
|
|
|
new WP_Error( |
200
|
|
|
'state_malformed', |
|
|
|
|
201
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
202
|
|
|
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ), |
203
|
|
|
400 |
204
|
|
|
) |
205
|
|
|
); |
206
|
|
|
} elseif ( self::SECRETS_MISSING === $stored_secrets ) { |
207
|
|
|
$error = $return_error( |
208
|
|
|
new WP_Error( |
209
|
|
|
'verify_secrets_missing', |
|
|
|
|
210
|
|
|
__( 'Verification secrets not found', 'jetpack' ), |
211
|
|
|
400 |
212
|
|
|
) |
213
|
|
|
); |
214
|
|
|
} elseif ( self::SECRETS_EXPIRED === $stored_secrets ) { |
215
|
|
|
$error = $return_error( |
216
|
|
|
new WP_Error( |
217
|
|
|
'verify_secrets_expired', |
|
|
|
|
218
|
|
|
__( 'Verification took too long', 'jetpack' ), |
219
|
|
|
400 |
220
|
|
|
) |
221
|
|
|
); |
222
|
|
|
} elseif ( ! $stored_secrets ) { |
223
|
|
|
$error = $return_error( |
224
|
|
|
new WP_Error( |
225
|
|
|
'verify_secrets_empty', |
|
|
|
|
226
|
|
|
__( 'Verification secrets are empty', 'jetpack' ), |
227
|
|
|
400 |
228
|
|
|
) |
229
|
|
|
); |
230
|
|
|
} elseif ( is_wp_error( $stored_secrets ) ) { |
231
|
|
|
$stored_secrets->add_data( 400 ); |
|
|
|
|
232
|
|
|
$error = $return_error( $stored_secrets ); |
233
|
|
|
} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) { |
234
|
|
|
$error = $return_error( |
235
|
|
|
new WP_Error( |
236
|
|
|
'verify_secrets_incomplete', |
|
|
|
|
237
|
|
|
__( 'Verification secrets are incomplete', 'jetpack' ), |
238
|
|
|
400 |
239
|
|
|
) |
240
|
|
|
); |
241
|
|
|
} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) { |
242
|
|
|
$error = $return_error( |
243
|
|
|
new WP_Error( |
244
|
|
|
'verify_secrets_mismatch', |
|
|
|
|
245
|
|
|
__( 'Secret mismatch', 'jetpack' ), |
246
|
|
|
400 |
247
|
|
|
) |
248
|
|
|
); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
// Something went wrong during the checks, returning the error. |
252
|
|
|
if ( ! empty( $error ) ) { |
253
|
|
|
return $error; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* We've succeeded at verifying the previously generated secret. |
258
|
|
|
* |
259
|
|
|
* @since 7.5.0 |
260
|
|
|
* |
261
|
|
|
* @param string $action The type of secret to verify. |
262
|
|
|
* @param \WP_User $user The user object. |
263
|
|
|
*/ |
264
|
|
|
do_action( 'jetpack_verify_secrets_success', $action, $user ); |
265
|
|
|
|
266
|
|
|
return $stored_secrets['secret_2']; |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.