Completed
Push — add/handling-connection-errors ( ad1b5d...35744a )
by
unknown
13:50 queued 50s
created

Error_Handler::delete_verified_errors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * The Jetpack Connection error class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
namespace Automattic\Jetpack\Connection;
9
10
/**
11
 * The Jetpack Connection Errors that handles errors
12
 */
13
class Error_Handler {
14
15
	/**
16
	 * The name of the option that stores the errors
17
	 *
18
	 * @var string
19
	 */
20
	const STORED_ERRORS_OPTION = 'jetpack_connection_xmlrpc_errors';
21
22
	/**
23
	 * The name of the option that stores the errors
24
	 *
25
	 * @var string
26
	 */
27
	const STORED_VERIFIED_ERRORS_OPTION = 'jetpack_connection_xmlrpc_verified_errors';
28
29
	/**
30
	 * The prefix of the transient that controls the gate for each error code
31
	 *
32
	 * @var string
33
	 */
34
	const ERROR_REPORTING_GATE = 'jetpack_connection_error_reporting_gate_';
35
36
	/**
37
	 * Holds the instance of this singleton class
38
	 *
39
	 * @var Error_Handler $instance
40
	 */
41
	public static $instance = null;
42
43
	/**
44
	 * Initialize hooks
45
	 */
46
	private function __construct() {
47
		add_action( 'rest_api_init', array( $this, 'register_verify_error_endpoint' ) );
48
	}
49
50
	/**
51
	 * Gets the instance of this singleton class
52
	 *
53
	 * @return Error_Handler $instance
54
	 */
55
	public static function get_instance() {
56
		if ( is_null( self::$instance ) ) {
57
			self::$instance = new self();
58
		}
59
		return self::$instance;
60
	}
61
62
	/**
63
	 * Keep track of a connection error that was encoutered
64
	 *
65
	 * @param \WP_Error $error the error object.
66
	 * @param boolean   $force Force the report, even if should_report_error is false.
67
	 * @return void
68
	 */
69
	public function report_error( \WP_Error $error, $force = false ) {
70
		if ( $this->should_report_error( $error ) || $force ) {
71
			$this->store_error( $error );
72
		}
73
	}
74
75
	/**
76
	 * Checks the status of the gate
77
	 *
78
	 * This protects the site (and WPCOM) against over loads.
79
	 *
80
	 * @param \WP_Error $error the error object.
81
	 * @return boolean $should_report True if gate is open and the error should be reported.
82
	 */
83
	public function should_report_error( \WP_Error $error ) {
84
85
		if ( defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG ) {
86
			return true;
87
		}
88
89
		$bypass_gate = apply_filters( 'jetpack_connection_bypass_error_reporting_gate', false );
90
		if ( true === $bypass_gate ) {
91
			return true;
92
		}
93
94
		$transient = self::ERROR_REPORTING_GATE . $error->get_error_code();
0 ignored issues
show
Bug introduced by
The method get_error_code() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
95
96
		if ( get_transient( $transient ) ) {
97
			return false;
98
		}
99
100
		set_transient( $transient, true, HOUR_IN_SECONDS );
101
		return true;
102
	}
103
104
	/**
105
	 * Stores the error in the database so we know there is an issue and can inform the user
106
	 *
107
	 * @param \WP_Error $error the error object.
108
	 * @return boolean False if stored errors were not updated and true if stored errors were updated.
109
	 */
110
	public function store_error( \WP_Error $error ) {
111
		$stored_errors = $this->get_stored_errors();
112
113
		$data       = $error->get_error_data();
0 ignored issues
show
Bug introduced by
The method get_error_data() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
114
		$error_code = $error->get_error_code();
0 ignored issues
show
Bug introduced by
The method get_error_code() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
115
116
		if ( ! isset( $data['signature_details'] ) || ! is_array( $data['signature_details'] ) ) {
117
			return false;
118
		}
119
120
		$data = $data['signature_details'];
121
122
		if ( ! isset( $data['token'] ) || empty( $data['token'] ) ) {
123
			return false;
124
		}
125
126
		$user_id = $this->get_user_id_from_token( $data['token'] );
127
128
		if ( ! isset( $stored_errors[ $error_code ] ) || ! is_array( $stored_errors[ $error_code ] ) ) {
129
			$stored_errors[ $error_code ] = array();
130
		}
131
132
		$stored_errors[ $error_code ][ $user_id ] = array(
133
			'error_code'    => $error_code,
134
			'user_id'       => $user_id,
135
			'error_message' => $error->get_error_message(),
0 ignored issues
show
Bug introduced by
The method get_error_message() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
136
			'error_data'    => $data,
137
			'timestamp'     => time(),
138
			'nonce'         => wp_generate_password( 10, false ),
139
		);
140
141
		// Let's store a maximum of 5 different user ids for each error code.
142
		if ( count( $stored_errors[ $error_code ] ) > 5 ) {
143
			// array_shift will destroy keys here because they are numeric, so manually remove first item.
144
			$keys = array_keys( $stored_errors[ $error_code ] );
145
			unset( $stored_errors[ $error_code ][ $keys[0] ] );
146
		}
147
148
		return update_option( self::STORED_ERRORS_OPTION, $stored_errors );
149
	}
150
151
	/**
152
	 * Extracts the user ID from a token
153
	 *
154
	 * @param string $token the token used to make the xml-rpc request.
155
	 * @return string $the user id or `invalid` if user id not present.
156
	 */
157
	public function get_user_id_from_token( $token ) {
158
		$parsed_token = explode( ':', wp_unslash( $token ) );
159
160
		if ( isset( $parsed_token[2] ) && ! empty( $parsed_token[2] ) && ctype_digit( $parsed_token[2] ) ) {
161
			$user_id = $parsed_token[2];
162
		} else {
163
			$user_id = 'invalid';
164
		}
165
166
		return $user_id;
167
168
	}
169
170
	/**
171
	 * Gets the reported errors stored in the database
172
	 *
173
	 * @return array $errors
174
	 */
175
	public function get_stored_errors() {
176
		$stored_errors = get_option( self::STORED_ERRORS_OPTION );
177
		if ( ! is_array( $stored_errors ) ) {
178
			$stored_errors = array();
179
		}
180
		return $stored_errors;
181
	}
182
183
	/**
184
	 * Gets the verified errors stored in the database
185
	 *
186
	 * @return array $errors
187
	 */
188
	public function get_verified_errors() {
189
		$verified_errors = get_option( self::STORED_VERIFIED_ERRORS_OPTION );
190
		if ( ! is_array( $verified_errors ) ) {
191
			$verified_errors = array();
192
		}
193
		return $verified_errors;
194
	}
195
196
	/**
197
	 * Delete the reported errors stored in the database
198
	 *
199
	 * @return boolean True, if option is successfully deleted. False on failure.
200
	 */
201
	public function delete_stored_errors() {
202
		return delete_option( self::STORED_ERRORS_OPTION );
203
	}
204
205
	/**
206
	 * Delete the verified errors stored in the database
207
	 *
208
	 * @return boolean True, if option is successfully deleted. False on failure.
209
	 */
210
	public function delete_verified_errors() {
211
		return delete_option( self::STORED_VERIFIED_ERRORS_OPTION );
212
	}
213
214
	/**
215
	 * Verifies an error based on the nonce
216
	 *
217
	 * Receives a nonce and finds the related error. If error is found, move it to the verified errors option.
218
	 *
219
	 * @param string $nonce The nonce created for the error we want to get.
220
	 * @return null|array Returns the error array representation or null if error not found.
221
	 */
222
	public function get_error_by_nonce( $nonce ) {
223
		$errors = $this->get_stored_errors();
224
		foreach ( $errors as $user_group ) {
225
			foreach ( $user_group as $error ) {
226
				if ( $error['nonce'] === $nonce ) {
227
					return $error;
228
				}
229
			}
230
		}
231
		return null;
232
	}
233
234
	/**
235
	 * Adds an error to the verified error list
236
	 *
237
	 * @param array $error The error array, as it was saved in the unverified errors list.
238
	 * @return void
239
	 */
240
	public function verify_error( $error ) {
241
242
		$verified_errors = $this->get_verified_errors();
243
		$error_code      = $error['error_code'];
244
		$user_id         = $error['user_id'];
245
246
		if ( ! isset( $verified_errors[ $error_code ] ) ) {
247
			$verified_errors[ $error_code ] = array();
248
		}
249
250
		$verified_errors[ $error_code ][ $user_id ] = $error;
251
252
		update_option( self::STORED_VERIFIED_ERRORS_OPTION, $verified_errors );
253
254
	}
255
256
	/**
257
	 * Register REST API end point for error hanlding.
258
	 *
259
	 * @since 8.7.0
260
	 *
261
	 * @return void
262
	 */
263
	public function register_verify_error_endpoint() {
264
		register_rest_route(
265
			'jetpack/v4',
266
			'/verify_xmlrpc_error',
267
			array(
268
				'methods'  => \WP_REST_Server::CREATABLE,
269
				'callback' => array( $this, 'verify_xml_rpc_error' ),
270
				'args'     => array(
271
					'nonce' => array(
272
						'required' => true,
273
						'type'     => 'string',
274
					),
275
				),
276
			)
277
		);
278
	}
279
280
	/**
281
	 * Handles verification that a xml rpc error is legit and came from WordPres.com
282
	 *
283
	 * @since 8.7.0
284
	 *
285
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
286
	 *
287
	 * @return boolean
288
	 */
289
	public function verify_xml_rpc_error( \WP_REST_Request $request ) {
290
291
		// TODO: decrypt data and confirm it came from WPCOM.
292
293
		$error = $this->get_error_by_nonce( $request['nonce'] );
294
295
		if ( $error ) {
296
			$this->verify_error( $error );
297
		}
298
299
		// We don't give any useful information away.
300
		return new \WP_REST_Response( true, 200 );
301
302
	}
303
304
}
305