PluginProbe
UpdraftCentral Dashboard / 0.7.2
UpdraftCentral Dashboard v0.7.2
0.8.33 0.7.2 0.7.3 0.7.4 0.8.0 0.8.1 0.8.10 0.8.11 0.8.12 0.8.13 0.8.14 0.8.15 0.8.16 0.8.17 0.8.18 0.8.19 0.8.2 0.8.20 0.8.21 0.8.22 0.8.23 0.8.24 0.8.25 0.8.26 0.8.27 All 51 releases
updraftcentral / classes / class-siterpc.php

class-siterpc.php in UpdraftCentral Dashboard 0.7.2, at classes/class-siterpc.php

526 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!class_exists('UpdraftCentral_Remote_Communications')) :
4
5 /**
6 * This class dispatches command(s) to the remote website
7 *
8 * Pre-validates submitted data, sends command and processes
9 * the response received. Also responsible for caching result data
10 * whenever applicable, and can pull cached data whether or not having
11 * a maximum age (in seconds).
12 */
13 class UpdraftCentral_Remote_Communications {
14
15 private $is_preencrypted;
16
17 private $command;
18
19 private $rc;
20
21 private $user;
22
23 private $data;
24
25 private $site_id;
26
27 private $site;
28
29 private $ud_rpc;
30
31 private $site_meta;
32
33 private $admin_url;
34
35 private $errors;
36
37 private $response;
38
39 private $table_prefix;
40
41 /**
42 * UpdraftCentral_Remote_Communications constructor.
43 *
44 * Does initial checks, pre-load certain properties needed to communicate to the remote website
45 * and loads an error collection needed when returning errors to the caller.
46 *
47 * @param object $user An UpdraftCentral_User instance
48 * @param array $response An initial response array
49 * @param array $post_data Request data for processing
50 * @return self
51 */
52 public function __construct($user, $response, $post_data) {
53 if (!is_a($user, 'UpdraftCentral_User')) {
54 throw new Exception('Unexpected parameter. Argument to UpdraftCentral_Remote_Communications needs to be of type UpdraftCentral_User.');
55 }
56
57 $this->rc = UpdraftCentral();
58 $this->user = $user;
59 $this->data = $post_data;
60 $this->response = $response;
61
62 $this->is_preencrypted = !empty($post_data['site_rpc_preencrypted']);
63 $this->site_id = (int) $post_data['site_id'];
64 $this->site = $user->sites[$this->site_id];
65 $this->ud_rpc = $this->rc->get_udrpc($this->site->key_name_indicator);
66 $this->table_prefix = defined('UPDRAFTCENTRAL_TABLE_PREFIX') ? UPDRAFTCENTRAL_TABLE_PREFIX : 'updraftcentral_';
67
68 $this->site_meta = empty($user->sites_meta[$this->site->site_id]) ? array() : $user->sites_meta[$this->site->site_id];
69 $this->admin_url = empty($this->site->admin_url) ? $this->site->url : untrailingslashit($this->site->admin_url).'/admin-ajax.php';
70 if (preg_match('#/admin-ajax.php$#', $this->admin_url)) {
71 // wp-admin/admin-ajax.php before WP 3.5 will die() if $_REQUEST['action'] is not set (3.2) or is empty (3.4). Later WP versions also check that, but after (instead of before) wp-load.php, which is where we are ultimately hooked in.
72 $this->admin_url .= '?action=updraft_central';
73 }
74
75 $this->ud_rpc->set_destination_url($this->admin_url);
76 $this->load_errors();
77 }
78
79 /**
80 * Loads required objects if they're currently not available for the
81 * current request/process.
82 *
83 * @internal
84 */
85 private function maybe_load_objects() {
86 // We'll make sure that we don't have an empty site_meta instance or
87 // any object needed by the process.
88 if (empty($this->rc)) {
89 $this->rc = UpdraftCentral();
90 }
91
92 if (empty($this->rc->site_meta)) {
93 if (!class_exists('UpdraftCentral_Site_Meta')) include_once UD_CENTRAL_DIR.'/classes/site-meta.php';
94 $this->rc->site_meta = new UpdraftCentral_Site_Meta($this->table_prefix);
95 }
96 }
97
98 /**
99 * Loads error collection that will be referenced when
100 * returning specific error code for a failed process.
101 *
102 * @internal
103 */
104 private function load_errors() {
105 $errors = array(
106 'generic_error' => __('An error has occurred while processing your request.', 'updraftcentral'),
107 'missing_data' => __('Missing information', 'updraftcentral'),
108 'nonexistent_site' => sprintf(__('This site (%d / %d) was not found', 'updraftcentral'), $this->user->user_id, $this->site_id),
109 'nonexistent_site_key' => sprintf(__('The key for this site (%d / %d) was not found', 'updraftcentral'), $this->user->user_id, $this->site_id),
110 'site_unlicensed' => apply_filters('updraftcentral_site_unlicensed_message', __('You have more sites in your dashboard than licences. As a result, you cannot perform actions on this site.', 'updraftcentral').' '.__('You will need to obtain more licences, or remove some sites.', 'updraftcentral')),
111 'cannot_contact_localdev' => __('You cannot contact a website hosted on a site-local network (e.g. localhost) from this dashboard - it cannot be reached.', 'updraftcentral'),
112 'no_digest_before_php54' => sprintf(__('To use HTTP digest authentication, your server running the UpdraftCentral dashboard needs at least PHP %s (your version is %s)', 'updraftcentral'), '5.4', PHP_VERSION),
113 'incompatible_udrpc_php' => sprintf(__('The loaded UDRPC library (%s) is too old - you probably need to update your installed UpdraftPlus on the server', 'updraftcentral'), $this->ud_rpc->version),
114 );
115
116 $this->errors = $errors;
117 }
118
119 /**
120 * Pulls information regarding the error
121 *
122 * @internal
123 * @param string $code A unique error code that is used to pull the error information
124 * @return array - Returns the error information for a specific error key/code
125 */
126 private function return_error($code) {
127 $response = array(
128 'responsetype' => 'error',
129 'code' => $code,
130 'message' => isset($this->errors[$code]) ? $this->errors[$code] : $this->errors['generic_error']
131 );
132
133 return $response;
134 }
135
136 /**
137 * Validates if the cached data is still acceptable based from
138 * the maximum age required
139 *
140 * @internal
141 * @param string $created The time (number of seconds) when the data was cached/stored in DB
142 * @param int $maximum_age The maximum age (in seconds) to consider the cached data is still acceptable for consumption
143 * @return boolean - True if cached data is within the maximum age required, False otherwise
144 */
145 private function check_data_validity($created, $maximum_age) {
146 return (time() - (int) $created) <= $maximum_age;
147 }
148
149 /**
150 * Cache the response received from the remote website
151 *
152 * @internal
153 * @param array $response The response array that contains the result of the command that was sent to the remote website
154 * @param string $meta_key A unique string that serves as an identifier for the cached data
155 * @return boolean - True if data was successfully cached, False otherwise
156 */
157 private function cache_response($response, $meta_key) {
158 if (!empty($response)) {
159 // Check to see if we have an existing meta for data caching. If so, update the
160 // cached data with the current response, otherwise we'll create a new entry in DB.
161 $check = $this->rc->site_meta->get_site_meta($this->site_id, $meta_key, true);
162
163 if (!empty($check)) {
164 $result = $this->rc->site_meta->update_site_meta($this->site_id, $meta_key, $response);
165 } else {
166 $result = $this->rc->site_meta->add_site_meta($this->site_id, $meta_key, $response);
167 }
168
169 if (false !== $result) return true;
170 }
171
172 return false;
173 }
174
175 /**
176 * Pulls the cached/stored data either from the in-memory data loaded
177 * by the UpdraftCentral_User or from DB
178 *
179 * @internal
180 * @param string $meta_key A unique string that serves as an identifier for the cached data
181 * @param int $maximum_age The maximum age (in seconds) to consider the cached data is still acceptable for consumption
182 * @return mixed - Returns the stored data if successful, False otherwise
183 */
184 private function get_cached_data($meta_key, $maximum_age) {
185 $maximum_age = (!empty($maximum_age)) ? (int) $maximum_age : false;
186 if ($maximum_age) {
187 // Check in-memory data first (loaded under UpdraftCentral_User->load_user_sites()) before checking the DB
188 $loaded_data = $this->user->sites_meta[$this->site_id][$meta_key];
189 if (!empty($loaded_data)) {
190 if ($this->check_data_validity($loaded_data->created, $maximum_age)) {
191 return $loaded_data->value;
192 }
193 }
194
195 // Anything can happen from the time the sites meta were loaded during UDC page load.
196 // Due to ajax requests it can be populated along the way, thus, if the above in-memory check
197 // fails we'll proceed in checking the DB.
198 $stored_data = $this->rc->site_meta->get_site_meta($this->site_id, $meta_key, true, $maximum_age);
199 if (!empty($stored_data)) {
200 return $stored_data;
201 }
202 }
203
204 return false;
205 }
206
207 /**
208 * Cache response whenever applicable
209 *
210 * @internal
211 * @param string $command The command to execute
212 * @param array $response The response array that contains the result of the command that was sent to the remote website
213 * @param string $meta_key A unique string that serves as an identifier for the cached data
214 * @param boolean $force_save Optional. A flag to indicate whether we need to force the saving of the response from the remote website
215 * @return array - The original response array
216 */
217 private function maybe_cache_response($command, $response, $meta_key, $force_save = false) {
218 // We're only saving the response to DB if $reply is not an instance
219 // of WP_Error class and command is currently not empty.
220 $reply = isset($response['reply']) ? $response['reply'] : $response;
221
222 if (!is_wp_error($reply) && !empty($command)) {
223 $remote_response = isset($reply['response']) ? $reply['response'] : null;
224
225 // Caching is only applicable to non-preencrypted data.
226 if (!$this->is_preencrypted && !empty($remote_response) && 'rpcerror' !== $remote_response) {
227
228 if ($force_save) {
229 // If $force_save is true then we're forced to save the response to DB. Most likely, this is
230 // set(required) from a CRON process where the results are force to be saved into DB.
231 $this->cache_response($response, $meta_key);
232 } else {
233 // Here, we're only storing/caching the response when needed, as not all commands
234 // requires caching. Add an 'updraftcentral_cache_commands' filter the UDC module if you wish to cache
235 // any specific commands.
236 $commands = apply_filters('updraftcentral_cache_commands', array());
237 if (in_array($command, $commands) && !empty($meta_key)) {
238 $this->cache_response($response, $meta_key);
239 }
240 }
241
242 }
243 }
244
245 return $response;
246 }
247
248 /**
249 * Do a post check of the result/response of the currently executed command
250 *
251 * @param array $result The result of the command that was sent to the remote website
252 * @return array - The request's response
253 */
254 private function response_post_check($result) {
255
256 $caught_output = $result['caught_output'];
257 $reply = $result['reply'];
258 $response = $this->response;
259
260 // Pass on PHP events from the remote side
261 if (!empty($response['data']['php_events'])) $response['php_events'] = $response['data']['php_events'];
262 if (!empty($caught_output)) $response['mothership_caught_output'] = $caught_output;
263 if (is_wp_error($reply)) {
264 $response['responsetype'] = 'error';
265 $response['message'] = $reply->get_error_message();
266 $response['code'] = $reply->get_error_code();
267 $response['data'] = $reply->get_error_data();
268 } elseif (is_array($reply) && !empty($reply['response']) && 'error' == $reply['response']) {
269 $response['responsetype'] = 'error';
270 $response['message'] = empty($reply['message']) ? __('The connection to the remote site returned an error', 'updraftcentral') : $reply['message'];
271 $response['data'] = $reply;
272 } elseif ((!$this->is_preencrypted && (!is_array($reply) || empty($reply['response']) || (('ping' == $this->command && 'pong' != $reply['response'])) && 'rpcok' != $reply['response'])) || ($this->is_preencrypted && null === ($decoded_reply = json_decode($reply, true)) && (false == ($found_at = strpos($reply, '{"format":')) || null === ($decoded_reply = json_decode(substr($reply, $found_at), true))))) {
273 // If it is pre-encrypted, we expect a field 'udrpc_message' in the reply (after it's been JSON-decoded). We could check that. But instead, we just pass it back to the browser, since it'll be checked there anyway.
274 $response['responsetype'] = 'error';
275 $response['message'] = __('There was an error in contacting the remote site.', 'updraftcentral').' '.__("You should check that the remote site is online, is not firewalled, has remote control enabled, and that no security module is blocking the access. Then, check the logs on the remote site and your browser's JavaScript console.", 'updraftcentral').' '.__('If none of that helps, then you should try re-adding the site with a fresh key.', 'updraftcentral');
276 $response['data'] = $reply;
277 $response['code'] = 'no_pong';
278 } else {
279 $response['responsetype'] = 'ok';
280 $response['message'] = __('The site was connected to, and returned a response', 'updraftcentral');
281 if ($this->is_preencrypted) {
282 $response['wrapped_response'] = $decoded_reply;
283 } elseif ('siteinfo' == $this->command) {
284 $response['rpc_response'] = $this->user->deep_sanitize($reply);
285 } else {
286 $response['rpc_response'] = $reply;
287 }
288 }
289
290 return $response;
291 }
292
293 /**
294 * Generates a unique key out from the site id, command and data parameters to
295 * be used as a meta key when saving the data to the DB.
296 *
297 * @param string $command The current command to execute
298 * @param array $data An array containing the command parameters
299 * @return string - The generated key
300 */
301 private function generate_meta_key($command, $data) {
302 return 'cached_data_'.md5('_site'.$this->site_id.'_command'.$command.serialize($data));
303 }
304
305 /**
306 * Pulls the meta key and cached data if available
307 *
308 * @param string $command The current command to execute
309 * @param array $data An array containing the command parameters
310 * @return array
311 */
312 private function pull_data($command, $data) {
313 // Default: 10 minutes (600 seconds) if UPDRAFTCENTRAL_DATA_MAXIMUM_AGE is not defined
314 //
315 // N.B. The maximum_age should be found attached to the "data" parameter if the developer
316 // wishes to have a specific maximum age (freshness of data) for the current command, otherwise
317 // the default_maximum_age will be used.
318 $default_maximum_age = (defined('UPDRAFTCENTRAL_DATA_MAXIMUM_AGE')) ? UPDRAFTCENTRAL_DATA_MAXIMUM_AGE : 600;
319
320 $key = $this->generate_meta_key($command, $data);
321 $maximum_age = isset($data['maximum_age']) ? $data['maximum_age'] : $default_maximum_age;
322
323 // Pull and return cached data whenever applicable.
324 $cached_data = $this->get_cached_data($key, $maximum_age);
325
326 $data = array();
327 if (!empty($cached_data)) {
328 $data = $this->response_post_check(maybe_unserialize($cached_data));
329 }
330
331 return array(
332 'key' => $key,
333 'data' => $data
334 );
335 }
336
337 /**
338 * Sends command to the remote website and processes the response
339 *
340 * @param boolean $force_save Optional. A flag to indicate whether we need to force the saving of the response from the remote website
341 * @return array - The response array that contains the result of the currently processed command
342 */
343 public function send_message($force_save = false) {
344
345 $this->command = isset($this->data['data']['command']) ? (string) $this->data['data']['command'] : '';
346 $is_multiplexed = ('core.execute_commands' === $this->command) ? true : false;
347
348 $data = isset($this->data['data']['data']) ? $this->data['data']['data'] : null;
349 if ($this->is_preencrypted) $data = $this->data['wrapped_message'];
350
351 if (!empty($data)) {
352
353 // Possibly load required objects for this process if not available.
354 $this->maybe_load_objects();
355
356 $cached_data = $this->pull_data($this->command, $data);
357 $computed_meta_key = $cached_data['key'];
358
359 // We're pulling individual cache data for the same sub-command
360 // if we've already had a previously cached response.
361 if ($is_multiplexed) {
362 $computed_keys = array();
363 $result = array();
364
365 // Make sure that we're getting the latest cached data for the command
366 // instead of an old result from a multiplexed command's response.
367 //
368 // N.B. Need to run through all available commands under the multiplexed
369 // command executed to get the latest (fresh) data that was previously cached
370 // if available.
371 foreach ($data as $sub_command => $sub_data) {
372 $cached = $this->pull_data($sub_command, $sub_data);
373
374 $computed_keys[$sub_command] = $cached['key'];
375 if (!empty($cached['data'])) $result[$sub_command] = $cached['data'];
376 }
377
378 // Update the reply with the latest cached response whenever applicable.
379 if (!empty($result) && !empty($cached_data['data'])) {
380 $cached_data['data']['reply'] = $result;
381 }
382 }
383
384 // Return any cached data found if not empty.
385 if (!empty($cached_data['data'])) {
386
387 // With commands that were saved under a multiplexed command, the caught_output
388 // is not applicable (it is only applicable to the "core.execute_commands" - multiplexed command), thus,
389 // we're making sure that we're returning a consistent (expected) data result properties even if its empty.
390 //
391 // N.B. This is safe since we're only saving (caching) if we don't encounter any error in the caught_output
392 // as it won't make any sense if we're saving errors.
393 if (isset($cached_data['data']['reply']) && !isset($cached_data['data']['caught_output'])) {
394 $command_data = array(
395 'caught_output' => '',
396 'reply' => $cached_data['data']['reply']
397 );
398
399 $cached_data['data'] = $this->response_post_check($command_data);
400 }
401
402 return $cached_data['data'];
403 }
404 }
405
406 // If we reached this far then that would mean that we currently don't have any cache data
407 // associated with the submitted command. Thus, we will proceed in sending the request to the remote website.
408
409 // @codingStandardsIgnoreLine
410 @ob_start();
411
412 if (!empty($this->site_meta['http_username']->value)) {
413 $authentication_method = empty($this->site_meta['http_authentication_method']->value) ? 'basic' : $this->site_meta['http_authentication_method']->value;
414 $http_password = empty($this->site_meta['http_password']->value) ? '' : (string) $this->site_meta['http_password']->value;
415
416 if ('basic' != $authentication_method && version_compare(PHP_VERSION, '5.4', '<')) {
417 $error_code = 'no_digest_before_php54';
418 $reply = new WP_Error($error_code, $this->errors[$error_code], PHP_VERSION);
419 } else {
420 // Guzzle supports HTTP digest authentication - the WP HTTP API doesn't.
421 include_once UD_CENTRAL_DIR.'/vendor/autoload.php';
422 $guzzle_client = new GuzzleHttp\Client();
423
424 if (!method_exists($this->ud_rpc, 'set_http_transport') || !method_exists($this->ud_rpc, 'set_http_credentials')) {
425 // That's the probable cause, because we can assume that UC has a bundled UDRPC that's new enough.
426 $error_code = 'incompatible_udrpc_php';
427 $reply = new WP_Error($error_code, $this->errors[$error_code]);
428 } else {
429 $this->ud_rpc->set_http_transport($guzzle_client);
430 $this->ud_rpc->set_http_credentials(array('username' => $this->site_meta['http_username']->value, 'password' => $http_password, 'authentication_method' => $authentication_method));
431 }
432 }
433 }
434
435 if ($this->is_preencrypted) {
436 // Command is not applicable to this area, so we empty it if we have a pre-encrypted data
437 // since subsequent process may want to check the command before proceeding.
438 $this->command = '';
439
440 $reply = $this->user->send_message($this->ud_rpc, '__updraftcentral_internal_preencrypted', $data, 30);
441 } else {
442 $this->ud_rpc->set_key_local($this->site->key_local_private);
443 $this->ud_rpc->set_key_remote($this->site->key_remote_public);
444 $this->ud_rpc->activate_replay_protection();
445
446 $reply = $this->user->send_message($this->ud_rpc, $this->command, $data, 30);
447 }
448
449 // @codingStandardsIgnoreStart
450 $caught_output = @ob_get_contents();
451 @ob_end_clean();
452 // @codingStandardsIgnoreEnd
453
454
455 // Cache response whenever applicable
456 $result = $this->response_post_check($this->maybe_cache_response($this->command, array(
457 'caught_output' => $caught_output,
458 'reply' => $reply
459 ), $computed_meta_key, $force_save));
460
461 // Check if we're currently running a multiplexed command. If so,
462 // we're going to save each individual results separately, so that it can be retrieved later
463 // when a command is sent individually with the same signature (command name and data parameters).
464 if ($is_multiplexed) {
465 if ('ok' === $result['responsetype'] && is_array($result['rpc_response'])) {
466 foreach ($result['rpc_response'] as $command => $data) {
467 $key = $computed_keys[$command];
468 if (isset($key)) {
469 $this->maybe_cache_response($command, $data, $key, $force_save);
470 }
471 }
472 }
473 }
474
475 return $result;
476 }
477
478 /**
479 * Validates the submitted data before sending the command to the remote site
480 *
481 * @return mixed - True on success, an error array containing the error information on failure
482 */
483 public function validate_input() {
484 if (!empty($this->data)) {
485
486 // Check the sent data
487 if ($this->is_preencrypted) {
488 if (!isset($this->data['wrapped_message']) || !is_array($this->data['wrapped_message']) || empty($this->data['site_id']) || !is_numeric($this->data['site_id'])) {
489
490 return $this->return_error('missing_data');
491 }
492 } else {
493 if (!isset($this->data['data']) || !is_array($this->data['data']) || empty($this->data['data']['command']) || empty($this->data['site_id']) || !is_numeric($this->data['site_id'])) {
494
495 return $this->return_error('missing_data');
496 }
497 }
498
499 // This is also a security check - whether the specified site belongs to the current user
500 if (empty($this->site)) {
501 return $this->return_error('nonexistent_site');
502 }
503
504 if (empty($this->site->key_local_private)) {
505 return $this->return_error('nonexistent_site_key');
506 }
507
508 if (!empty($this->site->unlicensed)) {
509 return $this->return_error('site_unlicensed');
510 }
511
512 if ($this->rc->url_looks_internal($this->site->url) && !$this->rc->url_looks_internal(site_url()) && !apply_filters('updraftcentral_allow_contacting_internal_url_from_server', true, $this->site->url)) {
513 $url_scheme = strtolower(parse_url($this->site->url, PHP_URL_SCHEME));
514
515 return $this->return_error('cannot_contact_localdev');
516 }
517
518 return true;
519 } else {
520 return $this->return_error('missing_data');
521 }
522 }
523 }
524
525 endif;
526