PluginProbe
Docket Cache – Object Cache Accelerator / 24.07.06
Docket Cache – Object Cache Accelerator v24.07.06
26.04.05 trunk 22.07.01 22.07.02 22.07.03 22.07.04 22.07.05 23.08.01 23.08.02 24.07.01 24.07.02 24.07.03 24.07.04 24.07.05 24.07.06 24.07.07 26.04.03 26.04.04
docket-cache / includes / src / CronAgent.php

CronAgent.php in Docket Cache – Object Cache Accelerator 24.07.06, at includes/src/CronAgent.php

572 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Docket Cache.
4 *
5 * @author Nawawi Jamili
6 * @license MIT
7 *
8 * @see https://github.com/nawawi/docket-cache
9 */
10
11 namespace Nawawi\DocketCache;
12
13 \defined('ABSPATH') || exit;
14
15 final class CronAgent
16 {
17 private $is_pingpong;
18 private $pt;
19
20 public function __construct(Plugin $pt)
21 {
22 $this->pt = $pt;
23 $this->is_pingpong = false;
24
25 // warn issue: a non-numeric value encountered
26 !\defined('WP_CRON_LOCK_TIMEOUT') && \define('WP_CRON_LOCK_TIMEOUT', MINUTE_IN_SECONDS);
27 }
28
29 public function register()
30 {
31 add_action(
32 'wp',
33 function () {
34 $this->receive_ping();
35 },
36 \PHP_INT_MIN
37 );
38
39 add_action(
40 'shutdown',
41 function () {
42 $this->check_connection();
43 },
44 \PHP_INT_MAX
45 );
46
47 add_filter(
48 'docketcache/filter/active/cronbot',
49 function ($status) {
50 $status = $status ? 'on' : 'off';
51
52 return $this->send_action($status);
53 },
54 \PHP_INT_MAX
55 );
56
57 add_filter(
58 'docketcache/filter/check/cronbot',
59 function () {
60 return $this->send_action('on', 'pong');
61 },
62 \PHP_INT_MAX
63 );
64
65 add_filter(
66 'docketcache/filter/runevent/cronbot',
67 function ($runnow) {
68 $is_switch = $this->pt->switch_cron_site();
69
70 $results = $this->run_wpcron($runnow);
71
72 if (!empty($results) && \is_array($results) && !empty($results['wpcron_return'])) {
73 @Crawler::fetch_admin(admin_url('/'));
74 } else {
75 $results = false;
76 }
77
78 if ($is_switch) {
79 restore_current_blog();
80 }
81
82 return $results;
83 },
84 \PHP_INT_MAX
85 );
86 }
87
88 private function is_ping_request()
89 {
90 return !empty($_POST['ping']) && !empty($_GET['docketcache_ping']) && !empty($_SERVER['REQUEST_URI']) && false !== strpos($_SERVER['REQUEST_URI'], '/?docketcache_ping=');
91 }
92
93 private function maybe_disable_wp_cron()
94 {
95 // signal wp do not run wp-cron
96 $this->pt->cf()->maybe_define('DISABLE_WP_CRON', true);
97 }
98
99 private function send_action($action, $is_hello = false)
100 {
101 $is_quick = $is_hello && 'pong' !== $is_hello ? true : false;
102 $is_pong = 'pong' === $is_hello;
103
104 $uip = $this->pt->get_user_ip();
105
106 static $stmp = 0;
107 static $cache = [];
108
109 if (isset($cache[$uip])) {
110 return $cache[$uip];
111 }
112
113 $site_url = $this->pt->site_url();
114 $site_key = substr(md5($site_url), 0, 22);
115 $site_body = $this->pt->nw_encrypt($site_url, $site_key);
116 $site_id = $this->pt->nw_encrypt($site_key, $site_body);
117
118 $args = [
119 'blocking' => $is_quick ? false : true,
120 'body' => [
121 'timestamp' => date('Y-m-d H:i:s T'),
122 'timezone' => wp_timezone_string(),
123 'site' => $this->pt->base64_encode_url($site_body),
124 'meta' => $this->pt->site_meta(),
125 'status' => $action,
126 ],
127 'headers' => [
128 'REFERER' => $this->pt->site_url(true, true),
129 'DOCKETID' => $site_id,
130 'DOCKETOB' => $this->pt->cx()->validate() ? 'on' : 'off',
131 ],
132 ];
133
134 if (0 === $stmp) {
135 $stmp = time() + 120;
136 }
137
138 $cronbot_endpoint = $this->pt->cronbot_endpoint.'/checkstatus?v='.$stmp;
139 $results = Crawler::post($cronbot_endpoint, $args);
140
141 if ($is_quick) {
142 return true;
143 }
144
145 $output = [
146 'timestamp' => time(),
147 'endpoint' => $cronbot_endpoint,
148 'connected' => false,
149 'last_status' => $action,
150 'request' => [
151 'headers' => $args['headers'],
152 'content' => $args['body'],
153 ],
154 ];
155
156 if (is_wp_error($results)) {
157 $output['error'] = $results->get_error_message();
158
159 $this->pt->co()->save_part($output, 'cronbot');
160 $this->pt->co()->lookup_set('cronboterror', $output['error']);
161
162 $cache[$uip] = false;
163
164 return false;
165 }
166
167 $output['response'] = wp_remote_retrieve_body($results);
168 if (!empty($output['response'])) {
169 $output['response'] = json_decode($output['response'], true);
170 if (\JSON_ERROR_NONE === json_last_error()) {
171 if (!empty($output['response']['error'])) {
172 $output['error'] = $output['response']['error'];
173
174 $this->pt->co()->save_part($output, 'cronbot');
175 $this->pt->co()->lookup_set('cronboterror', $output['error']);
176
177 $cache[$uip] = false;
178
179 return false;
180 }
181 }
182 }
183
184 $code = (int) wp_remote_retrieve_response_code($results);
185 if ($code > 400) {
186 $output['error'] = $code;
187
188 if (!$is_pong) {
189 $this->pt->co()->save_part($output, 'cronbot');
190 }
191
192 $this->pt->co()->lookup_set('cronboterror', 'Error '.$output['error']);
193
194 $cache[$uip] = false;
195
196 return false;
197 }
198
199 $output['connected'] = 'off' === $action ? false : true;
200
201 if (!$is_pong) {
202 $this->pt->co()->save_part($output, 'cronbot');
203 }
204
205 $cache[$uip] = true;
206
207 return true;
208 }
209
210 private function close_ping($response)
211 {
212 $output = $response;
213 $output['request'] = array_filter(
214 $_SERVER,
215 function ($arr) {
216 if ('HTTP_' === substr($arr, 0, 5)) {
217 return true;
218 }
219 },
220 \ARRAY_FILTER_USE_KEY
221 );
222
223 $output['selfcheck'] = time() + 5400; // 90min
224 $this->pt->co()->save_part($output, 'pings');
225
226 $this->pt->json_header();
227 $this->pt->close_exit(json_encode($response, \JSON_UNESCAPED_SLASHES));
228 }
229
230 private function run_wpcron($run_now = false)
231 {
232 $this->pt->cf()->maybe_define('DOING_RUN_WPCRON', true);
233
234 $run_uno = false;
235 $uno_ehk = '';
236 $uno_eky = '';
237 if (!empty($run_now) && \is_array($run_now)) {
238 if (empty($run_now['ehk']) || empty($run_now['eky'])) {
239 $results['wpcron_return'] = 0;
240 $results['wpcron_msg'] = esc_html__('Invalid request for single event', 'docket-cache');
241
242 return $results;
243 }
244
245 $uno_ehk = sanitize_text_field($run_now['ehk']);
246 $uno_eky = sanitize_text_field($run_now['eky']);
247
248 if (!has_action($uno_ehk)) {
249 $results['wpcron_return'] = 1;
250
251 /* translators: %s: Event Hook. */
252 $results['wpcron_msg'] = sprintf(esc_html__('Event hook not found %s', 'docket-cache'), $uno_ehk);
253
254 return $results;
255 }
256
257 $run_now = true;
258 $run_uno = true;
259 }
260
261 $crons = $this->pt->get_crons($run_now, $cron_event);
262
263 $results = [
264 'wpcron_return' => 0,
265 'wpcron_msg' => '',
266 'wpcron_crons' => $cron_event,
267 'wpcron_event' => 0,
268 ];
269
270 if (empty($crons)) {
271 $results['wpcron_return'] = 1;
272 $results['wpcron_msg'] = esc_html__('No scheduled event ready to run', 'docket-cache');
273
274 return $results;
275 }
276
277 if (false !== strpos($_SERVER['REQUEST_URI'], '/wp-cron.php') || isset($_GET['doing_wp_cron']) || wp_doing_cron()) {
278 $results['wpcron_return'] = 1;
279 $results['wpcron_msg'] = esc_html__('Another cron process is currently running wp-cron.php', 'docket-cache');
280
281 return $results;
282 }
283
284 $gmt_time = microtime(true);
285
286 // overwrite cron lock
287 $doing_wp_cron = sprintf('%.22F', microtime(true));
288 set_transient('doing_cron', $doing_wp_cron, 86400);
289
290 $run_event = 0;
291 $slowdown = 0;
292 $delay = $this->is_pingpong ? 850 : 200;
293
294 $max_execution_time = $this->pt->get_max_execution_time();
295
296 foreach ($crons as $timestamp => $cronhooks) {
297 if ($max_execution_time > 0 && (microtime(true) - WP_START_TIMESTAMP) > $max_execution_time) {
298 break;
299 }
300
301 if (false === $run_now && ($timestamp > $gmt_time)) {
302 continue;
303 }
304
305 if ($slowdown > 10) {
306 $slowdown = 0;
307 usleep($delay);
308 }
309
310 ++$slowdown;
311
312 foreach ($cronhooks as $hook => $keys) {
313 if (!has_action($hook)) {
314 // wp_clear_scheduled_hook($hook);
315 continue;
316 }
317
318 // single
319 if ($run_uno && $hook !== $uno_ehk) {
320 continue;
321 }
322
323 foreach ($keys as $k => $v) {
324 // single
325 if ($run_uno && $k !== $uno_eky) {
326 continue;
327 }
328
329 $schedule = $v['schedule'];
330
331 if ($schedule) {
332 if (false === wp_reschedule_event($timestamp, $schedule, $hook, $v['args'])) {
333 continue;
334 }
335 }
336
337 if (false === wp_unschedule_event($timestamp, $hook, $v['args'])) {
338 continue;
339 }
340
341 $hcontent = '';
342 try {
343 ob_start();
344 do_action_ref_array($hook, $v['args']);
345 $hcontent = trim(ob_get_contents());
346 ob_end_clean();
347 ++$run_event;
348 } catch (\Throwable $e) {
349 $results['wpcron_error'][$hook] = $e->getMessage();
350 // wp_clear_scheduled_hook($hook);
351
352 if ($run_uno) {
353 $results['wpcron_return'] = 0;
354 $results['wpcron_event'] = 1;
355 $results['wpcron_uno'] = $uno_ehk;
356 break;
357 }
358 --$run_event;
359 }
360
361 if ('' !== $hcontent) {
362 $results['wpcron_output'][$hook] = $hcontent;
363 }
364
365 usleep(100);
366 }
367 }
368 }
369
370 unset($crons, $cronhooks, $hook, $keys);
371
372 // lock must below 10 minutes
373 // wp-includes/cron.php -> spawn_cron()
374 // wp-cron.php
375 $lock_wp_cron = microtime(true) + 300;
376 set_transient('doing_cron', $lock_wp_cron, 86400);
377
378 $results['wpcron_return'] = 1;
379 $results['wpcron_event'] = $run_event;
380
381 if ($run_uno && 1 === $run_event) {
382 $results['wpcron_uno'] = $uno_ehk;
383 }
384
385 return $results;
386 }
387
388 private function receive_ping()
389 {
390 if (headers_sent() || !$this->is_ping_request()) {
391 return;
392 }
393
394 if ($_POST['ping'] !== md5($_GET['docketcache_ping'])) {
395 $this->close_ping('Invalid ping');
396
397 return;
398 }
399
400 // signal wp do not run wp-cron
401 $this->maybe_disable_wp_cron();
402
403 $site_url = $this->pt->site_url();
404
405 if (!empty($_POST['token'])) {
406 $verify = $this->pt->nw_decrypt($_POST['token'], $_POST['ping']);
407 if ($verify !== $site_url) {
408 $this->close_ping('Invalid token');
409
410 return;
411 }
412 }
413
414 $uagent = $this->pt->get_user_agent();
415 if (false === strpos($uagent, 'docket-cache/') || !@preg_match('@compatible;\s+cronbot/[0-9\.]+;\s+docket\-cache/[0-9\.]+;\s+@', $uagent)) {
416 $this->close_ping('Invalid version');
417
418 return;
419 }
420
421 $uip = $this->pt->get_user_ip();
422
423 static $cache = [];
424 if (!empty($cache[$uip])) {
425 return $cache[$uip];
426 }
427
428 $response = [
429 'timestamp' => date('Y-m-d H:i:s T'),
430 'timezone' => wp_timezone_string(),
431 'site' => $this->pt->site_url(),
432 'meta' => $this->pt->site_meta(),
433 'status' => 1,
434 ];
435
436 if ($this->pt->cf()->is_dcfalse('CRONBOT')) {
437 $response['status'] = 0;
438 $cache[$uip] = $response;
439 $this->close_ping($response);
440 }
441
442 if ($this->pt->co()->lockproc('receive_ping', time() + 60)) {
443 $response['msg'] = esc_html__('Already received. Try again in a few minutes', 'docket-cache');
444 $this->close_ping($response);
445
446 return false;
447 }
448
449 $this->is_pingpong = true;
450 $is_multisite = is_multisite();
451 $siteall = 0;
452 $sites = $this->pt->get_network_sites($siteall);
453 $maxrun = 0;
454 $maxcan = (int) $this->pt->cf()->dcvalue('CRONBOT_MAX');
455 $maxcan = $maxcan < 1 ? 5 : $maxcan;
456 $halt = false;
457
458 // finish it if user connection closed
459 if (\function_exists('ignore_user_abort')) {
460 ignore_user_abort(true);
461 }
462
463 foreach ($sites as $num => $site) {
464 if ($halt) {
465 break;
466 }
467
468 if ($is_multisite) {
469 switch_to_blog($site['id']);
470 }
471
472 if ($maxrun >= $maxcan) {
473 $results = [
474 'wpcron_return' => 3,
475 'wpcron_msg' => 'Reach maximum run: '.$maxrun.'/'.$siteall,
476 'wpcron_crons' => 0,
477 'wpcron_event' => 0,
478 ];
479 $halt = true;
480 } else {
481 $results = $this->run_wpcron();
482 }
483
484 if ($is_multisite) {
485 restore_current_blog();
486 }
487
488 if ($site['is_main']) {
489 $response = array_merge($response, $results);
490 --$maxrun;
491 } else {
492 $response['site_'.$site['id']] = [
493 'site' => $site['url'],
494 'wpcron' => $results,
495 ];
496 }
497 ++$maxrun;
498
499 usleep(100);
500 }
501
502 unset($sites);
503
504 $cache[$uip] = $response;
505
506 $this->is_pingpong = false;
507 $this->close_ping($cache[$uip]);
508 }
509
510 private function check_connection()
511 {
512 static $done = false;
513
514 if ($done) {
515 return;
516 }
517
518 if (\function_exists('wp_is_maintenance_mode') && wp_is_maintenance_mode()) {
519 return;
520 }
521
522 // only main site
523 if (!is_main_site() || $this->pt->cf()->is_true('WP_IMPORTING') || $this->pt->is_request_from_theme_editor() || $this->is_ping_request()) {
524 return;
525 }
526
527 if ($this->pt->co()->lockexp('check_connection')) {
528 return;
529 }
530
531 $crondata = $this->pt->co()->get_part('cronbot', true);
532 if (empty($crondata)) {
533 return;
534 }
535
536 if (\is_array($crondata) && !empty($crondata['connected'])) {
537 $pingdata = $this->pt->co()->get_part('pings', true);
538 if (empty($pingdata)) {
539 return;
540 }
541 if (\is_array($pingdata) && !empty($pingdata['selfcheck'])) {
542 $selfcheck = $pingdata['selfcheck'];
543 if (0 === $this->pt->sanitize_timestamp($selfcheck)) {
544 return;
545 }
546
547 $locktime = time() + 300; // 5min
548 if (($selfcheck > 0 && time() > $selfcheck) && $this->pt->co()->setlock('check_connection', $locktime)) {
549 // signal wp do not run wp-cron
550 $this->maybe_disable_wp_cron();
551
552 // go background if possible
553 $this->pt->fastcgi_close();
554 $this->send_action('on', true);
555
556 $pingdata['selfcheck'] = time() + 5400; // 90min
557 $this->pt->co()->save_part($pingdata, 'pings');
558
559 if (!empty($crondata) && !$this->pt->cx()->validate()) {
560 $crondata['connected'] = false;
561 $this->pt->co()->save_part($crondata, 'cronbot');
562 }
563
564 $done = true;
565 }
566 }
567 }
568
569 unset($crondata);
570 }
571 }
572