PluginProbe
WPSSO Core – Complete Schema Markup and Meta Tags / trunk
WPSSO Core – Complete Schema Markup and Meta Tags vtrunk
22.6.1 22.6.0 22.5.3 22.5.2 22.5.1 22.4.0 22.3.0 22.2.1 22.2.0 22.1.3 22.1.2 22.1.1 22.1.0 22.0.0 21.13.3 21.13.2 trunk 21.13.1
wpsso / lib / com / cache.php

cache.php in WPSSO Core – Complete Schema Markup and Meta Tags trunk, at lib/com/cache.php

1,306 lines 35.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * License: GPLv3
4 * License URI: https://www.gnu.org/licenses/gpl.txt
5 * Copyright 2012-2026 Jean-Sebastien Morisset (https://surniaulula.com/)
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9
10 die( 'These aren\'t the droids you\'re looking for.' );
11 }
12
13 if ( ! class_exists( 'SucomCache' ) ) {
14
15 class SucomCache {
16
17 private $p; // Plugin class object.
18
19 private $plugin_id = 'sucom';
20 private $plugin_ucid = 'SUCOM';
21 private $text_domain = 'sucom';
22 private $label_transl = '';
23
24 public $base_dir = '';
25 public $base_url = '/cache/';
26
27 public $default_file_cache_exp = DAY_IN_SECONDS;
28 public $default_object_cache_exp = DAY_IN_SECONDS;
29
30 public $curl_connect_timeout = 5; // The number of seconds to wait while trying to connect. Use 0 to wait indefinitely.
31 public $curl_timeout = 10; // The maximum number of seconds to allow cURL functions to execute.
32 public $curl_max_redirs = 10; // The maximum amount of HTTP redirections to follow.
33
34 private $url_get_mtimes = array(); // Associative array of URL fetch times (true, false, or microtime).
35 private $ignored = array(
36 'expires' => DAY_IN_SECONDS,
37 'loaded' => false,
38 'for_secs' => 600, // Ignore failed URLs for 10 mins by default.
39 'urls' => array(),
40 );
41
42 public function __construct( $plugin = null, $plugin_id = null, $text_domain = null, $label_transl = null ) {
43
44 if ( ! class_exists( 'SucomUtil' ) ) { // Just in case.
45
46 require_once trailingslashit( dirname( __FILE__ ) ) . 'util.php';
47 }
48
49 $this->set_config( $plugin, $plugin_id, $text_domain, $label_transl );
50
51 add_action( 'shutdown', array( $this, 'save_ignored_urls' ), -3000, 0 );
52 }
53
54 /*
55 * Set property values for text domain, notice label, etc.
56 */
57 private function set_config( $plugin = null, $plugin_id = null, $text_domain = null, $label_transl = null ) {
58
59 if ( $plugin !== null ) {
60
61 $this->p =& $plugin;
62
63 if ( ! empty( $this->p->debug->enabled ) ) {
64
65 $this->p->debug->mark();
66 }
67 }
68
69 if ( $plugin_id !== null ) {
70
71 $this->plugin_id = $plugin_id;
72
73 } elseif ( ! empty( $this->p->id ) ) {
74
75 $this->plugin_id = $this->p->id;
76 }
77
78 $this->plugin_ucid = strtoupper( $this->plugin_id );
79
80 if ( $text_domain !== null ) {
81
82 $this->text_domain = $text_domain;
83
84 } elseif ( ! empty( $this->p->cf[ 'plugin' ][ $this->plugin_id ][ 'text_domain' ] ) ) {
85
86 $this->text_domain = $this->p->cf[ 'plugin' ][ $this->plugin_id ][ 'text_domain' ];
87 }
88
89 if ( $label_transl !== null ) {
90
91 $this->label_transl = $label_transl; // argument is already translated
92
93 } elseif ( ! empty( $this->p->cf[ 'menu' ][ 'title' ] ) ) {
94
95 $this->label_transl = sprintf( __( '%s Notice', $this->text_domain ),
96 _x( $this->p->cf[ 'menu' ][ 'title' ], 'menu title', $this->text_domain ) );
97
98 } else {
99
100 $this->label_transl = __( 'Notice', $this->text_domain );
101 }
102
103 $this->base_dir = trailingslashit( constant( $this->plugin_ucid . '_CACHE_DIR' ) );
104
105 $this->base_url = trailingslashit( constant( $this->plugin_ucid . '_CACHE_URL' ) );
106 }
107
108 /*
109 * This method is only run on an as-needed basis.
110 */
111 public function maybe_load_ignored() {
112
113 if ( $this->ignored[ 'loaded' ] ) {
114
115 return;
116 }
117
118 if ( ! empty( $this->p->debug->enabled ) ) {
119
120 $this->p->debug->log( 'getting transient for ignored urls' );
121 }
122
123 $cache_md5_pre = $this->plugin_id . '_';
124 $cache_salt = __CLASS__ . '::ignored_urls';
125 $cache_id = $cache_md5_pre . md5( $cache_salt );
126 $cache_ret = get_transient( $cache_id );
127
128 /*
129 * Retrieve the list of ignored URLs cached, while keeping the existing 'expires' and 'for_secs' array values.
130 */
131 if ( isset( $cache_ret[ 'urls' ] ) ) {
132
133 $this->ignored[ 'urls' ] = $cache_ret[ 'urls' ];
134
135 $this->ignored[ 'saved_urls' ] = $cache_ret[ 'urls' ];
136
137 } else {
138
139 $this->ignored[ 'urls' ] = array();
140
141 $this->ignored[ 'saved_urls' ] = array();
142 }
143
144 $this->ignored[ 'for_secs' ] = (int) apply_filters( 'sucom_cache_ignored_for_secs', $this->ignored[ 'for_secs' ] );
145
146 $this->ignored[ 'loaded' ] = true;
147 }
148
149 public function save_ignored_urls() {
150
151 if ( ! empty( $this->p->debug->enabled ) ) {
152
153 $this->p->debug->mark();
154 }
155
156 /*
157 * 'loaded' will be true if any ignored URL method has been run.
158 */
159 if ( $this->ignored[ 'loaded' ] ) {
160
161 /*
162 * Only save ignored URLs if the current URL array is different to the saved URL array.
163 */
164 if ( $this->ignored[ 'urls' ] !== $this->ignored[ 'saved_urls' ] ) {
165
166 $cache_md5_pre = $this->plugin_id . '_';
167 $cache_salt = __CLASS__ . '::ignored_urls';
168 $cache_id = $cache_md5_pre . md5( $cache_salt );
169
170 if ( ! empty( $this->p->debug->enabled ) ) {
171
172 $this->p->debug->log( 'saving ' . count( $this->ignored[ 'urls' ] ) . ' ignored urls' );
173 }
174
175 set_transient( $cache_id, $this->ignored, $this->ignored[ 'expires' ] );
176
177 $this->ignored[ 'saved_urls' ] = $this->ignored[ 'urls' ]; // Just in case.
178 }
179 }
180 }
181
182 public function count_ignored_urls() {
183
184 return count( $this->get_ignored_urls() );
185 }
186
187 public function get_ignored_urls() {
188
189 $this->maybe_load_ignored();
190
191 return $this->ignored[ 'urls' ];
192 }
193
194 /*
195 * Returns false or time in seconds.
196 */
197 public function is_ignored_url( $url_nofrag ) {
198
199 $this->maybe_load_ignored();
200
201 if ( isset( $this->ignored[ 'urls' ][ $url_nofrag ] ) ) {
202
203 $time_diff = time() - $this->ignored[ 'urls' ][ $url_nofrag ];
204 $time_left = $this->ignored[ 'for_secs' ] - $time_diff;
205
206 if ( $time_left > 0 ) {
207
208 if ( $this->p->debug->enabled ) {
209
210 $this->p->debug->log( 'requests to retrieve ' . $url_nofrag . ' ignored for another ' . $time_left . ' second(s)' );
211 }
212
213 return $time_left;
214
215 }
216
217 unset( $this->ignored[ 'urls' ][ $url_nofrag ] );
218 }
219
220 return false;
221 }
222
223 public function add_ignored_url( $url_nofrag, $mtime_total = null, $curl_errnum = 0, $curl_errmsg = '', $ssl_verify = 0, $http_code = 0 ) {
224
225 $this->maybe_load_ignored();
226
227 $this->ignored[ 'urls' ][ $url_nofrag ] = time();
228
229 if ( is_admin() ) {
230
231 $errors = array();
232
233 if ( $mtime_total ) {
234
235 $errors[] = sprintf( __( 'Error retrieving %1$s for caching (after %2$.3f seconds).',
236 $this->text_domain ), '<a href="' . $url_nofrag . '">' . $url_nofrag . '</a>', $mtime_total );
237
238 } else {
239
240 $errors[] = sprintf( __( 'Error retrieving %1$s for caching.',
241 $this->text_domain ), '<a href="' . $url_nofrag . '">' . $url_nofrag . '</a>' );
242 }
243
244 if ( $curl_errnum ) {
245
246 $errors[] = sprintf( __( 'cURL error code %1$d %2$s.', $this->text_domain ),
247 $curl_errnum, trim( $curl_errmsg, ' .' ) );
248 }
249
250 if ( $ssl_verify ) {
251
252 $errors[] = sprintf( __( 'SSL verification failed with code %1$d.', $this->text_domain ), $ssl_verify );
253 }
254
255 if ( $http_code ) {
256
257 $errors[] = sprintf( __( 'HTTP connection returned code %1$d.', $this->text_domain ), $http_code );
258 }
259
260 if ( 301 === $http_code || 302 === $http_code ) {
261
262 /*
263 * PHP safe mode is an attempt to solve the shared-server security problem. It is
264 * architecturally incorrect to try to solve this problem at the PHP level, but since the
265 * alternatives at the web server and OS levels aren't very realistic, many people,
266 * especially ISP's, use safe mode for now.
267 *
268 * This feature has been DEPRECATED as of PHP 5.3 and REMOVED as of PHP 5.4.
269 */
270 if ( ini_get( 'safe_mode' ) ) {
271
272 $errors[] = sprintf( __( 'The PHP "%s" setting is enabled - the PHP cURL library cannot follow URL redirects.',
273 $this->text_domain ), 'safe_mode' );
274
275 $errors[] = sprintf( __( 'The "%s" setting is deprecated since PHP version 5.3 and removed from PHP since version 5.4.',
276 $this->text_domain ), 'safe_mode' );
277
278 $errors[] = __( 'Please contact your hosting provider to have this setting disabled or install a newer version of PHP.',
279 $this->text_domain );
280
281 /*
282 * open_basedir can be used to limit the files that can be accessed by PHP to the specified
283 * directory-tree, including the file itself. When a script tries to access the filesystem,
284 * for example using include, or fopen(), the location of the file is checked. When the
285 * file is outside the specified directory-tree, PHP will refuse to access it.
286 */
287 } elseif ( ini_get( 'open_basedir' ) ) {
288
289 $errors[] = sprintf( __( 'The PHP "%s" setting is enabled - the PHP cURL library cannot follow URL redirects.',
290 $this->text_domain ), 'open_basedir' );
291
292 $errors[] = __( 'Please contact your hosting provider to have this setting disabled.',
293 $this->text_domain );
294
295 } else {
296
297 $errors[] = sprintf( __( 'The maximum number of URL redirects (%d) may have been exceeded.',
298 $this->text_domain ), $this->curl_max_redirs );
299 }
300 }
301
302 $errors[] = sprintf( __( 'Additional requests to retrieve this URL will be ignored for another %d second(s).',
303 $this->text_domain ), $this->ignored[ 'for_secs' ] );
304
305 /*
306 * Combine all strings into one error notice.
307 */
308 $this->p->notice->err( $errors );
309 }
310
311 if ( $this->p->debug->enabled ) {
312
313 $this->p->debug->log( 'ignoring requests to retrieve ' . $url_nofrag );
314 }
315 }
316
317 /*
318 * Clear all ignored URLs.
319 *
320 * Returns a count of cleared URLs.
321 */
322 public function clear_ignored_urls() {
323
324 $this->maybe_load_ignored();
325
326 $count = count( $this->ignored[ 'urls' ] );
327
328 $this->ignored[ 'urls' ] = array(); // Clear the ignored URLs.
329
330 $this->save_ignored_urls();
331
332 return $count;
333 }
334
335 /*
336 * Clear a single ignored URL.
337 *
338 * Returns true if the URL was ignored and is now cleared.
339 */
340 public function clear_ignored_url( $url_nofrag ) {
341
342 $this->maybe_load_ignored();
343
344 if ( isset( $this->ignored[ 'urls' ][ $url_nofrag ] ) ) {
345
346 unset( $this->ignored[ 'urls' ][ $url_nofrag ] );
347
348 return true;
349 }
350
351 return false;
352 }
353
354 public function clear( $url, $cache_pre_ext = '' ) {
355
356 if ( ! empty( $this->p->debug->enabled ) ) {
357
358 $this->p->debug->mark();
359 }
360
361 $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the fragment.
362
363 $cache_salt = __CLASS__ . '::get(url:' . $url_nofrag . ')';
364
365 $this->clear_ignored_url( $url_nofrag );
366
367 $this->clear_cache_data( $cache_salt, $cache_pre_ext, $url );
368 }
369
370 /*
371 * Get the formatted microtime it took to retrieve the URL.
372 *
373 * Returns false if retrieving failed, true if the URL was already cached, or the formatted microtime.
374 */
375 public function get_url_mtime( $url, $precision = 3 ) {
376
377 if ( isset( $this->url_get_mtimes[ $url ] ) ) {
378
379 if ( is_bool( $this->url_get_mtimes[ $url ] ) ) {
380
381 return $this->url_get_mtimes[ $url ];
382
383 } else {
384
385 return sprintf( '%.0' . $precision . 'f', $this->url_get_mtimes[ $url ] );
386 }
387
388 } else {
389
390 return false;
391 }
392 }
393
394 /*
395 * Get image size for remote URL and cache for 300 seconds (5 minutes) by default.
396 *
397 * If $cache_exp_secs is null, then use the default expiration time.
398 *
399 * If $cache_exp_secs is false, then get but do not save the data.
400 *
401 * Note that PHP v7.1 or better is required to get the image size of WebP images.
402 */
403 public function get_image_size( $image_url, $cache_exp_secs = 300, $curl_opts = array(), $error_handler = null ) {
404
405 if ( $this->p->debug->enabled ) {
406
407 $this->p->debug->mark();
408 }
409
410 $file_path = $this->get( $image_url, 'file_path', $cache_type = 'file', $cache_exp_secs, $cache_pre_ext = '', $curl_opts );
411
412 if ( ! empty( $file_path ) ) { // False on error.
413
414 if ( file_exists( $file_path ) ) {
415
416 if ( null !== $error_handler ) {
417
418 $previous_error_handler = set_error_handler( $error_handler );
419 }
420
421 $image_size = getimagesize( $file_path ); // Note that WebP is only supported since PHP v7.1.
422
423 if ( null !== $error_handler ) {
424
425 restore_error_handler();
426 }
427
428 if ( $this->p->debug->enabled ) {
429
430 $this->p->debug->log_arr( 'getimagesize', $image_size );
431 }
432
433 return $image_size;
434
435 } elseif ( $this->p->debug->enabled ) {
436
437 $this->p->debug->log( $file_path.' does not exist' );
438 }
439
440 } elseif ( $this->p->debug->enabled ) {
441
442 $this->p->debug->log( 'returned image file path is empty' );
443 }
444
445 return false;
446 }
447
448 /*
449 * Called by SucomForm->get_event_load_json_script().
450 */
451 public function get_data_url( $cache_salt, $cache_data, $cache_exp_secs = null, $cache_pre_ext = '' ) {
452
453 if ( $this->p->debug->enabled ) {
454
455 $this->p->debug->mark();
456 }
457
458 $file_name = md5( $cache_salt ) . $cache_pre_ext;
459 $cache_file = $this->base_dir . $file_name;
460 $cache_url = $this->base_url . $file_name;
461
462 if ( file_exists( $cache_file ) ) {
463
464 $file_mod_time = filemtime( $cache_file );
465 $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs;
466
467 if ( false !== $file_exp_secs && $file_mod_time > time() - $file_exp_secs ) {
468
469 if ( $this->p->debug->enabled ) {
470
471 $this->p->debug->log( 'cached file found: returning url ' . $cache_url );
472 }
473
474 $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url );
475
476 return $cache_url;
477
478 } elseif ( @unlink( $cache_file ) ) {
479
480 if ( $this->p->debug->enabled ) {
481
482 $this->p->debug->log( 'removed expired cache file ' . $cache_file );
483 }
484
485 } else {
486
487 if ( $this->p->debug->enabled ) {
488
489 $this->p->debug->log( 'error removing cache file ' . $cache_file );
490 }
491
492 $notice_pre = sprintf( '%s error:', __METHOD__ );
493 $notice_msg = sprintf( __( 'Error removing cache file %s.', $this->text_domain ), $cache_file );
494
495 $this->p->notice->err( $notice_msg );
496
497 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
498 }
499
500 } elseif ( $this->p->debug->enabled ) {
501
502 $this->p->debug->log( 'cached file not found: creating ' . $cache_url );
503 }
504
505 if ( $this->save_cache_data( $cache_salt, $cache_data, $cache_type = 'file', $cache_exp_secs, $cache_pre_ext ) ) {
506
507 if ( file_exists( $cache_file ) ) {
508
509 $file_mod_time = filemtime( $cache_file );
510
511 if ( $this->p->debug->enabled ) {
512
513 $this->p->debug->log( 'cache data sucessfully saved' );
514 }
515
516 $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url );
517
518 return $cache_url;
519 }
520 }
521
522 return false;
523 }
524
525 public function get_cache_files_size_mb() {
526
527 if ( ! $dh = @opendir( $this->base_dir ) ) return false;
528
529 $cache_files = array();
530
531 while ( $file_name = @readdir( $dh ) ) {
532
533 $cache_file = $this->base_dir . $file_name;
534
535 if ( ! preg_match( '/^(\..*|index\.php)$/', $file_name ) && is_file( $cache_file ) ) {
536
537 if ( $file_ext = strtoupper( pathinfo( $cache_file, PATHINFO_EXTENSION ) ) ) {
538
539 if ( ! isset( $cache_files[ $file_ext ] ) ) {
540
541 $cache_files[ $file_ext ] = array( 'count' => 0, 'size' => 0 );
542 }
543
544 $cache_files[ $file_ext ][ 'count' ]++;
545
546 $cache_files[ $file_ext ][ 'size' ] += filesize( $cache_file );
547 }
548 }
549 }
550
551 closedir( $dh );
552
553 foreach ( $cache_files as $ext => &$info ) {
554
555 $info[ 'size' ] = $info[ 'size' ] / 1024 / 1024;
556 }
557
558 return $cache_files;
559 }
560
561 /*
562 * If $cache_exp_secs is null, then use the default expiration time.
563 *
564 * If $cache_exp_secs is false, then get but do not save the data.
565 */
566 public function get( $url, $cache_format = 'url', $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '', $curl_opts = array() ) {
567
568 if ( $this->p->debug->enabled ) {
569
570 $this->p->debug->mark();
571 }
572
573 $failure = 'url' === $cache_format ? $url : false;
574
575 $this->url_get_mtimes[ $url ] = false; // Default value for failure.
576
577 if ( ! extension_loaded( 'curl' ) ) {
578
579 if ( $this->p->debug->enabled ) {
580
581 $this->p->debug->log( 'exiting early: curl library is missing' );
582 }
583
584 $notice_pre = sprintf( '%s error:', __METHOD__ );
585 $notice_msg = __( 'PHP cURL library missing - contact your hosting provider to have the cURL library installed.', $this->text_domain );
586
587 $this->p->notice->err( $notice_msg );
588
589 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
590
591 return $failure;
592 }
593
594 $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the URL fragment.
595 $url_path = wp_parse_url( $url_nofrag, PHP_URL_PATH );
596
597 if ( '' === $cache_pre_ext ) { // Default is an empty string.
598
599 if ( $cache_pre_ext = pathinfo( $url_path, PATHINFO_EXTENSION ) ) {
600
601 $cache_pre_ext = '.' . $cache_pre_ext;
602 }
603 }
604
605 $url_fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
606
607 if ( ! empty( $url_fragment ) ) {
608
609 $url_fragment = '#' . $url_fragment;
610 }
611
612 $cache_salt = __CLASS__ . '::get(url:' . $url_nofrag . ')';
613 $cache_file = $this->base_dir . md5( $cache_salt ) . $cache_pre_ext;
614 $cache_url = $this->base_url . md5( $cache_salt ) . $cache_pre_ext . $url_fragment;
615 $cache_data = false;
616
617 /*
618 * Return immediately if the cache contains what we need.
619 */
620 switch ( $cache_format ) {
621
622 case 'raw':
623
624 $cache_data = $this->get_cache_data( $cache_salt, $cache_type, $cache_exp_secs, $cache_pre_ext );
625
626 if ( false !== $cache_data ) {
627
628 if ( $this->p->debug->enabled ) {
629
630 $this->p->debug->log( 'cached data found: returning ' . strlen( $cache_data ) . ' chars' );
631 }
632
633 $this->url_get_mtimes[ $url ] = true; // Signal return is from cache.
634
635 return $cache_data;
636 }
637
638 break;
639
640 case 'url':
641 case 'file_path':
642
643 if ( file_exists( $cache_file ) ) {
644
645 $file_mod_time = filemtime( $cache_file );
646 $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs;
647
648 if ( false !== $cache_exp_secs && $file_mod_time > time() - $file_exp_secs ) {
649
650 $this->url_get_mtimes[ $url ] = true; // Signal that return is from cache.
651
652 if ( 'url' === $cache_format ) {
653
654 $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url );
655
656 if ( $this->p->debug->enabled ) {
657
658 $this->p->debug->log( 'cached file found: returning ' . $cache_format . ' ' . $cache_url );
659 }
660
661 return $cache_url;
662
663 } else {
664
665 if ( $this->p->debug->enabled ) {
666
667 $this->p->debug->log( 'cached file found: returning ' . $cache_format . ' ' . $cache_file );
668 }
669
670 return $cache_file;
671 }
672
673 } elseif ( @unlink( $cache_file ) ) { // Remove expired file.
674
675 if ( $this->p->debug->enabled ) {
676
677 $this->p->debug->log( 'removed expired cache file ' . $cache_file );
678 }
679
680 } else {
681
682 if ( $this->p->debug->enabled ) {
683
684 $this->p->debug->log( 'error removing cache file ' . $cache_file );
685 }
686
687 $notice_pre = sprintf( '%s error:', __METHOD__ );
688 $notice_msg = sprintf( __( 'Error removing cache file %s.', $this->text_domain ), $cache_file );
689
690 $this->p->notice->err( $notice_msg );
691
692 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
693 }
694 }
695
696 break;
697
698 default:
699
700 if ( $this->p->debug->enabled ) {
701
702 $this->p->debug->log( 'cache return type for ' . $cache_format . ' is unknown' );
703 }
704
705 return $failure;
706 }
707
708 if ( $this->is_ignored_url( $url_nofrag ) ) { // Returns false or time in seconds.
709
710 return $failure;
711 }
712
713 /*
714 * Maybe throttle connections for specific hosts, like YouTube for example.
715 */
716 $this->maybe_throttle_host( $url_nofrag, $curl_opts );
717
718 $ch = curl_init();
719
720 curl_setopt( $ch, CURLOPT_URL, $url_nofrag );
721 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
722 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $this->curl_connect_timeout ); // 10 seconds (default is 300 seconds).
723 curl_setopt( $ch, CURLOPT_TIMEOUT, $this->curl_timeout ); // 15 seconds (default is no timeout).
724
725 /*
726 * When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity. Curl
727 * verifies whether the certificate is authentic, i.e. that you can trust that the server is who the
728 * certificate says it is. This trust is based on a chain of digital signatures, rooted in certification
729 * authority (CA) certificates you supply. curl uses a default bundle of CA certificates (the path for that
730 * is determined at build time) and you can specify alternate certificates with the CURLOPT_CAINFO option
731 * or the CURLOPT_CAPATH option.
732 *
733 * See https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html
734 */
735 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 1 );
736
737 /*
738 * When CURLOPT_SSL_VERIFYHOST is 2, that certificate must indicate that the server is the server to which
739 * you meant to connect, or the connection fails. Simply put, it means it has to have the same name in the
740 * certificate as is in the URL you operate against.
741 *
742 * See https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
743 */
744 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
745
746 /*
747 * Define and disable the "Expect: 100-continue" header.
748 */
749 curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Expect:' ) );
750
751 if ( ! ini_get( 'safe_mode' ) && ! ini_get( 'open_basedir' ) ) {
752
753 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
754 curl_setopt( $ch, CURLOPT_MAXREDIRS, $this->curl_max_redirs );
755
756 } elseif ( $this->p->debug->enabled ) {
757
758 $this->p->debug->log( 'curl: PHP safe_mode or open_basedir defined - cannot use CURLOPT_FOLLOWLOCATION' );
759 }
760
761 /*
762 * Define cURL options from values defined as constants.
763 */
764 $allowed_curl_const = array(
765 'CAINFO',
766 'CONNECTTIMEOUT',
767 'PROXY',
768 'PROXYUSERPWD',
769 'SSLVERSION',
770 'USERAGENT',
771 );
772
773 foreach ( $allowed_curl_const as $const_suffix ) {
774
775 $curl_const_name = 'CURLOPT_' . $const_suffix;
776
777 $custom_const_name = $this->plugin_ucid . '_PHP_CURL_' . $const_suffix;
778
779 if ( defined( $custom_const_name ) ) {
780
781 curl_setopt( $ch, constant( $curl_const_name ), constant( $custom_const_name ) );
782 }
783 }
784
785 /*
786 * Define cURL options provided by the $curl_opts method arguent (empty by default).
787 *
788 * Example:
789 *
790 * $curl_opts = array(
791 * 'CURLOPT_USERAGENT' => 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
792 * 'CURLOPT_COOKIELIST' => 'ALL', // Erases all cookies held in memory.
793 * 'CURLOPT_CACHE_THROTTLE' => $throttle_secs, // Internal curl option.
794 * );
795 */
796 if ( ! empty( $curl_opts ) ) {
797
798 foreach ( $curl_opts as $opt_key => $opt_val ) {
799
800 if ( 0 === strpos( $opt_key, 'CURLOPT_' ) ) { // Just in case.
801
802 if ( 'CURLOPT_CACHE_THROTTLE' === $opt_key ) continue; // Internal curl option.
803
804 if ( $this->p->debug->enabled ) {
805
806 $this->p->debug->log( 'curl: setting custom curl option ' . $opt_key . ' = ' . $opt_val );
807 }
808
809 curl_setopt( $ch, constant( $opt_key ), $opt_val );
810 }
811 }
812 }
813
814 if ( $this->p->debug->enabled ) {
815
816 $this->p->debug->log( 'curl: fetching ' . $url_nofrag );
817 }
818
819 $mtime_start = microtime( $get_float = true );
820 $cache_data = curl_exec( $ch );
821 $mtime_total = microtime( $get_float = true ) - $mtime_start;
822 $curl_errnum = curl_errno( $ch );
823 $curl_errmsg = curl_error( $ch ); // See https://curl.se/libcurl/c/libcurl-errors.html.
824 $ssl_verify = (int) curl_getinfo( $ch, CURLINFO_SSL_VERIFYRESULT ); // See https://www.php.net/manual/en/function.curl-getinfo.php.
825 $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
826
827 curl_close( $ch );
828
829 $http_success_codes = array( 200, 201 );
830
831 if ( $this->p->debug->enabled ) {
832
833 $this->p->debug->log( 'curl: execution time = ' . $mtime_total );
834 $this->p->debug->log( 'curl: error number = ' . $curl_errnum );
835 $this->p->debug->log( 'curl: error message = ' . $curl_errmsg );
836 $this->p->debug->log( 'curl: ssl verify code = ' . $ssl_verify );
837 $this->p->debug->log( 'curl: http return code = ' . $http_code );
838 }
839
840 if ( $curl_errnum || $ssl_verify || ! in_array( $http_code, $http_success_codes ) ) {
841
842 $this->add_ignored_url( $url_nofrag, $mtime_total, $curl_errnum, $curl_errmsg, $ssl_verify, $http_code );
843
844 } else {
845
846 $this->url_get_mtimes[ $url ] = $mtime_total;
847
848 if ( empty( $cache_data ) ) {
849
850 if ( $this->p->debug->enabled ) {
851
852 $this->p->debug->log( 'cache data returned is empty' );
853 }
854
855 } elseif ( false !== $cache_exp_secs ) { // Optimize and check first.
856
857 if ( $this->save_cache_data( $cache_salt, $cache_data, $cache_type, $cache_exp_secs, $cache_pre_ext ) ) {
858
859 if ( $this->p->debug->enabled ) {
860
861 $this->p->debug->log( 'cache data sucessfully saved' );
862 }
863 }
864 }
865
866 switch ( $cache_format ) {
867
868 case 'raw':
869
870 return $cache_data;
871
872 case 'url':
873
874 $file_mod_time = filemtime( $cache_file );
875
876 $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url );
877
878 return $cache_url;
879
880 case 'file_path':
881
882 return $cache_file;
883 }
884 }
885
886 return $failure;
887 }
888
889 /*
890 * If $cache_exp_secs is null, then use the default expiration time.
891 *
892 * If $cache_exp_secs is false, then get but do not save the data.
893 */
894 public function get_cache_data( $cache_salt, $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '' ) {
895
896 $cache_data = false;
897
898 if ( false === $cache_exp_secs ) {
899
900 return $cache_data;
901 }
902
903 if ( $this->p->debug->enabled ) {
904
905 $this->p->debug->log( $cache_type . ' cache salt ' . $cache_salt );
906 }
907
908 $cache_md5_pre = $cache_pre_ext ? $cache_pre_ext : $this->plugin_id . '_'; // Default is an empty string.
909 $cache_id = $cache_md5_pre . md5( $cache_salt );
910
911 switch ( $cache_type ) {
912
913 case 'wp_cache':
914
915 $cache_data = wp_cache_get( $cache_id, __CLASS__ );
916
917 break;
918
919 case 'transient':
920
921 $cache_data = get_transient( $cache_id );
922
923 break;
924
925 case 'file':
926
927 $file_name = md5( $cache_salt ) . $cache_pre_ext;
928 $cache_file = $this->base_dir . $file_name;
929 $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs;
930
931 if ( ! file_exists( $cache_file ) ) {
932
933 if ( $this->p->debug->enabled ) {
934
935 $this->p->debug->log( 'cache file ' . $cache_file . ' does not exist' );
936 }
937
938 } elseif ( ! is_readable( $cache_file ) ) {
939
940 if ( $this->p->debug->enabled ) {
941
942 $this->p->debug->log( 'cache file ' . $cache_file . ' is not readable' );
943 }
944
945 $notice_pre = sprintf( '%s error:', __METHOD__ );
946 $notice_msg = sprintf( __( 'Cache file %s is not readable.', $this->text_domain ), $cache_file );
947
948 $this->p->notice->err( $notice_msg );
949
950 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
951
952 } elseif ( filemtime( $cache_file ) < time() - $file_exp_secs ) {
953
954 if ( $this->p->debug->enabled ) {
955
956 $this->p->debug->log( $cache_file . ' is expired' );
957 }
958
959 @unlink( $cache_file );
960
961 } elseif ( ! $fz = filesize( $cache_file ) ) { // Check if file size is greater then 0.
962
963 if ( $this->p->debug->enabled ) {
964
965 $this->p->debug->log( $cache_file . ' is empty' );
966 }
967
968 @unlink( $cache_file );
969
970 } elseif ( ! $fh = @fopen( $cache_file, 'rb' ) ) { // Check if we get a file handle.
971
972 if ( $this->p->debug->enabled ) {
973
974 $this->p->debug->log( 'failed to open the cache file ' . $cache_file . ' for reading' );
975 }
976
977 $notice_pre = sprintf( '%s error:', __METHOD__ );
978 $notice_msg = sprintf( __( 'Failed to open the cache file %s for reading.', $this->text_domain ), $cache_file );
979
980 $this->p->notice->err( $notice_msg );
981
982 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
983
984 } else {
985
986 $cache_data = fread( $fh, $fz );
987
988 fclose( $fh );
989 }
990
991 break;
992
993 default:
994
995 if ( $this->p->debug->enabled ) {
996
997 $this->p->debug->log( 'unknown cache type: ' . $cache_type );
998 }
999
1000 break;
1001 }
1002
1003 if ( $this->p->debug->enabled && false !== $cache_data ) {
1004
1005 $this->p->debug->log( 'cache data retrieved from ' . $cache_type );
1006 }
1007
1008 return $cache_data; // return data or empty string
1009 }
1010
1011 /*
1012 * If $cache_exp_secs is null, then use the default expiration time.
1013 *
1014 * If $cache_exp_secs is false, then get but do not save the data.
1015 */
1016 public function save_cache_data( $cache_salt, $cache_data = '', $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '' ) {
1017
1018 $data_saved = false;
1019
1020 if ( empty( $cache_data ) || false === $cache_exp_secs ) {
1021
1022 return $data_saved;
1023 }
1024
1025 $obj_exp_secs = null === $cache_exp_secs ? $this->default_object_cache_exp : $cache_exp_secs;
1026 $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs;
1027
1028 if ( $this->p->debug->enabled ) {
1029
1030 $this->p->debug->log( $cache_type . ' cache salt ' . $cache_salt );
1031 }
1032
1033 $cache_md5_pre = $cache_pre_ext ? $cache_pre_ext : $this->plugin_id . '_'; // Default is an empty string.
1034 $cache_id = $cache_md5_pre . md5( $cache_salt );
1035
1036 switch ( $cache_type ) {
1037
1038 case 'wp_cache':
1039
1040 wp_cache_set( $cache_id, $cache_data, __CLASS__, $obj_exp_secs );
1041
1042 if ( $this->p->debug->enabled ) {
1043
1044 $this->p->debug->log( 'cache data saved to ' . $cache_type . ' ' .
1045 $cache_id . ' (' . $obj_exp_secs . ' seconds)' );
1046 }
1047
1048 $data_saved = true; // Success.
1049
1050 break;
1051
1052 case 'transient':
1053
1054 set_transient( $cache_id, $cache_data, $obj_exp_secs );
1055
1056 if ( $this->p->debug->enabled ) {
1057
1058 $this->p->debug->log( 'cache data saved to ' . $cache_type . ' ' .
1059 $cache_id . ' (' . $obj_exp_secs . ' seconds)' );
1060 }
1061
1062 $data_saved = true; // Success.
1063
1064 break;
1065
1066 case 'file':
1067
1068 $file_name = md5( $cache_salt ) . $cache_pre_ext;
1069 $cache_file = $this->base_dir . $file_name;
1070
1071 if ( ! $file_exp_secs ) { // False or 0.
1072
1073 if ( file_exists( $cache_file ) ) {
1074
1075 @unlink( $cache_file );
1076 }
1077
1078 } elseif ( ! is_dir( $this->base_dir ) && ! mkdir( $this->base_dir ) ) {
1079
1080 if ( $this->p->debug->enabled ) {
1081
1082 $this->p->debug->log( 'failed to create the ' . $this->base_dir . ' cache folder.' );
1083 }
1084
1085 $notice_pre = sprintf( '%s error:', __METHOD__ );
1086 $notice_msg = sprintf( __( 'Failed to create the %s cache folder.', $this->text_domain ), $this->base_dir );
1087
1088 $this->p->notice->err( $notice_msg );
1089
1090 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
1091
1092 } elseif ( ! is_writable( $this->base_dir ) ) {
1093
1094 if ( $this->p->debug->enabled ) {
1095
1096 $this->p->debug->log( 'cache folder ' . $this->base_dir . ' is not writable' );
1097 }
1098
1099 $notice_pre = sprintf( '%s error:', __METHOD__ );
1100 $notice_msg = sprintf( __( 'Cache folder %s is not writable.', $this->text_domain ), $this->base_dir );
1101
1102 $this->p->notice->err( $notice_msg );
1103
1104 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
1105
1106 } elseif ( ! $fh = @fopen( $cache_file, 'wb' ) ) { // Check if we get a file handle.
1107
1108 if ( $this->p->debug->enabled ) {
1109
1110 $this->p->debug->log( 'failed to open the cache file ' . $cache_file . ' for writing' );
1111 }
1112
1113 $notice_pre = sprintf( '%s error:', __METHOD__ );
1114 $notice_msg = sprintf( __( 'Failed to open the cache file %s for writing.', $this->text_domain ), $cache_file );
1115
1116 $this->p->notice->err( $notice_msg );
1117
1118 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
1119
1120 } elseif ( fwrite( $fh, $cache_data ) ) {
1121
1122 if ( $this->p->debug->enabled ) {
1123
1124 $this->p->debug->log( 'data saved to cache file ' . $cache_file );
1125 }
1126
1127 fclose( $fh );
1128
1129 $data_saved = true; // Success.
1130
1131 } else { // Error writing to cache file.
1132
1133 fclose( $fh );
1134
1135 @unlink( $cache_file );
1136
1137 if ( $this->p->debug->enabled ) {
1138
1139 $this->p->debug->log( 'failed writing data to cache file ' . $cache_file );
1140 }
1141
1142 $notice_pre = sprintf( '%s error:', __METHOD__ );
1143 $notice_msg = sprintf( __( 'Failed writing data to cache file %s.', $this->text_domain ), $cache_file );
1144
1145 $this->p->notice->err( $notice_msg );
1146
1147 SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg );
1148 }
1149
1150 break;
1151
1152 default:
1153
1154 if ( $this->p->debug->enabled ) {
1155
1156 $this->p->debug->log( 'unknown cache type: ' . $cache_type );
1157 }
1158
1159 break;
1160 }
1161
1162 return $data_saved; // Return true or false.
1163 }
1164
1165 public function clear_cache_data( $cache_salt, $cache_pre_ext = '', $url = '' ) {
1166
1167 if ( 0 !== strpos( $cache_pre_ext, '.' ) ) { // Not a filename extension.
1168
1169 $cache_md5_pre = $cache_pre_ext ? $cache_pre_ext : $this->plugin_id . '_'; // Default is an empty string.
1170 $cache_id = $cache_md5_pre . md5( $cache_salt );
1171
1172 if ( wp_cache_delete( $cache_id, __CLASS__ ) ) {
1173
1174 if ( $this->p->debug->enabled ) {
1175
1176 $this->p->debug->log( 'cleared object cache salt: ' . $cache_salt );
1177 }
1178 }
1179
1180 if ( delete_transient( $cache_id ) ) {
1181
1182 if ( $this->p->debug->enabled ) {
1183
1184 $this->p->debug->log( 'cleared transient cache salt: ' . $cache_salt );
1185 }
1186 }
1187 }
1188
1189 if ( $this->base_dir ) { // Just in case.
1190
1191 if ( '' === $cache_pre_ext ) { // Default is an empty string.
1192
1193 if ( ! empty( $url ) ) {
1194
1195 $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the fragment.
1196 $url_path = wp_parse_url( $url_nofrag, PHP_URL_PATH );
1197
1198 if ( $cache_pre_ext = pathinfo( $url_path, PATHINFO_EXTENSION ) ) {
1199
1200 $cache_pre_ext = '.' . $cache_pre_ext;
1201 }
1202 }
1203 }
1204
1205 $file_name = md5( $cache_salt ) . $cache_pre_ext;
1206 $cache_file = $this->base_dir . $file_name;
1207
1208 if ( file_exists( $cache_file ) ) {
1209
1210 if ( @unlink( $cache_file ) ) {
1211
1212 if ( $this->p->debug->enabled ) {
1213
1214 $this->p->debug->log( 'cleared local cache file: ' . $cache_file );
1215 }
1216 }
1217 }
1218 }
1219 }
1220
1221 public function is_cached( $url, $cache_format = 'url', $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '' ) {
1222
1223 $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the URL fragment.
1224 $url_path = wp_parse_url( $url_nofrag, PHP_URL_PATH );
1225
1226 if ( '' === $cache_pre_ext ) { // Default is an empty string.
1227
1228 if ( $cache_pre_ext = pathinfo( $url_path, PATHINFO_EXTENSION ) ) {
1229
1230 $cache_pre_ext = '.' . $cache_pre_ext;
1231 }
1232 }
1233
1234 $cache_salt = __CLASS__ . '::get(url:' . $url_nofrag . ')';
1235 $cache_file = $this->base_dir . md5( $cache_salt ) . $cache_pre_ext;
1236 $cache_url = $this->base_url . md5( $cache_salt ) . $cache_pre_ext;
1237
1238 /*
1239 * Return immediately if the cache contains what we need.
1240 */
1241 switch ( $cache_format ) {
1242
1243 case 'raw':
1244
1245 if ( false !== $this->get_cache_data( $cache_salt, $cache_type, $cache_exp_secs, $cache_pre_ext ) ) {
1246
1247 return true;
1248 }
1249
1250 case 'url':
1251 case 'file_path':
1252
1253 if ( file_exists( $cache_file ) ) {
1254
1255 $file_mod_time = filemtime( $cache_file );
1256 $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs;
1257
1258 if ( false !== $cache_exp_secs && $file_mod_time > time() - $file_exp_secs ) {
1259
1260 if ( 'url' === $cache_format ) {
1261
1262 $cache_url = add_query_arg( 'moddate', gmdate( 'c', $file_mod_time ), $cache_url );
1263
1264 return $cache_url;
1265
1266 } else return $cache_file;
1267 }
1268 }
1269
1270 break;
1271 }
1272
1273 return false;
1274 }
1275
1276 /*
1277 * Maybe throttle connections for specific hosts, like YouTube for example.
1278 */
1279 public function maybe_throttle_host( $url, $curl_opts ) {
1280
1281 if ( ! empty( $curl_opts[ 'CURLOPT_CACHE_THROTTLE' ] ) ) {
1282
1283 $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the fragment.
1284 $url_host = wp_parse_url( $url_nofrag, PHP_URL_HOST );
1285 $cache_md5_pre = $this->plugin_id . '_!_'; // Preserved on clear cache.
1286 $cache_salt = __METHOD__ . '(url_host:' . $url_host . ')'; // Throttle by host.
1287 $cache_id = $cache_md5_pre . md5( $cache_salt );
1288
1289 while ( $cache_ret = get_transient( $cache_id ) ) {
1290
1291 if ( $cache_ret + $curl_opts[ 'CURLOPT_CACHE_THROTTLE' ] > time() ) {
1292
1293 sleep( 1 );
1294
1295 continue;
1296 }
1297
1298 break;
1299 }
1300
1301 set_transient( $cache_id, time(), $curl_opts[ 'CURLOPT_CACHE_THROTTLE' ] );
1302 }
1303 }
1304 }
1305 }
1306