Completed
Push — add/handling-connection-errors ( b95e3b...06a037 )
by
unknown
72:15 queued 65:33
created

Error_Handler   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 282
rs 9.44
c 0
b 0
f 0
wmc 37
lcom 2
cbo 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A get_instance() 0 6 2
A report_error() 0 5 3
A should_report_error() 0 20 5
B store_error() 0 40 8
A get_user_id_from_token() 0 12 4
A get_stored_errors() 0 7 2
A get_verified_errors() 0 7 2
A delete_stored_errors() 0 3 1
A get_error_by_nonce() 0 11 4
A verify_error() 0 15 2
A register_verify_error_endpoint() 0 16 1
A verify_xml_rpc_error() 0 13 2
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
	 * Verifies an error based on the nonce
207
	 *
208
	 * Receives a nonce and finds the related error. If error is found, move it to the verified errors option.
209
	 *
210
	 * @param string $nonce The nonce created for the error we want to get.
211
	 * @return null|array Returns the error array representation or null if error not found.
212
	 */
213
	public function get_error_by_nonce( $nonce ) {
214
		$errors = $this->get_stored_errors();
215
		foreach ( $errors as $user_group ) {
216
			foreach ( $user_group as $error ) {
217
				if ( $error['nonce'] === $nonce ) {
218
					return $error;
219
				}
220
			}
221
		}
222
		return null;
223
	}
224
225
	/**
226
	 * Adds an error to the verified error list
227
	 *
228
	 * @param array $error The error array, as it was saved in the unverified errors list.
229
	 * @return void
230
	 */
231
	public function verify_error( $error ) {
232
233
		$verified_errors = $this->get_verified_errors();
234
		$error_code      = $error['error_code'];
235
		$user_id         = $error['user_id'];
236
237
		if ( ! isset( $verified_errors[ $error_code ] ) ) {
238
			$verified_errors[ $error_code ] = array();
239
		}
240
241
		$verified_errors[ $error_code ][ $user_id ] = $error;
242
243
		update_option( self::STORED_VERIFIED_ERRORS_OPTION, $verified_errors );
244
245
	}
246
247
	/**
248
	 * Register REST API end point for error hanlding.
249
	 *
250
	 * @since 8.7.0
251
	 *
252
	 * @return void
253
	 */
254
	public function register_verify_error_endpoint() {
255
		register_rest_route(
256
			'jetpack/v4',
257
			'/verify_xmlrpc_error',
258
			array(
259
				'methods'  => \WP_REST_Server::CREATABLE,
260
				'callback' => array( $this, 'verify_xml_rpc_error' ),
261
				'args'     => array(
262
					'nonce' => array(
263
						'required' => true,
264
						'type'     => 'string',
265
					),
266
				),
267
			)
268
		);
269
	}
270
271
	/**
272
	 * Handles verification that a xml rpc error is legit and came from WordPres.com
273
	 *
274
	 * @since 8.7.0
275
	 *
276
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
277
	 *
278
	 * @return boolean
279
	 */
280
	public function verify_xml_rpc_error( \WP_REST_Request $request ) {
281
282
		// TODO: decrypt data and confirm it came from WPCOM.
283
284
		$error = $this->get_error_by_nonce( $request['nonce'] );
285
		if ( $error ) {
286
			$this->verify_error( $error );
287
		}
288
289
		// We don't give any useful information away.
290
		return true;
291
292
	}
293
294
}
295