PluginProbe
Docket Cache – Object Cache Accelerator / 22.07.05
Docket Cache – Object Cache Accelerator v22.07.05
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.05, at includes/src/Filesystem.php

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