PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / includes / class-wpo-ajax.php

class-wpo-ajax.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at includes/class-wpo-ajax.php

451 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('Access denied.');
4
5 if (!class_exists('WPO_Ajax')) :
6
7 class WPO_Ajax {
8
9 private $nonce;
10
11 private $subaction;
12
13 private $data;
14
15 private $commands;
16
17 private $results;
18
19 const HEARTBEAT_INTERVAL = 15; // in seconds
20
21 /**
22 * Constructor
23 */
24 private function __construct() {
25 add_action('wp_ajax_wp_optimize_ajax', array($this, 'handle_ajax_requests'));
26 add_filter('wp_optimize_heartbeat_ajax', array($this, 'handle_heartbeat_requests'));
27 add_filter('wp_optimize_is_heartbeat_valid_ajax_command', array($this, 'is_heartbeat_command_valid'));
28 }
29
30 /**
31 * Check if a command is valid for this class
32 *
33 * @param string $command
34 * @return bool
35 */
36 public function is_heartbeat_command_valid($command) {
37 $this->set_heartbeat_subaction($command);
38 $this->set_commands();
39 return !$this->is_invalid_command();
40 }
41
42 /**
43 * Return singleton instance
44 *
45 * @return WPO_Ajax Returns WPO_Ajax object
46 */
47 public static function get_instance() {
48 static $instance = null;
49 if (null === $instance) {
50 $instance = new self();
51 }
52 return $instance;
53 }
54
55 /**
56 * Handles heartbeat requests
57 *
58 * @param string $action The action we want to run
59 * @return mixed
60 */
61 public function handle_heartbeat_requests($action) {
62 $this->set_heartbeat_subaction($action);
63
64 if (!WP_Optimize()->current_user_can()) {
65 return wp_json_encode($this->send_user_capability_error_response(false));
66 }
67
68 if (is_multisite() && !WP_Optimize()->current_user_can('manage_network_options')) {
69 if (!$this->is_valid_multisite_command()) {
70 return wp_json_encode($this->send_invalid_multisite_command_error_response(false));
71 }
72 }
73
74 $this->set_commands();
75 if ($this->is_invalid_command()) {
76 $this->add_invalid_command_error_log_entry();
77 $this->set_invalid_command_error_response();
78 } else {
79 $this->execute_command();
80 $this->maybe_fix_status_box_content();
81 $this->set_error_response_on_wp_error();
82 $this->maybe_set_results_as_null();
83 }
84
85 $this->json_encode_results();
86
87 $json_last_error = json_last_error();
88 if ($json_last_error) {
89 $this->set_error_response_on_json_encode_error($json_last_error);
90 }
91
92 return $this->results;
93 }
94
95 /**
96 * Handles ajax requests
97 *
98 * @return void
99 */
100 public function handle_ajax_requests() {
101 $this->set_nonce();
102 $this->set_subaction();
103 $this->set_data();
104
105 if (!$this->is_valid_request()) {
106 $this->send_security_check_failed_error_response();
107 }
108
109 if (!WP_Optimize()->current_user_can()) {
110 $this->send_user_capability_error_response();
111 }
112
113 if (is_multisite() && !WP_Optimize()->current_user_can('manage_network_options')) {
114 if (!$this->is_valid_multisite_command()) {
115 $this->send_invalid_multisite_command_error_response();
116 }
117 }
118
119 if ($this->is_subaction_a_dismissed_notice()) {
120 $this->handle_notice_dismissals();
121 } else {
122 $this->set_commands();
123 if ($this->is_invalid_command()) {
124 $this->add_invalid_command_error_log_entry();
125 $this->set_invalid_command_error_response();
126 } else {
127 $this->execute_command();
128 $this->maybe_fix_status_box_content();
129 $this->set_error_response_on_wp_error();
130 $this->maybe_set_results_as_null();
131 }
132 }
133
134 $this->json_encode_results();
135
136 $json_last_error = json_last_error();
137 if ($json_last_error) {
138 $this->set_error_response_on_json_encode_error($json_last_error);
139 }
140
141 echo $this->results; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output already escaped
142 die;
143 }
144
145 /**
146 * Sets nonce property value
147 */
148 private function set_nonce() {
149 $this->nonce = empty($_POST['nonce']) ? '' : sanitize_key(wp_unslash($_POST['nonce'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- is_valid_request() checks nonce
150 }
151
152 /**
153 * Sets subaction property value
154 */
155 private function set_subaction() {
156 $this->subaction = empty($_POST['subaction']) ? '' : sanitize_key(wp_unslash($_POST['subaction'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- is_valid_request() checks nonce
157 }
158
159 /**
160 * Sets heartbeat subaction property value
161 *
162 * @param string $action_name The name of the heartbeat action to run
163 */
164 private function set_heartbeat_subaction($action_name) {
165 $this->subaction = $action_name;
166 }
167
168 /**
169 * Sets data property value
170 */
171 private function set_data() {
172 $this->data = isset($_POST['data']) ? stripslashes_deep($_POST['data']) : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- is_valid_request() checks nonce, sanitization takes place later
173 }
174
175 /**
176 * Checks whether the request is valid or not
177 *
178 * @return bool
179 */
180 private function is_valid_request() {
181 return wp_verify_nonce($this->nonce, 'wp-optimize-ajax-nonce') && !empty($this->subaction);
182 }
183
184 /**
185 * Send security check failed error response to browser and die
186 */
187 private function send_security_check_failed_error_response() {
188 wp_send_json(array(
189 'result' => false,
190 'error_code' => 'security_check',
191 'error_message' => __('The security check failed; try refreshing the page.', 'wp-optimize')
192 ));
193 }
194
195 /**
196 * Send user capability check failed error response to browser and possibly die
197 *
198 * @param boolean $send - if true, then the response is output; otherwise, it is returned
199 */
200 private function send_user_capability_error_response($send = true) {
201 $error = array(
202 'result' => false,
203 'error_code' => 'security_check',
204 'error_message' => __('You are not allowed to run this command.', 'wp-optimize')
205 );
206
207 if ($send) {
208 wp_send_json($error);
209 } else {
210 return $error;
211 }
212 }
213
214 /**
215 * Checks whether subaction is a valid multisite command
216 *
217 * @return bool
218 */
219 private function is_valid_multisite_command() {
220 /**
221 * Filters the commands allowed to the sub site admins. Other commands are only available to network admin. Only used in a multisite context.
222 */
223 $allowed_multisite_commands = apply_filters('wpo_multisite_allowed_commands', array('check_server_status', 'compress_single_image', 'restore_single_image'));
224 return in_array($this->subaction, $allowed_multisite_commands);
225 }
226
227 /**
228 * Send invalid multisite command error response to browser and die
229 */
230 private function send_invalid_multisite_command_error_response($send = true) {
231 $error = array(
232 'result' => false,
233 'error_code' => 'update_failed',
234 'error_message' => __('Options can only be saved by network admin', 'wp-optimize')
235 );
236
237 if ($send) {
238 wp_send_json($error);
239 } else {
240 return $error;
241 }
242 }
243
244 /**
245 * Checks if subaction is a notice dismissal or not
246 *
247 * @return bool True for notice dismiss actions, false otherwise
248 */
249 private function is_subaction_a_dismissed_notice() {
250 $dismiss_actions = $this->get_dismiss_actions();
251 return in_array($this->subaction, $dismiss_actions);
252 }
253
254 /**
255 * Returns an array of notice dismiss action names
256 *
257 * @return array An array of notice dismiss actions
258 */
259 private function get_dismiss_actions() {
260 return array(
261 'dismiss_dash_notice_until',
262 'dismiss_season',
263 'dismiss_page_notice_until',
264 'dismiss_notice',
265 'dismiss_review_notice',
266 );
267 }
268
269 /**
270 * Handles notice dismissals
271 */
272 private function handle_notice_dismissals() {
273 $options = WP_Optimize()->get_options();
274 // Some commands that are available via AJAX only.
275 if (in_array($this->subaction, array('dismiss_dash_notice_until', 'dismiss_season'))) {
276 $options->update_option($this->subaction, (time() + 366 * 86400));
277 } elseif (in_array($this->subaction, array('dismiss_page_notice_until', 'dismiss_notice'))) {
278 $options->update_option($this->subaction, (time() + 84 * 86400));
279 } elseif ('dismiss_review_notice' === $this->subaction) {
280 if (empty($this->data['dismiss_forever'])) {
281 $options->update_option($this->subaction, time() + 84 * 86400);
282 } else {
283 $options->update_option($this->subaction, 100 * (365.25 * 86400));
284 }
285 }
286 }
287
288 /**
289 * Sets commands property value
290 */
291 private function set_commands() {
292 $this->commands = apply_filters('wpo_premium_ajax_commands', new WP_Optimize_Commands());
293
294 $minify_commands = $this->get_minify_commands();
295 if ($this->is_subaction_a_minify_command($minify_commands)) {
296 $this->commands = $minify_commands;
297 }
298
299 $cache_commands = $this->get_cache_commands();
300 if ($this->is_subaction_a_cache_command($cache_commands)) {
301 $this->commands = $cache_commands;
302 }
303 }
304
305 /**
306 * Gets minify commands
307 *
308 * @return WP_Optimize_Minify_Commands
309 */
310 private function get_minify_commands() {
311 return new WP_Optimize_Minify_Commands();
312 }
313
314 /**
315 * Gets cache commands
316 *
317 * @return WP_Optimize_Cache_Commands|WP_Optimize_Cache_Commands_Premium
318 */
319 private function get_cache_commands() {
320 if (WP_Optimize::is_premium()) {
321 $cache_commands = new WP_Optimize_Cache_Commands_Premium();
322 } else {
323 $cache_commands = new WP_Optimize_Cache_Commands();
324 }
325 return $cache_commands;
326 }
327
328 /**
329 * Checks if applied ajax command is a minify command or not
330 *
331 * @param WP_Optimize_Minify_Commands $minify_commands an instance of minify commands class
332 *
333 * @return bool Returns true if ajax command is a minify command, false otherwise
334 */
335 private function is_subaction_a_minify_command($minify_commands) {
336 return !is_callable(array($this->commands, $this->subaction)) && is_callable(array($minify_commands, $this->subaction));
337 }
338
339 /**
340 * Checks if applied ajax command is a cache command or not
341 *
342 * @param WP_Optimize_Cache_Commands|WP_Optimize_Cache_Commands_Premium $cache_commands an instance of cache commands
343 *
344 * @return bool Returns true if ajax command is a cache command, false otherwise
345 */
346 private function is_subaction_a_cache_command($cache_commands) {
347 return !is_callable(array($this->commands, $this->subaction)) && is_callable(array($cache_commands, $this->subaction));
348 }
349
350 /**
351 * Checks if applied ajax command is an invalid command or not
352 *
353 * @return bool Returns true if ajax command is an invalid command, false otherwise
354 */
355 private function is_invalid_command() {
356 return !is_callable(array($this->commands, $this->subaction));
357 }
358
359 /**
360 * Log an error message for invalid ajax command
361 */
362 private function add_invalid_command_error_log_entry() {
363 error_log("WP-Optimize: ajax_handler: no such command (" . $this->subaction . ")"); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Edge case, used for debugging
364 }
365
366 /**
367 * Set `results` property with error response array for invalid ajax command
368 *
369 * @return void
370 */
371 private function set_invalid_command_error_response() {
372 $this->results = array(
373 'result' => false,
374 'error_code' => 'command_not_found',
375 // translators: %s is an ajax command name
376 'error_message' => sprintf(__('The command "%s" was not found', 'wp-optimize'), $this->subaction)
377 );
378 }
379
380 /**
381 * Execute the ajax command
382 */
383 private function execute_command() {
384 $this->results = call_user_func(array($this->commands, $this->subaction), $this->data);
385 }
386
387 /**
388 * If status box content is present, fix it.
389 */
390 private function maybe_fix_status_box_content() {
391 // clean status box content, it broke json sometimes.
392 // Git commit wp-optimize/-/commit/c05686b39959b863f4e168af3fa54421c4870470
393 if (isset($this->results['status_box_contents'])) {
394 $this->results['status_box_contents'] = str_replace(array("\n", "\t"), '', $this->results['status_box_contents']);
395 }
396 }
397
398 /**
399 * Set `results` property with error message
400 */
401 private function set_error_response_on_wp_error() {
402 if (is_wp_error($this->results)) {
403 $this->results = array(
404 'result' => false,
405 'error_code' => $this->results->get_error_code(),
406 'error_message' => $this->results->get_error_message(),
407 'error_data' => $this->results->get_error_data(),
408 );
409 }
410 }
411
412 /**
413 * Set `results` property to null, if it is not yet set
414 */
415 private function maybe_set_results_as_null() {
416 // if nothing was returned for some reason, set as result null.
417 if (empty($this->results)) {
418 $this->results = array(
419 'result' => null
420 );
421 }
422 }
423
424 /**
425 * Sets `results` property with json encode error
426 *
427 * @param int $json_last_error
428 *
429 * @return void
430 */
431 private function set_error_response_on_json_encode_error($json_last_error) {
432 $this->results = array(
433 'result' => false,
434 'error_code' => $json_last_error,
435 'error_message' => 'json_encode error : ' . $json_last_error,
436 'error_data' => '',
437 );
438
439 $this->results = wp_json_encode($this->results);
440 }
441
442 /**
443 * Json encode the `results` property value
444 */
445 private function json_encode_results() {
446 $this->results = wp_json_encode($this->results);
447 }
448 }
449
450 endif;
451