PluginProbe
Docket Cache – Object Cache Accelerator / 26.04.03
Docket Cache – Object Cache Accelerator v26.04.03
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 26.04.03, at includes/src/CronAgent.php

577 lines 16.6 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
140 if (!@is_file(__DIR__.'/Crawler.php') || !class_exists('Nawawi\\DocketCache\\Crawler')) {
141 return false;
142 }
143
144 $results = Crawler::post($cronbot_endpoint, $args);
145
146 if ($is_quick) {
147 return true;
148 }
149
150 $output = [
151 'timestamp' => time(),
152 'endpoint' => $cronbot_endpoint,
153 'connected' => false,
154 'last_status' => $action,
155 'request' => [
156 'headers' => $args['headers'],
157 'content' => $args['body'],
158 ],
159 ];
160
161 if (is_wp_error($results)) {
162 $output['error'] = $results->get_error_message();
163
164 $this->pt->co()->save_part($output, 'cronbot');
165 $this->pt->co()->lookup_set('cronboterror', $output['error']);
166
167 $cache[$uip] = false;
168
169 return false;
170 }
171
172 $output['response'] = wp_remote_retrieve_body($results);
173 if (!empty($output['response'])) {
174 $output['response'] = json_decode($output['response'], true);
175 if (\JSON_ERROR_NONE === json_last_error()) {
176 if (!empty($output['response']['error'])) {
177 $output['error'] = $output['response']['error'];
178
179 $this->pt->co()->save_part($output, 'cronbot');
180 $this->pt->co()->lookup_set('cronboterror', $output['error']);
181
182 $cache[$uip] = false;
183
184 return false;
185 }
186 }
187 }
188
189 $code = (int) wp_remote_retrieve_response_code($results);
190 if ($code > 400) {
191 $output['error'] = $code;
192
193 if (!$is_pong) {
194 $this->pt->co()->save_part($output, 'cronbot');
195 }
196
197 $this->pt->co()->lookup_set('cronboterror', 'Error '.$output['error']);
198
199 $cache[$uip] = false;
200
201 return false;
202 }
203
204 $output['connected'] = 'off' === $action ? false : true;
205
206 if (!$is_pong) {
207 $this->pt->co()->save_part($output, 'cronbot');
208 }
209
210 $cache[$uip] = true;
211
212 return true;
213 }
214
215 private function close_ping($response)
216 {
217 $output = $response;
218 $output['request'] = array_filter(
219 $_SERVER,
220 function ($arr) {
221 if ('HTTP_' === substr($arr, 0, 5)) {
222 return true;
223 }
224 },
225 \ARRAY_FILTER_USE_KEY
226 );
227
228 $output['selfcheck'] = time() + 5400; // 90min
229 $this->pt->co()->save_part($output, 'pings');
230
231 $this->pt->json_header();
232 $this->pt->close_exit(json_encode($response, \JSON_UNESCAPED_SLASHES));
233 }
234
235 private function run_wpcron($run_now = false)
236 {
237 $this->pt->cf()->maybe_define('DOING_RUN_WPCRON', true);
238
239 $run_uno = false;
240 $uno_ehk = '';
241 $uno_eky = '';
242 if (!empty($run_now) && \is_array($run_now)) {
243 if (empty($run_now['ehk']) || empty($run_now['eky'])) {
244 $results['wpcron_return'] = 0;
245 $results['wpcron_msg'] = esc_html__('Invalid request for single event', 'docket-cache');
246
247 return $results;
248 }
249
250 $uno_ehk = sanitize_text_field($run_now['ehk']);
251 $uno_eky = sanitize_text_field($run_now['eky']);
252
253 if (!has_action($uno_ehk)) {
254 $results['wpcron_return'] = 1;
255
256 /* translators: %s: Event Hook. */
257 $results['wpcron_msg'] = sprintf(esc_html__('Event hook not found %s', 'docket-cache'), $uno_ehk);
258
259 return $results;
260 }
261
262 $run_now = true;
263 $run_uno = true;
264 }
265
266 $crons = $this->pt->get_crons($run_now, $cron_event);
267
268 $results = [
269 'wpcron_return' => 0,
270 'wpcron_msg' => '',
271 'wpcron_crons' => $cron_event,
272 'wpcron_event' => 0,
273 ];
274
275 if (empty($crons)) {
276 $results['wpcron_return'] = 1;
277 $results['wpcron_msg'] = esc_html__('No scheduled event ready to run', 'docket-cache');
278
279 return $results;
280 }
281
282 if (false !== strpos($_SERVER['REQUEST_URI'], '/wp-cron.php') || isset($_GET['doing_wp_cron']) || wp_doing_cron()) {
283 $results['wpcron_return'] = 1;
284 $results['wpcron_msg'] = esc_html__('Another cron process is currently running wp-cron.php', 'docket-cache');
285
286 return $results;
287 }
288
289 $gmt_time = microtime(true);
290
291 // overwrite cron lock
292 $doing_wp_cron = sprintf('%.22F', microtime(true));
293 set_transient('doing_cron', $doing_wp_cron, 86400);
294
295 $run_event = 0;
296 $slowdown = 0;
297 $delay = $this->is_pingpong ? 850 : 200;
298
299 $max_execution_time = $this->pt->get_max_execution_time();
300
301 foreach ($crons as $timestamp => $cronhooks) {
302 if ($max_execution_time > 0 && (microtime(true) - WP_START_TIMESTAMP) > $max_execution_time) {
303 break;
304 }
305
306 if (false === $run_now && ($timestamp > $gmt_time)) {
307 continue;
308 }
309
310 if ($slowdown > 10) {
311 $slowdown = 0;
312 usleep($delay);
313 }
314
315 ++$slowdown;
316
317 foreach ($cronhooks as $hook => $keys) {
318 if (!has_action($hook)) {
319 // wp_clear_scheduled_hook($hook);
320 continue;
321 }
322
323 // single
324 if ($run_uno && $hook !== $uno_ehk) {
325 continue;
326 }
327
328 foreach ($keys as $k => $v) {
329 // single
330 if ($run_uno && $k !== $uno_eky) {
331 continue;
332 }
333
334 $schedule = $v['schedule'];
335
336 if ($schedule) {
337 if (false === wp_reschedule_event($timestamp, $schedule, $hook, $v['args'])) {
338 continue;
339 }
340 }
341
342 if (false === wp_unschedule_event($timestamp, $hook, $v['args'])) {
343 continue;
344 }
345
346 $hcontent = '';
347 try {
348 ob_start();
349 do_action_ref_array($hook, $v['args']);
350 $hcontent = trim(ob_get_contents());
351 ob_end_clean();
352 ++$run_event;
353 } catch (\Throwable $e) {
354 $results['wpcron_error'][$hook] = $e->getMessage();
355 // wp_clear_scheduled_hook($hook);
356
357 if ($run_uno) {
358 $results['wpcron_return'] = 0;
359 $results['wpcron_event'] = 1;
360 $results['wpcron_uno'] = $uno_ehk;
361 break;
362 }
363 --$run_event;
364 }
365
366 if ('' !== $hcontent) {
367 $results['wpcron_output'][$hook] = $hcontent;
368 }
369
370 usleep(100);
371 }
372 }
373 }
374
375 unset($crons, $cronhooks, $hook, $keys);
376
377 // lock must below 10 minutes
378 // wp-includes/cron.php -> spawn_cron()
379 // wp-cron.php
380 $lock_wp_cron = microtime(true) + 300;
381 set_transient('doing_cron', $lock_wp_cron, 86400);
382
383 $results['wpcron_return'] = 1;
384 $results['wpcron_event'] = $run_event;
385
386 if ($run_uno && 1 === $run_event) {
387 $results['wpcron_uno'] = $uno_ehk;
388 }
389
390 return $results;
391 }
392
393 private function receive_ping()
394 {
395 if (headers_sent() || !$this->is_ping_request()) {
396 return;
397 }
398
399 if ($_POST['ping'] !== md5($_GET['docketcache_ping'])) {
400 $this->close_ping('Invalid ping');
401
402 return;
403 }
404
405 // signal wp do not run wp-cron
406 $this->maybe_disable_wp_cron();
407
408 $site_url = $this->pt->site_url();
409
410 if (!empty($_POST['token'])) {
411 $verify = $this->pt->nw_decrypt($_POST['token'], $_POST['ping']);
412 if ($verify !== $site_url) {
413 $this->close_ping('Invalid token');
414
415 return;
416 }
417 }
418
419 $uagent = $this->pt->get_user_agent();
420 if (false === strpos($uagent, 'docket-cache/') || !@preg_match('@compatible;\s+cronbot/[0-9\.]+;\s+docket\-cache/[0-9\.]+;\s+@', $uagent)) {
421 $this->close_ping('Invalid version');
422
423 return;
424 }
425
426 $uip = $this->pt->get_user_ip();
427
428 static $cache = [];
429 if (!empty($cache[$uip])) {
430 return $cache[$uip];
431 }
432
433 $response = [
434 'timestamp' => date('Y-m-d H:i:s T'),
435 'timezone' => wp_timezone_string(),
436 'site' => $this->pt->site_url(),
437 'meta' => $this->pt->site_meta(),
438 'status' => 1,
439 ];
440
441 if ($this->pt->cf()->is_dcfalse('CRONBOT')) {
442 $response['status'] = 0;
443 $cache[$uip] = $response;
444 $this->close_ping($response);
445 }
446
447 if ($this->pt->co()->lockproc('receive_ping', time() + 60)) {
448 $response['msg'] = esc_html__('Already received. Try again in a few minutes', 'docket-cache');
449 $this->close_ping($response);
450
451 return false;
452 }
453
454 $this->is_pingpong = true;
455 $is_multisite = is_multisite();
456 $siteall = 0;
457 $sites = $this->pt->get_network_sites($siteall);
458 $maxrun = 0;
459 $maxcan = (int) $this->pt->cf()->dcvalue('CRONBOT_MAX');
460 $maxcan = $maxcan < 1 ? 5 : $maxcan;
461 $halt = false;
462
463 // finish it if user connection closed
464 if (\function_exists('ignore_user_abort')) {
465 ignore_user_abort(true);
466 }
467
468 foreach ($sites as $num => $site) {
469 if ($halt) {
470 break;
471 }
472
473 if ($is_multisite) {
474 switch_to_blog($site['id']);
475 }
476
477 if ($maxrun >= $maxcan) {
478 $results = [
479 'wpcron_return' => 3,
480 'wpcron_msg' => 'Reach maximum run: '.$maxrun.'/'.$siteall,
481 'wpcron_crons' => 0,
482 'wpcron_event' => 0,
483 ];
484 $halt = true;
485 } else {
486 $results = $this->run_wpcron();
487 }
488
489 if ($is_multisite) {
490 restore_current_blog();
491 }
492
493 if ($site['is_main']) {
494 $response = array_merge($response, $results);
495 --$maxrun;
496 } else {
497 $response['site_'.$site['id']] = [
498 'site' => $site['url'],
499 'wpcron' => $results,
500 ];
501 }
502 ++$maxrun;
503
504 usleep(100);
505 }
506
507 unset($sites);
508
509 $cache[$uip] = $response;
510
511 $this->is_pingpong = false;
512 $this->close_ping($cache[$uip]);
513 }
514
515 private function check_connection()
516 {
517 static $done = false;
518
519 if ($done) {
520 return;
521 }
522
523 if (\function_exists('wp_is_maintenance_mode') && wp_is_maintenance_mode()) {
524 return;
525 }
526
527 // only main site
528 if (!is_main_site() || $this->pt->cf()->is_true('WP_IMPORTING') || $this->pt->is_request_from_theme_editor() || $this->is_ping_request()) {
529 return;
530 }
531
532 if ($this->pt->co()->lockexp('check_connection')) {
533 return;
534 }
535
536 $crondata = $this->pt->co()->get_part('cronbot', true);
537 if (empty($crondata)) {
538 return;
539 }
540
541 if (\is_array($crondata) && !empty($crondata['connected'])) {
542 $pingdata = $this->pt->co()->get_part('pings', true);
543 if (empty($pingdata)) {
544 return;
545 }
546 if (\is_array($pingdata) && !empty($pingdata['selfcheck'])) {
547 $selfcheck = $pingdata['selfcheck'];
548 if (0 === $this->pt->sanitize_timestamp($selfcheck)) {
549 return;
550 }
551
552 $locktime = time() + 300; // 5min
553 if (($selfcheck > 0 && time() > $selfcheck) && $this->pt->co()->setlock('check_connection', $locktime)) {
554 // signal wp do not run wp-cron
555 $this->maybe_disable_wp_cron();
556
557 // go background if possible
558 $this->pt->fastcgi_close();
559 $this->send_action('on', true);
560
561 $pingdata['selfcheck'] = time() + 5400; // 90min
562 $this->pt->co()->save_part($pingdata, 'pings');
563
564 if (!empty($crondata) && !$this->pt->cx()->validate()) {
565 $crondata['connected'] = false;
566 $this->pt->co()->save_part($crondata, 'cronbot');
567 }
568
569 $done = true;
570 }
571 }
572 }
573
574 unset($crondata);
575 }
576 }
577