PluginProbe
Docket Cache – Object Cache Accelerator / 22.07.02
Docket Cache – Object Cache Accelerator v22.07.02
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 / Filesystem.php

Filesystem.php in Docket Cache – Object Cache Accelerator 22.07.02, at includes/src/Filesystem.php

1,600 lines 42.1 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 use Nawawi\DocketCache\Exporter\VarExporter;
16
17 class Filesystem
18 {
19 /**
20 * is_request_from_theme_editor.
21 */
22 public function is_request_from_theme_editor()
23 {
24 if (!empty($_POST)) {
25 if (!empty($_POST['_wp_http_referer'])) {
26 $wp_referer = $_POST['_wp_http_referer'];
27 if ((false !== strpos($wp_referer, '/theme-editor.php?file=') || false !== strpos($wp_referer, '/plugin-editor.php?file=')) && (!empty($_POST['newcontent']) && false !== strpos($_POST['newcontent'], '<?php'))) {
28 return true;
29 }
30 }
31
32 if (!empty($_POST['action']) && 'heartbeat' === $_POST['action'] && !empty($_POST['screen_id']) && ('theme-editor' === $_POST['screen_id'] || 'plugin-editor' === $_POST['screen_id'])) {
33 return true;
34 }
35 }
36
37 if (!empty($_GET) && !empty($_GET['wp_scrape_key']) && !empty($_GET['wp_scrape_nonce'])) {
38 return true;
39 }
40
41 return false;
42 }
43
44 /**
45 * fastcgi_close.
46 */
47 public function fastcgi_close()
48 {
49 if (\function_exists('fastcgi_finish_request') && !$this->is_request_from_theme_editor()) {
50 return @fastcgi_finish_request();
51 }
52
53 return false;
54 }
55
56 /**
57 * close_buffer.
58 */
59 public function close_buffer()
60 {
61 if (!@ob_get_level() && $this->fastcgi_close()) {
62 return true;
63 }
64
65 return false;
66 }
67
68 /**
69 * is_docketcachedir.
70 */
71 public function is_docketcachedir($path)
72 {
73 static $cached = [];
74
75 if (!empty($cached[$path])) {
76 return true;
77 }
78
79 $name = 'docket-cache';
80 $ok = false;
81
82 $dir = nwdcx_normalizepath($path);
83
84 if (false === strpos($dir.'/', '/'.$name.'/')) {
85 return $ok;
86 }
87
88 $dir = array_reverse(explode('/', trim($dir, '/')));
89
90 // 28042022: new cache directory.
91 // depth = 3: cache/docket-cache/93/b2/8a/
92 // depth = 4: cache/docket-cache/network-1/93/b2/8a/
93 $maxdepth = 4;
94 foreach ($dir as $n => $c) {
95 if ($n <= $maxdepth && 0 === strcmp($name, $c)) {
96 $ok = true;
97 $cached[$path] = 1;
98 break;
99 }
100 }
101
102 return $ok;
103 }
104
105 /**
106 * is_docketcachefile.
107 */
108 public function is_docketcachefile($file)
109 {
110 static $cached = [];
111
112 if (!empty($cached[$file])) {
113 return true;
114 }
115
116 if (false !== strpos($file, '/docket-cache/') && @preg_match('@^([a-z0-9]{12})\-([a-z0-9]{12})\.php$@', basename($file))) {
117 $cached[$file] = 1;
118
119 return true;
120 }
121
122 return false;
123 }
124
125 /**
126 * is_docketcachegroup.
127 */
128 public function is_docketcachegroup($group)
129 {
130 return 'docketcache' === substr($group, 0, 11);
131 }
132
133 /**
134 * is_transient.
135 */
136 public function is_transient($group)
137 {
138 return 'transient' === $group || 'site-transient' === $group;
139 }
140
141 /**
142 * is_dirempty.
143 */
144 public function is_dirempty($dir)
145 {
146 foreach (new \DirectoryIterator($dir) as $object) {
147 if ($object->isDot()) {
148 continue;
149 }
150
151 return false;
152 }
153
154 return true;
155 }
156
157 /**
158 * get_max_execution_time.
159 */
160 public function get_max_execution_time()
161 {
162 $max_execution_time = (int) \ini_get('max_execution_time');
163 if ($max_execution_time > 10) {
164 --$max_execution_time;
165 }
166
167 return $max_execution_time;
168 }
169
170 /**
171 * filesize.
172 */
173 public function filesize($file)
174 {
175 if (!@is_file($file)) {
176 return 0;
177 }
178
179 return sprintf('%u', @filesize($file));
180 }
181
182 /**
183 * touch.
184 */
185 public function touch($file, $time = 0, $atime = 0)
186 {
187 if (0 == $time) {
188 $time = time();
189 }
190
191 if (0 == $atime) {
192 $atime = time();
193 }
194
195 $nwdcx_suppresserrors = nwdcx_suppresserrors(true);
196
197 $ok = @touch($file, $time, $atime);
198
199 // user:group not same -> Utime failed: Operation not permitted
200 if (!$ok) {
201 $e = error_get_last();
202 if (!\is_array($e)) {
203 nwdcx_throwable(__METHOD__, $e);
204 }
205 }
206
207 // restore error level
208 nwdcx_suppresserrors($nwdcx_suppresserrors);
209
210 return $ok;
211 }
212
213 /**
214 * getchmod.
215 */
216 public function getchmod($file)
217 {
218 return substr(decoct(@fileperms($file)), -3);
219 }
220
221 /**
222 * chmod.
223 */
224 public function chmod($file, $mode = false)
225 {
226 if (!$mode) {
227 if (@is_file($file) && \defined('FS_CHMOD_FILE')) {
228 $mode = FS_CHMOD_FILE;
229 } elseif (@is_dir($file) && \defined('FS_CHMOD_DIR')) {
230 $mode = FS_CHMOD_DIR;
231 } else {
232 clearstatcache();
233 $stat = @stat(\dirname($file));
234 $mode = $stat['mode'] & 0007777;
235
236 if (@is_file($file)) {
237 $mode = $mode & 0000666;
238 }
239 }
240 }
241
242 clearstatcache();
243
244 $nwdcx_suppresserrors = nwdcx_suppresserrors(true);
245
246 $ok = @chmod($file, $mode);
247
248 nwdcx_suppresserrors($nwdcx_suppresserrors);
249
250 return $ok;
251 }
252
253 /**
254 * mkdir.
255 */
256 public function mkdir_p($path)
257 {
258 $parent = \dirname($path);
259 $okperms = [
260 '777',
261 '775',
262 '755',
263 ];
264
265 if (@is_dir($path) && \in_array($this->getchmod($path), $okperms) && \in_array($this->getchmod($parent), $okperms)) {
266 return true;
267 }
268
269 $nwdcx_suppresserrors = nwdcx_suppresserrors(true);
270
271 if (\function_exists('wp_mkdir_p')) {
272 $ok = @wp_mkdir_p($path);
273 } else {
274 $stat = @stat($parent);
275 if ($stat) {
276 $dir_perms = $stat['mode'] & 0007777;
277 } else {
278 $dir_perms = 0777;
279 }
280
281 $ok = @mkdir($path, $dir_perms, true);
282 }
283
284 nwdcx_suppresserrors($nwdcx_suppresserrors);
285
286 if (!$ok) {
287 return false;
288 }
289
290 if (!\in_array($this->getchmod($parent), $okperms)) {
291 $this->chmod($parent, 0755);
292 }
293
294 if (!\in_array($this->getchmod($path), $okperms)) {
295 $this->chmod($path, 0755);
296 }
297
298 return true;
299 }
300
301 /**
302 * copy.
303 */
304 public function copy($src, $dst)
305 {
306 $this->opcache_flush($src);
307 $this->opcache_flush($dst);
308
309 if (@copy($src, $dst)) {
310 $this->chmod($dst);
311
312 return true;
313 }
314
315 return false;
316 }
317
318 /**
319 * scanfiles.
320 */
321 public function scanfiles($dir, $maxdepth = null, $pattern = false)
322 {
323 $dir = nwdcx_normalizepath(realpath($dir));
324 if (false === $dir || !is_dir($dir) || !is_readable($dir)) {
325 return [];
326 }
327
328 if (null === $maxdepth) {
329 $maxdepth = 13;
330 }
331
332 if (empty($pattern)) {
333 $pattern = '@^(dump_)?([a-z0-9_]+)\-([a-z0-9]+).*\.php$@';
334 }
335
336 $rec_dir_iterator_flags = \FilesystemIterator::SKIP_DOTS | \RecursiveDirectoryIterator::KEY_AS_FILENAME | \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO;
337 $rec_dir_iterator = new \RecursiveDirectoryIterator($dir, $rec_dir_iterator_flags);
338 $reg_ite_iterator = new \RecursiveIteratorIterator($rec_dir_iterator);
339 $reg_ite_pattern = $pattern;
340 $reg_ite_mode = \RegexIterator::MATCH;
341 $reg_ite_flags = \RegexIterator::USE_KEY;
342 $reg_iterator = new \RegexIterator($reg_ite_iterator, $reg_ite_pattern, $reg_ite_mode, $reg_ite_flags);
343 $reg_iterator->setMaxDepth($maxdepth);
344
345 return $reg_iterator;
346 }
347
348 /**
349 * validate_file.
350 */
351 public function validate_file($filename)
352 {
353 try {
354 $fileo = new \SplFileObject($filename, 'rb');
355 } catch (\Throwable $e) {
356 nwdcx_throwable(__METHOD__, $e);
357
358 return false;
359 }
360
361 if ($fileo->flock(\LOCK_EX)) {
362 $fileo->seek(\PHP_INT_MAX);
363 $lines = $fileo->key();
364 $object = new \LimitIterator($fileo, $lines - 2);
365 foreach ($object as $line) {
366 if (false !== strpos($line, '/*@DOCKET_CACHE_EOF*/')) {
367 $fileo->flock(\LOCK_UN);
368
369 return true;
370 }
371 }
372 $fileo->flock(\LOCK_UN);
373 }
374
375 $fileo = null;
376
377 return false;
378 }
379
380 /**
381 * export_var.
382 */
383 public function export_var($data, &$error = '')
384 {
385 try {
386 $data = VarExporter::export($data);
387 } catch (\Throwable $e) {
388 nwdcx_throwable(__METHOD__, $e);
389 $error = $e->getMessage();
390
391 if (false !== strpos($error, 'Cannot export value of type "stdClass"')) {
392 $data = var_export($data, 1);
393 $data = str_replace('stdClass::__set_state', '(object)', $data);
394 } else {
395 $this->log('err', '000000000000-000000000000', 'export_var: '.$error);
396
397 return false;
398 }
399 }
400
401 // alias: shorter name
402 // map it in includes/compat.php
403 $data = str_replace(
404 '\Nawawi\Symfony\Component\VarExporter\Internal\\',
405 '\Nawawi\DocketCache\Exporter\\',
406 $data
407 );
408
409 return $data;
410 }
411
412 /**
413 * shutdown_cleanup.
414 */
415 public function shutdown_cleanup($file, $seq = 10)
416 {
417 // dont use register_shutdown_function to avoid issue with page cache plugin
418 add_action(
419 'shutdown',
420 function () use ($file) {
421 if (@is_file($file)) {
422 @unlink($file);
423 }
424 },
425 $seq
426 );
427 }
428
429 /**
430 * unlink.
431 */
432 public function unlink($file, $is_delete = false, $is_block = false)
433 {
434 // skip if not exist
435 if (!@is_file($file)) {
436 return true;
437 }
438
439 $ok = false;
440
441 $handle = @fopen($file, 'cb');
442 if ($handle) {
443 $lock = $is_block ? \LOCK_EX : \LOCK_EX | \LOCK_NB;
444 if (@flock($handle, $lock)) {
445 $ok = @ftruncate($handle, 0); // true, false
446 @flock($handle, \LOCK_UN);
447 }
448 @fclose($handle);
449 }
450
451 // bcoz we empty the file
452 $this->opcache_flush($file);
453
454 $do_delete = (nwdcx_construe('FLUSH_DELETE') && $this->is_php($file)) || $is_delete;
455
456 if ($do_delete && @unlink($file)) {
457 $ok = true;
458 }
459
460 clearstatcache();
461
462 // cleanup if ftruncate() failed
463 if (false === $ok) {
464 if (@is_file($file) && !@unlink($file)) {
465 // try cleanup at shutdown
466 $this->shutdown_cleanup($file);
467 }
468 }
469
470 // always true
471 return true;
472 }
473
474 /**
475 * put.
476 */
477 public function put($file, $data, $flag = 'cb', $is_block = false)
478 {
479 if (!$handle = @fopen($file, $flag)) {
480 return false;
481 }
482
483 $lock = $is_block ? \LOCK_EX : \LOCK_EX | \LOCK_NB;
484 $ok = false;
485 if (@flock($handle, $lock)) {
486 $len = \strlen($data);
487 $cnt = @fwrite($handle, $data);
488 @fflush($handle);
489 @flock($handle, \LOCK_UN);
490 if ($len === $cnt) {
491 $ok = true;
492 }
493 }
494 @fclose($handle);
495 clearstatcache();
496
497 if (false === $ok) {
498 $this->unlink($file, true);
499
500 return -1;
501 }
502
503 $this->opcache_flush($file);
504 $this->chmod($file);
505
506 return $ok;
507 }
508
509 /**
510 * dump.
511 */
512 public function dump($file, $data, $is_validate = false)
513 {
514 $dir = \dirname($file);
515 $tmpfile = $dir.'/'.'dump_'.uniqid().'_'.basename($file);
516
517 // cleanup at shutdown
518 $this->shutdown_cleanup($tmpfile, \PHP_INT_MAX);
519
520 // truncate reason
521 $this->opcache_flush($file);
522
523 $ok = $this->put($tmpfile, $data, 'cb', true);
524 if (true === $ok) {
525 try {
526 clearstatcache();
527 if (@is_file($tmpfile) && @rename($tmpfile, $file)) {
528 if (isset($nwdcx_suppresserrors)) {
529 nwdcx_suppresserrors($nwdcx_suppresserrors);
530 }
531
532 if ($is_validate && !$this->validate_file($file)) {
533 return false;
534 }
535
536 $this->chmod($file);
537
538 // compile
539 $this->opcache_compile($file);
540
541 return true;
542 }
543 } catch (\Throwable $e) {
544 nwdcx_throwable(__METHOD__, $e);
545 }
546
547 // failed to replace
548 $ok = false;
549 }
550
551 // cleanup if not bool true
552 if (@is_file($tmpfile)) {
553 @unlink($tmpfile);
554 }
555
556 // maybe -1, >= 1, false: return from put()
557 return $ok;
558 }
559
560 /**
561 * placeholder.
562 */
563 public function placeholder($path)
564 {
565 if (!@is_dir($path)) {
566 return false;
567 }
568
569 $file = rtrim($path, '/\\').'/index.html';
570 if (@is_file($file)) {
571 return false;
572 }
573
574 $code = '<html><head><meta name="robots" content="noindex, nofollow"><title>Docket Cache</title></head>';
575 $code .= '<body>Generated by <a href="https://wordpress.org/plugins/docket-cache/" rel="nofollow">Docket Cache</a></body></html>';
576
577 return $this->put($file, $code);
578 }
579
580 /**
581 * is_php.
582 */
583 public function is_php($file)
584 {
585 $file = basename($file);
586
587 return '.php' === substr($file, -4);
588 }
589
590 public function is_function_disabled($name)
591 {
592 $disable_functions = @\ini_get('disable_functions');
593 if (!empty($disable_functions)) {
594 $funcs = explode(',', $disable_functions);
595 if (\in_array($name, $funcs)) {
596 return true;
597 }
598 }
599
600 return false;
601 }
602
603 public function get_chunk_path($group, $key)
604 {
605 $chunk = substr($group, 0, 2).substr($key, 0, 2).substr($key, -2);
606
607 return chunk_split($chunk, 2, '/');
608 }
609
610 /**
611 * remove cache file with previous structure.
612 */
613 public function remove_non_chunk_cache($cache_path, $file)
614 {
615 if (nwdcx_construe('CHUNKCACHEDIR')) {
616 $filename = basename($file);
617 $filepath = rtrim($cache_path, '\\/').'/'.$filename;
618 if (is_file($filepath) && is_writable($filepath) && $this->is_docketcachefile($filepath)) {
619 return @unlink($filepath);
620 }
621 }
622
623 return false;
624 }
625
626 /**
627 * opcache_function_exists.
628 */
629 public function opcache_function_exists($name = null)
630 {
631 $list = [
632 'opcache_get_status',
633 'opcache_reset',
634 'opcache_compile_file',
635 'opcache_invalidate',
636 'opcache_is_script_cached',
637 'opcache_get_configuration',
638 ];
639
640 if (!empty($name)) {
641 return \in_array($name, $list) && !$this->is_function_disabled($name) && \function_exists($name);
642 }
643
644 foreach ($list as $func) {
645 if (!$this->is_function_disabled($name) && \function_exists($func)) {
646 return true;
647 }
648 }
649
650 return false;
651 }
652
653 /**
654 * is_opcache_enable.
655 */
656 public function is_opcache_enable()
657 {
658 if (@\ini_get('opcache.enable')) {
659 if (\function_exists('extension_loaded') && \extension_loaded('Zend OPcache')) {
660 return true;
661 }
662
663 if (\function_exists('get_loaded_extensions')) {
664 $arr = get_loaded_extensions(true);
665 if (\is_array($arr) && \in_array('Zend OPcache', $arr)) {
666 return true;
667 }
668 }
669
670 if ($this->opcache_function_exists()) {
671 return true;
672 }
673 }
674
675 return false;
676 }
677
678 /**
679 * is_opcache_blacklisted.
680 */
681 public function is_opcache_blacklisted()
682 {
683 $conf = \ini_get('opcache.blacklist_filename');
684 if (empty($conf)) {
685 return false;
686 }
687
688 $files = glob($conf);
689 if (empty($files) || !\is_array($files)) {
690 return false;
691 }
692
693 $abspath = rtrim(ABSPATH, '\\/');
694 foreach ($files as $file) {
695 if (!is_file($file) || !is_readable($file)) {
696 continue;
697 }
698 $buff = file($file, \FILE_SKIP_EMPTY_LINES | \FILE_IGNORE_NEW_LINES);
699 if (!empty($buff) && \is_array($buff)) {
700 foreach ($buff as $line) {
701 if (empty($line) || ';' === substr($line, 0, 1)) {
702 continue;
703 }
704
705 if ($abspath === substr(rtrim($line, '\\/'), 0, \strlen($abspath))) {
706 return true;
707 }
708 }
709 }
710 unset($buff);
711 }
712
713 return false;
714 }
715
716 /**
717 * is_opcache_filecache_only.
718 */
719 public function is_opcache_filecache_only()
720 {
721 return @\ini_get('opcache.file_cache_only');
722 }
723
724 /**
725 * opcache_filecache_only.
726 */
727 public function opcache_filecache_only()
728 {
729 if ($this->is_opcache_filecache_only() && \function_exists('opcache_get_status')) {
730 $data = @opcache_get_status();
731 if (!empty($data) && \is_array($data) && !empty($data['file_cache_only']) && !empty($data['file_cache']) && is_dir($data['file_cache'])) {
732 return $data;
733 }
734 }
735
736 return false;
737 }
738
739 /**
740 * opcache_filecache_scanfiles.
741 */
742 public function opcache_filecache_scanfiles($dir)
743 {
744 return $this->scanfiles($dir, -1, '@.*\.php\.bin$@');
745 }
746
747 /**
748 * opcache_filecache_flush.
749 */
750 public function opcache_filecache_flush($file)
751 {
752 if (!($fcdata = $this->opcache_filecache_only()) || false === strpos(nwdcx_normalizepath($file), nwdcx_normalizepath(ABSPATH))) {
753 return false;
754 }
755
756 $filem = nwdcx_normalizepath(rtrim($fcdata['file_cache'], '/\\').'/*/'.ltrim($file, '/\\').'.bin');
757 $match = glob($filem);
758 if (!empty($match) && \is_array($match)) {
759 foreach ($match as $fm) {
760 if (is_file($fm) && is_writable($fm)) {
761 @unlink($fm);
762 }
763 }
764 }
765
766 return true;
767 }
768
769 /**
770 * opcache_filecache_reset.
771 */
772 public function opcache_filecache_reset()
773 {
774 static $is_done = false;
775
776 $fcdata = $this->opcache_filecache_only();
777 if (!$is_done && !empty($fcdata)) {
778 $dir = $fcdata['file_cache'];
779 $cnt = 0;
780 $max_execution_time = $this->get_max_execution_time();
781 $slowdown = 0;
782 foreach ($this->opcache_filecache_scanfiles($dir) as $object) {
783 try {
784 if ($object->isFile()) {
785 $file = nwdcx_normalizepath($object->getPathName());
786 if (false !== strpos($file, nwdcx_normalizepath(ABSPATH)) && is_writable($file)) {
787 @unlink($file);
788 ++$cnt;
789 }
790 }
791 } catch (\Throwable $e) {
792 nwdcx_throwable(__METHOD__, $e);
793 }
794
795 if ($slowdown > 10) {
796 $slowdown = 0;
797 usleep(5000);
798 }
799
800 ++$slowdown;
801
802 if ($max_execution_time > 0 && \defined('WP_START_TIMESTAMP') && (microtime(true) - WP_START_TIMESTAMP) > $max_execution_time) {
803 break;
804 }
805 }
806
807 $is_done = true;
808
809 return $cnt;
810 }
811
812 return false;
813 }
814
815 /**
816 * opcache_is_cached.
817 */
818 public function opcache_is_cached($file)
819 {
820 if (!$this->is_opcache_enable()) {
821 return -1;
822 }
823
824 $fcdata = $this->opcache_filecache_only();
825 if (!empty($fcdata) && false !== strpos(nwdcx_normalizepath($file), nwdcx_normalizepath(ABSPATH))) {
826 $filem = nwdcx_normalizepath(rtrim($fcdata['file_cache'], '/\\').'/*/'.ltrim($file, '/\\').'.bin');
827 $match = glob($filem);
828 if (!empty($match) && \is_array($match)) {
829 return true;
830 }
831 }
832
833 if (\function_exists('opcache_is_script_cached')) {
834 return @opcache_is_script_cached($file);
835 }
836
837 return -1;
838 }
839
840 /**
841 * opcache_flush.
842 */
843 public function opcache_flush($file)
844 {
845 if (!$this->is_php($file) || !@is_file($file) || !$this->is_opcache_enable()) {
846 return false;
847 }
848
849 if ($this->opcache_filecache_flush($file)) {
850 return true;
851 }
852
853 // wp 5.5
854 if (\function_exists('wp_opcache_invalidate')) {
855 return @wp_opcache_invalidate($file, true);
856 }
857
858 if (\function_exists('opcache_invalidate')) {
859 return @opcache_invalidate($file, true);
860 }
861
862 return false;
863 }
864
865 /**
866 * opcache_compile.
867 */
868 public function opcache_compile($file)
869 {
870 if (!$this->is_opcache_enable()) {
871 return false;
872 }
873
874 if (!empty($_GET['_wpnonce']) && !empty($_GET['action']) && !empty($_GET['page']) && 'docket-cache' === $_GET['page'] && 'docket-flush-opcache' === $_GET['action']) {
875 return -1;
876 }
877
878 if (\function_exists('opcache_compile_file') && $this->is_php($file) && @is_file($file) && false === $this->opcache_is_cached($file)) {
879 $this->touch($file, time() - 60);
880
881 try {
882 return @opcache_compile_file($file);
883 } catch (\Throwable $e) {
884 nwdcx_throwable(__METHOD__, $e);
885 }
886 }
887
888 return false;
889 }
890
891 /**
892 * opcache_reset.
893 */
894 public function opcache_reset()
895 {
896 if (!$this->is_opcache_enable()) {
897 return false;
898 }
899
900 try {
901 if (!@opcache_reset()) {
902 return $this->opcache_filecache_reset();
903 }
904 } catch (\Throwable $e) {
905 nwdcx_throwable(__METHOD__, $e);
906
907 return false;
908 }
909
910 // always true
911 return true;
912 }
913
914 /**
915 * opcache_cleanup.
916 */
917 public function opcache_cleanup()
918 {
919 add_action(
920 'shutdown',
921 function () {
922 // anything involve disk, don't go into background
923 // $this->close_buffer();
924 $this->opcache_reset();
925 },
926 \PHP_INT_MAX
927 );
928 }
929
930 /**
931 * define_cache_path.
932 */
933 public function define_cache_path($cache_path)
934 {
935 $content_path = \defined('DOCKET_CACHE_CONTENT_PATH') ? DOCKET_CACHE_CONTENT_PATH : WP_CONTENT_DIR;
936 $content_path = nwdcx_normalizepath($content_path);
937
938 $cache_path = !empty($cache_path) && '/' !== $cache_path ? rtrim($cache_path, '/\\').'/' : $content_path.'/cache/docket-cache/';
939 $cache_path = nwdcx_normalizepath($cache_path);
940
941 if (!$this->is_docketcachedir($cache_path)) {
942 $cache_path = rtrim($cache_path, '/\\').'docket-cache/';
943 }
944
945 // create if not normal installation
946 if (false === strpos($content_path, '/wp-content/')) {
947 if (!@is_dir($cache_path)) {
948 $this->mkdir_p($cache_path);
949 }
950
951 if (!@is_dir($content_path)) {
952 $this->mkdir_p($content_path);
953 }
954 }
955
956 return $cache_path;
957 }
958
959 /**
960 * cachedir_flush.
961 */
962 public function cachedir_flush($dir, $cleanup = false, &$is_timeout = false)
963 {
964 static $is_done = false;
965
966 if ($is_done) {
967 return 0;
968 }
969
970 clearstatcache();
971
972 $dir = nwdcx_normalizepath(realpath($dir));
973
974 if (false === $dir || !@is_dir($dir) || !@is_writable($dir) || !$this->is_docketcachedir($dir)) {
975 return false;
976 }
977
978 if ($this->is_dirempty($dir)) {
979 return true;
980 }
981
982 wp_suspend_cache_addition(true);
983
984 $max_execution_time = $this->get_max_execution_time();
985 $flush_lock = DOCKET_CACHE_CONTENT_PATH.'/.object-cache-flush.txt';
986 if ($this->put($flush_lock, time())) {
987 $this->touch($flush_lock, time() + $max_execution_time);
988 }
989
990 $slowdown = 0;
991 $cnt = 0;
992
993 foreach ($this->scanfiles($dir) as $object) {
994 try {
995 if ($object->isFile()) {
996 $this->unlink($object->getPathName(), $cleanup ? true : false);
997 ++$cnt;
998 }
999 } catch (\Throwable $e) {
1000 nwdcx_throwable(__METHOD__, $e);
1001 }
1002
1003 if ($slowdown > 10) {
1004 $slowdown = 0;
1005 usleep(5000);
1006 }
1007
1008 ++$slowdown;
1009 if ($max_execution_time > 0 && \defined('WP_START_TIMESTAMP') && (microtime(true) - WP_START_TIMESTAMP) > $max_execution_time) {
1010 $is_timeout = true;
1011 break;
1012 }
1013 }
1014
1015 if ($cleanup) {
1016 // 24122020: deprecate
1017 $this->unlink($dir.'/index.php', true);
1018
1019 // placeholder
1020 $this->unlink($dir.'/index.html', true);
1021 }
1022
1023 if (@is_file($flush_lock)) {
1024 @unlink($flush_lock);
1025 }
1026
1027 wp_suspend_cache_addition(false);
1028 $is_done = true;
1029
1030 return $cnt;
1031 }
1032
1033 /**
1034 * cache_size.
1035 */
1036 public function cache_size($dir)
1037 {
1038 static $is_done = false;
1039
1040 $bytestotal = 0;
1041 $fsizetotal = 0;
1042 $filestotal = 0;
1043
1044 clearstatcache();
1045 if (!$is_done && $this->is_docketcachedir($dir) && !@is_file(DOCKET_CACHE_CONTENT_PATH.'/.object-cache-flush.txt')) {
1046 // hardmax
1047 $maxfile = 999000; // 1000000 - 1000;
1048 $cnt = 0;
1049 $slowdown = 0;
1050
1051 ignore_user_abort(true);
1052 $max_execution_time = $this->get_max_execution_time();
1053
1054 $pattern = '@^([a-z0-9]{12})\-([a-z0-9]{12})\.php$@';
1055 foreach ($this->scanfiles($dir, null, $pattern) as $object) {
1056 try {
1057 if ($object->isFile()) {
1058 $fx = $object->getPathName();
1059
1060 if (!$this->remove_non_chunk_cache($dir, $fx)) {
1061 $fs = $object->getSize();
1062
1063 if ($cnt >= $maxfile) {
1064 $this->unlink($fx, true);
1065 } elseif (0 === $fs) {
1066 $this->unlink($fx, true);
1067 } else {
1068 $data = $this->cache_get($fx);
1069 if (false === $data) {
1070 $this->unlink($fx, true);
1071 } else {
1072 $bytestotal += \strlen(serialize($data));
1073 unset($data);
1074
1075 $fsizetotal += $fs;
1076
1077 ++$filestotal;
1078 ++$cnt;
1079 }
1080 }
1081 }
1082 }
1083 } catch (\Throwable $e) {
1084 nwdcx_throwable(__METHOD__, $e);
1085 }
1086
1087 if ($slowdown > 10) {
1088 $slowdown = 0;
1089 usleep(5000);
1090 }
1091
1092 ++$slowdown;
1093
1094 if ($max_execution_time > 0 && \defined('WP_START_TIMESTAMP') && (microtime(true) - WP_START_TIMESTAMP) > $max_execution_time) {
1095 break;
1096 }
1097 }
1098 }
1099
1100 $is_done = true;
1101
1102 wp_cache_set('numfile', $filestotal, 'docketcache-gc', 60);
1103
1104 return [
1105 'timestamp' => time(),
1106 'size' => $bytestotal,
1107 'filesize' => $fsizetotal,
1108 'files' => $filestotal,
1109 ];
1110 }
1111
1112 public function get_fatal_error_filename($file)
1113 {
1114 // sesimple yang mungkin.
1115 return $file.'-error.txt';
1116 }
1117
1118 public function has_fatal_error_before($file)
1119 {
1120 $file_fatal = $this->get_fatal_error_filename($file);
1121
1122 return @is_file($file_fatal);
1123 }
1124
1125 public function validate_fatal_error_file($file)
1126 {
1127 $file_fatal = $this->get_fatal_error_filename($file);
1128 if (!@is_file($file_fatal)) {
1129 return;
1130 }
1131
1132 if (!@is_file($file)) {
1133 @unlink($file_fatal);
1134
1135 return;
1136 }
1137
1138 $fm = time() - 10; // 10s
1139 if ($fm > @filemtime($file_fatal)) {
1140 if ($this->validate_file($file)) {
1141 @unlink($file_fatal);
1142
1143 return;
1144 }
1145
1146 // update timestamp
1147 $this->touch($file_fatal);
1148 }
1149 }
1150
1151 private function suspend_cache_file($file, $error, $seconds = 0)
1152 {
1153 $seconds = (int) $seconds;
1154 $file_fatal = $this->get_fatal_error_filename($file);
1155
1156 $errmsg = date('Y-m-d H:i:s T').\PHP_EOL.$error;
1157 if ($this->dump($file_fatal, $errmsg)) {
1158 if ($seconds > 0) {
1159 $this->touch($file, time() + $seconds);
1160 }
1161
1162 $this->dump(\dirname($file_fatal).'/last-error.txt', $errmsg);
1163
1164 return true;
1165 }
1166
1167 return false;
1168 }
1169
1170 private function capture_fatal_error()
1171 {
1172 register_shutdown_function(
1173 function () {
1174 $error = error_get_last();
1175 if ($error && \in_array($error['type'], [\E_ERROR, \E_PARSE, \E_COMPILE_ERROR, \E_USER_ERROR, \E_RECOVERABLE_ERROR, \E_CORE_ERROR], true)) {
1176 $file_error = $error['file'];
1177
1178 if ($this->is_docketcachefile($file_error)) {
1179 $this->dump($file_error, '<?php return false;');
1180
1181 $error['file'] = basename($error['file']);
1182 // 300s = 5m delay
1183 if ($this->suspend_cache_file($file_error, $this->export_var($error), 300)) {
1184 // refresh page if possible
1185 if ('cli' !== \PHP_SAPI && !wp_doing_ajax()) {
1186 echo '<script>document.body.innerHTML="";window.setTimeout(function() { window.location.assign(window.location.href); }, 750);</script>';
1187 }
1188 }
1189 }
1190 }
1191 }
1192 );
1193 }
1194
1195 /**
1196 * cache_get.
1197 */
1198 public function cache_get($file)
1199 {
1200 if (!@is_file($file) || empty($this->filesize($file))) {
1201 return false;
1202 }
1203
1204 if (!$handle = @fopen($file, 'rb')) {
1205 return false;
1206 }
1207
1208 if ($this->has_fatal_error_before($file)) {
1209 return false;
1210 }
1211
1212 // capture non-throwable
1213 if (nwdcx_construe('CAPTURE_FATALERROR')) {
1214 $this->capture_fatal_error();
1215 }
1216
1217 // cache data
1218 $data = [];
1219
1220 // include when we can read, try to avoid fatal error.
1221 // LOCK_SH = shared lock
1222 if (flock($handle, \LOCK_SH)) {
1223 try {
1224 $data = @include $file;
1225 } catch (\Throwable $e) {
1226 $error = $e->getMessage();
1227
1228 $file_error = $e->getFile();
1229 if ($this->is_docketcachefile($file_error)) {
1230 $errmsg = 'E: '.$error.\PHP_EOL;
1231 $errmsg .= 'L: '.$e->getLine().\PHP_EOL;
1232 $errmsg .= 'F: '.basename($file_error).\PHP_EOL;
1233 $this->suspend_cache_file($file, $errmsg);
1234 }
1235
1236 $this->log('err', '000000000000-000000000000', 'cache_get: '.$error);
1237 $data = false;
1238 }
1239
1240 @flock($handle, \LOCK_UN);
1241 }
1242 @fclose($handle);
1243
1244 if (empty($data) || !isset($data['data'])) {
1245 return false;
1246 }
1247
1248 // 15052022
1249 /*if (false === $this->opcache_is_cached($file)) {
1250 $this->opcache_compile($file);
1251 }*/
1252
1253 return $data;
1254 }
1255
1256 /**
1257 * code_stub.
1258 */
1259 public function code_stub($data = '')
1260 {
1261 $is_debug = \defined('WP_DEBUG') && WP_DEBUG;
1262 $is_data = !empty($data);
1263 $ucode = '';
1264 if ($is_data && false !== strpos($data, 'Registry::p(')) {
1265 if (@preg_match_all('@Registry::p\(\'([a-zA-Z_]+)\'\)@', $data, $mm)) {
1266 if (!empty($mm) && isset($mm[1]) && \is_array($mm[1])) {
1267 $cls = $mm[1];
1268 foreach ($cls as $clsname) {
1269 if ('stdClass' !== $clsname) {
1270 if ($is_debug) {
1271 $reflector = new \ReflectionClass($clsname);
1272 $clsfname = $reflector->getFileName();
1273 if (false !== $clsfname) {
1274 $ucode .= '/* f: '.str_replace(ABSPATH, '', $clsfname).' */'.\PHP_EOL;
1275 }
1276 }
1277 $ucode .= "if ( !@class_exists('".$clsname."', false) ) { return false; }".\PHP_EOL;
1278 }
1279 }
1280 unset($cls, $clsname);
1281 }
1282 unset($mm);
1283 }
1284 }
1285
1286 $code = '<?php ';
1287 $code .= "if ( !\defined('ABSPATH') ) { return false; }";
1288 if ($is_data) {
1289 $code .= \PHP_EOL;
1290 if (!empty($ucode)) {
1291 $code .= $ucode;
1292 }
1293 $code .= 'return '.$data.';'.\PHP_EOL;
1294 $code .= '/*@DOCKET_CACHE_EOF*/';
1295 }
1296
1297 return $code;
1298 }
1299
1300 /**
1301 * log.
1302 */
1303 public function log($tag, $id, $data, $caller = '')
1304 {
1305 $do_flush = false;
1306 $file = nwdcx_constval('LOG_FILE');
1307 if (empty($file)) {
1308 return false;
1309 }
1310
1311 $logsize = nwdcx_constval('LOG_SIZE');
1312 if (empty($logsize) || !\is_int($logsize)) {
1313 $logsize = 0;
1314 }
1315
1316 if (is_multisite()) {
1317 $file = nwdcx_network_filepath($file);
1318 }
1319
1320 if (@is_file($file)) {
1321 if (nwdcx_construe('LOG_FLUSH') && 'flush' === $tag || ($logsize > 0 && $this->filesize($file) >= $logsize)) {
1322 $do_flush = true;
1323 }
1324 }
1325
1326 $timestamp = date('Y-m-d H:i:s T');
1327
1328 $rtag = trim($tag);
1329 if (\in_array($rtag, ['hit', 'miss', 'err', 'exp', 'del', 'info', 'dev'])) {
1330 $tag = str_pad($rtag, 5);
1331 }
1332 $log = '['.$timestamp.'] '.$tag.': "'.$id.'" "'.trim($data).'" "'.$caller.'"';
1333
1334 $flags = !$do_flush ? \LOCK_EX | \FILE_APPEND : \LOCK_EX;
1335 $do_chmod = !@is_file($file);
1336 if (@file_put_contents($file, $log.\PHP_EOL, $flags)) {
1337 if ($do_chmod) {
1338 $this->chmod($file);
1339 }
1340
1341 return true;
1342 }
1343
1344 return false;
1345 }
1346
1347 /**
1348 * sanitize_timestamp.
1349 */
1350 public function sanitize_timestamp($time)
1351 {
1352 $time = (int) $time;
1353 if ($time < 0) {
1354 $time = 0;
1355 } else {
1356 $max = ceil(log10($time));
1357 if ($max > 10 || 'NaN' === $max) {
1358 $time = 0;
1359 }
1360 }
1361
1362 return $time;
1363 }
1364
1365 /**
1366 * sanitize_maxttl.
1367 */
1368 public function sanitize_maxttl($seconds)
1369 {
1370 $seconds = $this->sanitize_timestamp($seconds);
1371
1372 // 86400 = 1d
1373 // 345600 = 4d
1374 // 2419200 = 28d
1375 if ($seconds < 86400) {
1376 $seconds = 345600;
1377 } elseif ($seconds > 2419200) {
1378 $seconds = 2419200;
1379 }
1380
1381 return $seconds;
1382 }
1383
1384 /**
1385 * sanitize_maxfile.
1386 */
1387 public function sanitize_maxfile($maxfile, $default = 50000)
1388 {
1389 $maxfile = (int) $maxfile;
1390 $min = 200;
1391 $max = 1000000;
1392 if (empty($maxfile)) {
1393 $maxfile = $default;
1394 }
1395
1396 if ($maxfile < $min) {
1397 $maxfile = $default;
1398 } elseif ($maxfile > $max) {
1399 $maxfile = $max;
1400 }
1401
1402 return $maxfile;
1403 }
1404
1405 /**
1406 * sanitize_precache_maxfile.
1407 */
1408 public function sanitize_precache_maxfile($maxfile)
1409 {
1410 if (empty($maxfile) || (int) $maxfile < 1) {
1411 return 0;
1412 }
1413
1414 return $this->sanitize_maxfile($maxfile);
1415 }
1416
1417 /**
1418 * sanitize_maxsize.
1419 */
1420 public function sanitize_maxsize($bytes)
1421 {
1422 $min = 1048576; // 1M
1423 $max = 10485760; // 10M
1424 $bytes = (int) $bytes;
1425
1426 if ($bytes < $min) {
1427 return 3145728; // 3M
1428 }
1429
1430 if ($bytes > $max) {
1431 $bytes = $max;
1432 }
1433
1434 return $bytes;
1435 }
1436
1437 /**
1438 * sanitize_maxsizedisk.
1439 */
1440 public function sanitize_maxsizedisk($bytes)
1441 {
1442 if (empty($bytes) || !\is_int($bytes)) {
1443 $maxsizedisk = 524288000; // 500MB
1444 }
1445
1446 if ($bytes < 104857600) {
1447 $bytes = 104857600;
1448 }
1449
1450 return $bytes;
1451 }
1452
1453 /**
1454 * valid_timestamp.
1455 */
1456 public function valid_timestamp($timestamp)
1457 {
1458 $timestamp = $this->sanitize_timestamp($timestamp);
1459
1460 return $timestamp > 0;
1461 }
1462
1463 public function keys_alloptions()
1464 {
1465 // reference: wp-admin/includes/schema.php
1466 // exclude: rewrite_rules, active_plugins, uninstall_plugins, cron
1467 // exclude: widget_categories, widget_text, widget_rss
1468 return [
1469 'siteurl' => 1,
1470 'home' => 1,
1471 'blogname' => 1,
1472 'blogdescription' => 1,
1473 'users_can_register' => 1,
1474 'admin_email' => 1,
1475 'start_of_week' => 1,
1476 'use_balanceTags' => 1,
1477 'use_smilies' => 1,
1478 'require_name_email' => 1,
1479 'comments_notify' => 1,
1480 'posts_per_rss' => 1,
1481 'rss_use_excerpt' => 1,
1482 'mailserver_url' => 1,
1483 'mailserver_login' => 1,
1484 'mailserver_pass' => 1,
1485 'mailserver_port' => 1,
1486 'default_category' => 1,
1487 'default_comment_status' => 1,
1488 'default_ping_status' => 1,
1489 'default_pingback_flag' => 1,
1490 'posts_per_page' => 1,
1491 'date_format' => 1,
1492 'time_format' => 1,
1493 'links_updated_date_format' => 1,
1494 'comment_moderation' => 1,
1495 'moderation_notify' => 1,
1496 'permalink_structure' => 1,
1497 'hack_file' => 1,
1498 'blog_charset' => 1,
1499 'category_base' => 1,
1500 'ping_sites' => 1,
1501 'comment_max_links' => 1,
1502 'gmt_offset' => 1,
1503 'default_email_category' => 1,
1504 'template' => 1,
1505 'stylesheet' => 1,
1506 'comment_registration' => 1,
1507 'html_type' => 1,
1508 'use_trackback' => 1,
1509 'default_role' => 1,
1510 'db_version' => 1,
1511 'uploads_use_yearmonth_folders' => 1,
1512 'upload_path' => 1,
1513 'blog_public' => 1,
1514 'default_link_category' => 1,
1515 'show_on_front' => 1,
1516 'tag_base' => 1,
1517 'show_avatars' => 1,
1518 'avatar_rating' => 1,
1519 'upload_url_path' => 1,
1520 'thumbnail_size_w' => 1,
1521 'thumbnail_size_h' => 1,
1522 'thumbnail_crop' => 1,
1523 'medium_size_w' => 1,
1524 'medium_size_h' => 1,
1525 'avatar_default' => 1,
1526 'large_size_w' => 1,
1527 'large_size_h' => 1,
1528 'image_default_link_type' => 1,
1529 'image_default_size' => 1,
1530 'image_default_align' => 1,
1531 'close_comments_for_old_posts' => 1,
1532 'close_comments_days_old' => 1,
1533 'thread_comments' => 1,
1534 'thread_comments_depth' => 1,
1535 'page_comments' => 1,
1536 'comments_per_page' => 1,
1537 'default_comments_page' => 1,
1538 'comment_order' => 1,
1539 'timezone_string' => 1,
1540 'page_for_posts' => 1,
1541 'page_on_front' => 1,
1542 'default_post_format' => 1,
1543 'link_manager_enabled' => 1,
1544 'finished_splitting_shared_terms' => 1,
1545 'site_icon' => 1,
1546 'medium_large_size_w' => 1,
1547 'medium_large_size_h' => 1,
1548 'wp_page_for_privacy_policy' => 1,
1549 'show_comments_cookies_opt_in' => 1,
1550 'admin_email_lifespan' => 1,
1551 'initial_db_version' => 1,
1552 'fresh_site' => 1,
1553 'current_theme' => 1,
1554 'theme_switched' => 1,
1555 'generate_update_core_typography' => 1,
1556 'WPLANG' => 1,
1557 'new_admin_email' => 1,
1558 'recovery_mode_email_last_sent' => 1,
1559 'comment_previously_approved' => 1,
1560 'finished_updating_comment_type' => 1,
1561 'db_upgraded' => 1,
1562 /* wp >= 5.6.0 */
1563 'auto_update_core_dev' => 1,
1564 'auto_update_core_minor' => 1,
1565 'auto_update_core_major' => 1,
1566 /* wp >= 5.7 */
1567 'https_detection_errors' => 1,
1568 /* wp >= 5.8 */
1569 'wp_force_deactivated_plugins' => 1,
1570 ];
1571 }
1572
1573 /**
1574 * optimize_alloptions.
1575 */
1576 public function optimize_alloptions()
1577 {
1578 add_filter(
1579 'pre_cache_alloptions',
1580 function ($alloptions) {
1581 $wp_options = $this->keys_alloptions();
1582
1583 foreach ($alloptions as $key => $value) {
1584 // skip
1585 if (empty($value)) {
1586 continue;
1587 }
1588
1589 if (!\array_key_exists($key, $wp_options)) {
1590 unset($alloptions[$key]);
1591 }
1592 }
1593
1594 return $alloptions;
1595 },
1596 \PHP_INT_MIN
1597 );
1598 }
1599 }
1600