DAY_IN_SECONDS, 'loaded' => false, 'for_secs' => 600, // Ignore failed URLs for 10 mins by default. 'urls' => array(), ); public function __construct( $plugin = null, $plugin_id = null, $text_domain = null, $label_transl = null ) { if ( ! class_exists( 'SucomUtil' ) ) { // Just in case. require_once trailingslashit( dirname( __FILE__ ) ) . 'util.php'; } $this->set_config( $plugin, $plugin_id, $text_domain, $label_transl ); add_action( 'shutdown', array( $this, 'save_ignored_urls' ), -3000, 0 ); } /* * Set property values for text domain, notice label, etc. */ private function set_config( $plugin = null, $plugin_id = null, $text_domain = null, $label_transl = null ) { if ( $plugin !== null ) { $this->p =& $plugin; if ( ! empty( $this->p->debug->enabled ) ) { $this->p->debug->mark(); } } if ( $plugin_id !== null ) { $this->plugin_id = $plugin_id; } elseif ( ! empty( $this->p->id ) ) { $this->plugin_id = $this->p->id; } $this->plugin_ucid = strtoupper( $this->plugin_id ); if ( $text_domain !== null ) { $this->text_domain = $text_domain; } elseif ( ! empty( $this->p->cf[ 'plugin' ][ $this->plugin_id ][ 'text_domain' ] ) ) { $this->text_domain = $this->p->cf[ 'plugin' ][ $this->plugin_id ][ 'text_domain' ]; } if ( $label_transl !== null ) { $this->label_transl = $label_transl; // argument is already translated } elseif ( ! empty( $this->p->cf[ 'menu' ][ 'title' ] ) ) { $this->label_transl = sprintf( __( '%s Notice', $this->text_domain ), _x( $this->p->cf[ 'menu' ][ 'title' ], 'menu title', $this->text_domain ) ); } else { $this->label_transl = __( 'Notice', $this->text_domain ); } $this->base_dir = trailingslashit( constant( $this->plugin_ucid . '_CACHE_DIR' ) ); $this->base_url = trailingslashit( constant( $this->plugin_ucid . '_CACHE_URL' ) ); } /* * This method is only run on an as-needed basis. */ public function maybe_load_ignored() { if ( $this->ignored[ 'loaded' ] ) { return; } if ( ! empty( $this->p->debug->enabled ) ) { $this->p->debug->log( 'getting transient for ignored urls' ); } $cache_md5_pre = $this->plugin_id . '_'; $cache_salt = __CLASS__ . '::ignored_urls'; $cache_id = $cache_md5_pre . md5( $cache_salt ); $cache_ret = get_transient( $cache_id ); /* * Retrieve the list of ignored URLs cached, while keeping the existing 'expires' and 'for_secs' array values. */ if ( isset( $cache_ret[ 'urls' ] ) ) { $this->ignored[ 'urls' ] = $cache_ret[ 'urls' ]; $this->ignored[ 'saved_urls' ] = $cache_ret[ 'urls' ]; } else { $this->ignored[ 'urls' ] = array(); $this->ignored[ 'saved_urls' ] = array(); } $this->ignored[ 'for_secs' ] = (int) apply_filters( 'sucom_cache_ignored_for_secs', $this->ignored[ 'for_secs' ] ); $this->ignored[ 'loaded' ] = true; } public function save_ignored_urls() { if ( ! empty( $this->p->debug->enabled ) ) { $this->p->debug->mark(); } /* * 'loaded' will be true if any ignored URL method has been run. */ if ( $this->ignored[ 'loaded' ] ) { /* * Only save ignored URLs if the current URL array is different to the saved URL array. */ if ( $this->ignored[ 'urls' ] !== $this->ignored[ 'saved_urls' ] ) { $cache_md5_pre = $this->plugin_id . '_'; $cache_salt = __CLASS__ . '::ignored_urls'; $cache_id = $cache_md5_pre . md5( $cache_salt ); if ( ! empty( $this->p->debug->enabled ) ) { $this->p->debug->log( 'saving ' . count( $this->ignored[ 'urls' ] ) . ' ignored urls' ); } set_transient( $cache_id, $this->ignored, $this->ignored[ 'expires' ] ); $this->ignored[ 'saved_urls' ] = $this->ignored[ 'urls' ]; // Just in case. } } } public function count_ignored_urls() { return count( $this->get_ignored_urls() ); } public function get_ignored_urls() { $this->maybe_load_ignored(); return $this->ignored[ 'urls' ]; } /* * Returns false or time in seconds. */ public function is_ignored_url( $url_nofrag ) { $this->maybe_load_ignored(); if ( isset( $this->ignored[ 'urls' ][ $url_nofrag ] ) ) { $time_diff = time() - $this->ignored[ 'urls' ][ $url_nofrag ]; $time_left = $this->ignored[ 'for_secs' ] - $time_diff; if ( $time_left > 0 ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'requests to retrieve ' . $url_nofrag . ' ignored for another ' . $time_left . ' second(s)' ); } return $time_left; } unset( $this->ignored[ 'urls' ][ $url_nofrag ] ); } return false; } public function add_ignored_url( $url_nofrag, $mtime_total = null, $curl_errnum = 0, $curl_errmsg = '', $ssl_verify = 0, $http_code = 0 ) { $this->maybe_load_ignored(); $this->ignored[ 'urls' ][ $url_nofrag ] = time(); if ( is_admin() ) { $errors = array(); if ( $mtime_total ) { $errors[] = sprintf( __( 'Error retrieving %1$s for caching (after %2$.3f seconds).', $this->text_domain ), '' . $url_nofrag . '', $mtime_total ); } else { $errors[] = sprintf( __( 'Error retrieving %1$s for caching.', $this->text_domain ), '' . $url_nofrag . '' ); } if ( $curl_errnum ) { $errors[] = sprintf( __( 'cURL error code %1$d %2$s.', $this->text_domain ), $curl_errnum, trim( $curl_errmsg, ' .' ) ); } if ( $ssl_verify ) { $errors[] = sprintf( __( 'SSL verification failed with code %1$d.', $this->text_domain ), $ssl_verify ); } if ( $http_code ) { $errors[] = sprintf( __( 'HTTP connection returned code %1$d.', $this->text_domain ), $http_code ); } if ( 301 === $http_code || 302 === $http_code ) { /* * PHP safe mode is an attempt to solve the shared-server security problem. It is * architecturally incorrect to try to solve this problem at the PHP level, but since the * alternatives at the web server and OS levels aren't very realistic, many people, * especially ISP's, use safe mode for now. * * This feature has been DEPRECATED as of PHP 5.3 and REMOVED as of PHP 5.4. */ if ( ini_get( 'safe_mode' ) ) { $errors[] = sprintf( __( 'The PHP "%s" setting is enabled - the PHP cURL library cannot follow URL redirects.', $this->text_domain ), 'safe_mode' ); $errors[] = sprintf( __( 'The "%s" setting is deprecated since PHP version 5.3 and removed from PHP since version 5.4.', $this->text_domain ), 'safe_mode' ); $errors[] = __( 'Please contact your hosting provider to have this setting disabled or install a newer version of PHP.', $this->text_domain ); /* * open_basedir can be used to limit the files that can be accessed by PHP to the specified * directory-tree, including the file itself. When a script tries to access the filesystem, * for example using include, or fopen(), the location of the file is checked. When the * file is outside the specified directory-tree, PHP will refuse to access it. */ } elseif ( ini_get( 'open_basedir' ) ) { $errors[] = sprintf( __( 'The PHP "%s" setting is enabled - the PHP cURL library cannot follow URL redirects.', $this->text_domain ), 'open_basedir' ); $errors[] = __( 'Please contact your hosting provider to have this setting disabled.', $this->text_domain ); } else { $errors[] = sprintf( __( 'The maximum number of URL redirects (%d) may have been exceeded.', $this->text_domain ), $this->curl_max_redirs ); } } $errors[] = sprintf( __( 'Additional requests to retrieve this URL will be ignored for another %d second(s).', $this->text_domain ), $this->ignored[ 'for_secs' ] ); /* * Combine all strings into one error notice. */ $this->p->notice->err( $errors ); } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'ignoring requests to retrieve ' . $url_nofrag ); } } /* * Clear all ignored URLs. * * Returns a count of cleared URLs. */ public function clear_ignored_urls() { $this->maybe_load_ignored(); $count = count( $this->ignored[ 'urls' ] ); $this->ignored[ 'urls' ] = array(); // Clear the ignored URLs. $this->save_ignored_urls(); return $count; } /* * Clear a single ignored URL. * * Returns true if the URL was ignored and is now cleared. */ public function clear_ignored_url( $url_nofrag ) { $this->maybe_load_ignored(); if ( isset( $this->ignored[ 'urls' ][ $url_nofrag ] ) ) { unset( $this->ignored[ 'urls' ][ $url_nofrag ] ); return true; } return false; } public function clear( $url, $cache_pre_ext = '' ) { if ( ! empty( $this->p->debug->enabled ) ) { $this->p->debug->mark(); } $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the fragment. $cache_salt = __CLASS__ . '::get(url:' . $url_nofrag . ')'; $this->clear_ignored_url( $url_nofrag ); $this->clear_cache_data( $cache_salt, $cache_pre_ext, $url ); } /* * Get the formatted microtime it took to retrieve the URL. * * Returns false if retrieving failed, true if the URL was already cached, or the formatted microtime. */ public function get_url_mtime( $url, $precision = 3 ) { if ( isset( $this->url_get_mtimes[ $url ] ) ) { if ( is_bool( $this->url_get_mtimes[ $url ] ) ) { return $this->url_get_mtimes[ $url ]; } else { return sprintf( '%.0' . $precision . 'f', $this->url_get_mtimes[ $url ] ); } } else { return false; } } /* * Get image size for remote URL and cache for 300 seconds (5 minutes) by default. * * If $cache_exp_secs is null, then use the default expiration time. * * If $cache_exp_secs is false, then get but do not save the data. * * Note that PHP v7.1 or better is required to get the image size of WebP images. */ public function get_image_size( $image_url, $cache_exp_secs = 300, $curl_opts = array(), $error_handler = null ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } $file_path = $this->get( $image_url, 'file_path', $cache_type = 'file', $cache_exp_secs, $cache_pre_ext = '', $curl_opts ); if ( ! empty( $file_path ) ) { // False on error. if ( file_exists( $file_path ) ) { if ( null !== $error_handler ) { $previous_error_handler = set_error_handler( $error_handler ); } $image_size = getimagesize( $file_path ); // Note that WebP is only supported since PHP v7.1. if ( null !== $error_handler ) { restore_error_handler(); } if ( $this->p->debug->enabled ) { $this->p->debug->log_arr( 'getimagesize', $image_size ); } return $image_size; } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( $file_path.' does not exist' ); } } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'returned image file path is empty' ); } return false; } /* * Called by SucomForm->get_event_load_json_script(). */ public function get_data_url( $cache_salt, $cache_data, $cache_exp_secs = null, $cache_pre_ext = '' ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } $file_name = md5( $cache_salt ) . $cache_pre_ext; $cache_file = $this->base_dir . $file_name; $cache_url = $this->base_url . $file_name; if ( file_exists( $cache_file ) ) { $file_mod_time = filemtime( $cache_file ); $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs; if ( false !== $file_exp_secs && $file_mod_time > time() - $file_exp_secs ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cached file found: returning url ' . $cache_url ); } $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url ); return $cache_url; } elseif ( @unlink( $cache_file ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'removed expired cache file ' . $cache_file ); } } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'error removing cache file ' . $cache_file ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Error removing cache file %s.', $this->text_domain ), $cache_file ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'cached file not found: creating ' . $cache_url ); } if ( $this->save_cache_data( $cache_salt, $cache_data, $cache_type = 'file', $cache_exp_secs, $cache_pre_ext ) ) { if ( file_exists( $cache_file ) ) { $file_mod_time = filemtime( $cache_file ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache data sucessfully saved' ); } $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url ); return $cache_url; } } return false; } public function get_cache_files_size_mb() { if ( ! $dh = @opendir( $this->base_dir ) ) return false; $cache_files = array(); while ( $file_name = @readdir( $dh ) ) { $cache_file = $this->base_dir . $file_name; if ( ! preg_match( '/^(\..*|index\.php)$/', $file_name ) && is_file( $cache_file ) ) { if ( $file_ext = strtoupper( pathinfo( $cache_file, PATHINFO_EXTENSION ) ) ) { if ( ! isset( $cache_files[ $file_ext ] ) ) { $cache_files[ $file_ext ] = array( 'count' => 0, 'size' => 0 ); } $cache_files[ $file_ext ][ 'count' ]++; $cache_files[ $file_ext ][ 'size' ] += filesize( $cache_file ); } } } closedir( $dh ); foreach ( $cache_files as $ext => &$info ) { $info[ 'size' ] = $info[ 'size' ] / 1024 / 1024; } return $cache_files; } /* * If $cache_exp_secs is null, then use the default expiration time. * * If $cache_exp_secs is false, then get but do not save the data. */ public function get( $url, $cache_format = 'url', $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '', $curl_opts = array() ) { if ( $this->p->debug->enabled ) { $this->p->debug->mark(); } $failure = 'url' === $cache_format ? $url : false; $this->url_get_mtimes[ $url ] = false; // Default value for failure. if ( ! extension_loaded( 'curl' ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'exiting early: curl library is missing' ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = __( 'PHP cURL library missing - contact your hosting provider to have the cURL library installed.', $this->text_domain ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); return $failure; } $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the URL fragment. $url_path = wp_parse_url( $url_nofrag, PHP_URL_PATH ); if ( '' === $cache_pre_ext ) { // Default is an empty string. if ( $cache_pre_ext = pathinfo( $url_path, PATHINFO_EXTENSION ) ) { $cache_pre_ext = '.' . $cache_pre_ext; } } $url_fragment = wp_parse_url( $url, PHP_URL_FRAGMENT ); if ( ! empty( $url_fragment ) ) { $url_fragment = '#' . $url_fragment; } $cache_salt = __CLASS__ . '::get(url:' . $url_nofrag . ')'; $cache_file = $this->base_dir . md5( $cache_salt ) . $cache_pre_ext; $cache_url = $this->base_url . md5( $cache_salt ) . $cache_pre_ext . $url_fragment; $cache_data = false; /* * Return immediately if the cache contains what we need. */ switch ( $cache_format ) { case 'raw': $cache_data = $this->get_cache_data( $cache_salt, $cache_type, $cache_exp_secs, $cache_pre_ext ); if ( false !== $cache_data ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cached data found: returning ' . strlen( $cache_data ) . ' chars' ); } $this->url_get_mtimes[ $url ] = true; // Signal return is from cache. return $cache_data; } break; case 'url': case 'file_path': if ( file_exists( $cache_file ) ) { $file_mod_time = filemtime( $cache_file ); $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs; if ( false !== $cache_exp_secs && $file_mod_time > time() - $file_exp_secs ) { $this->url_get_mtimes[ $url ] = true; // Signal that return is from cache. if ( 'url' === $cache_format ) { $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cached file found: returning ' . $cache_format . ' ' . $cache_url ); } return $cache_url; } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cached file found: returning ' . $cache_format . ' ' . $cache_file ); } return $cache_file; } } elseif ( @unlink( $cache_file ) ) { // Remove expired file. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'removed expired cache file ' . $cache_file ); } } else { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'error removing cache file ' . $cache_file ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Error removing cache file %s.', $this->text_domain ), $cache_file ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } } break; default: if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache return type for ' . $cache_format . ' is unknown' ); } return $failure; } if ( $this->is_ignored_url( $url_nofrag ) ) { // Returns false or time in seconds. return $failure; } /* * Maybe throttle connections for specific hosts, like YouTube for example. */ $this->maybe_throttle_host( $url_nofrag, $curl_opts ); $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $url_nofrag ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $this->curl_connect_timeout ); // 10 seconds (default is 300 seconds). curl_setopt( $ch, CURLOPT_TIMEOUT, $this->curl_timeout ); // 15 seconds (default is no timeout). /* * When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity. Curl * verifies whether the certificate is authentic, i.e. that you can trust that the server is who the * certificate says it is. This trust is based on a chain of digital signatures, rooted in certification * authority (CA) certificates you supply. curl uses a default bundle of CA certificates (the path for that * is determined at build time) and you can specify alternate certificates with the CURLOPT_CAINFO option * or the CURLOPT_CAPATH option. * * See https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html */ curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 1 ); /* * When CURLOPT_SSL_VERIFYHOST is 2, that certificate must indicate that the server is the server to which * you meant to connect, or the connection fails. Simply put, it means it has to have the same name in the * certificate as is in the URL you operate against. * * See https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html */ curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 ); /* * Define and disable the "Expect: 100-continue" header. */ curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Expect:' ) ); if ( ! ini_get( 'safe_mode' ) && ! ini_get( 'open_basedir' ) ) { curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_setopt( $ch, CURLOPT_MAXREDIRS, $this->curl_max_redirs ); } elseif ( $this->p->debug->enabled ) { $this->p->debug->log( 'curl: PHP safe_mode or open_basedir defined - cannot use CURLOPT_FOLLOWLOCATION' ); } /* * Define cURL options from values defined as constants. */ $allowed_curl_const = array( 'CAINFO', 'CONNECTTIMEOUT', 'PROXY', 'PROXYUSERPWD', 'SSLVERSION', 'USERAGENT', ); foreach ( $allowed_curl_const as $const_suffix ) { $curl_const_name = 'CURLOPT_' . $const_suffix; $custom_const_name = $this->plugin_ucid . '_PHP_CURL_' . $const_suffix; if ( defined( $custom_const_name ) ) { curl_setopt( $ch, constant( $curl_const_name ), constant( $custom_const_name ) ); } } /* * Define cURL options provided by the $curl_opts method arguent (empty by default). * * Example: * * $curl_opts = array( * 'CURLOPT_USERAGENT' => 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)', * 'CURLOPT_COOKIELIST' => 'ALL', // Erases all cookies held in memory. * 'CURLOPT_CACHE_THROTTLE' => $throttle_secs, // Internal curl option. * ); */ if ( ! empty( $curl_opts ) ) { foreach ( $curl_opts as $opt_key => $opt_val ) { if ( 0 === strpos( $opt_key, 'CURLOPT_' ) ) { // Just in case. if ( 'CURLOPT_CACHE_THROTTLE' === $opt_key ) continue; // Internal curl option. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'curl: setting custom curl option ' . $opt_key . ' = ' . $opt_val ); } curl_setopt( $ch, constant( $opt_key ), $opt_val ); } } } if ( $this->p->debug->enabled ) { $this->p->debug->log( 'curl: fetching ' . $url_nofrag ); } $mtime_start = microtime( $get_float = true ); $cache_data = curl_exec( $ch ); $mtime_total = microtime( $get_float = true ) - $mtime_start; $curl_errnum = curl_errno( $ch ); $curl_errmsg = curl_error( $ch ); // See https://curl.se/libcurl/c/libcurl-errors.html. $ssl_verify = (int) curl_getinfo( $ch, CURLINFO_SSL_VERIFYRESULT ); // See https://www.php.net/manual/en/function.curl-getinfo.php. $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); $http_success_codes = array( 200, 201 ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'curl: execution time = ' . $mtime_total ); $this->p->debug->log( 'curl: error number = ' . $curl_errnum ); $this->p->debug->log( 'curl: error message = ' . $curl_errmsg ); $this->p->debug->log( 'curl: ssl verify code = ' . $ssl_verify ); $this->p->debug->log( 'curl: http return code = ' . $http_code ); } if ( $curl_errnum || $ssl_verify || ! in_array( $http_code, $http_success_codes ) ) { $this->add_ignored_url( $url_nofrag, $mtime_total, $curl_errnum, $curl_errmsg, $ssl_verify, $http_code ); } else { $this->url_get_mtimes[ $url ] = $mtime_total; if ( empty( $cache_data ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache data returned is empty' ); } } elseif ( false !== $cache_exp_secs ) { // Optimize and check first. if ( $this->save_cache_data( $cache_salt, $cache_data, $cache_type, $cache_exp_secs, $cache_pre_ext ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache data sucessfully saved' ); } } } switch ( $cache_format ) { case 'raw': return $cache_data; case 'url': $file_mod_time = filemtime( $cache_file ); $cache_url = add_query_arg( 'modtime', $file_mod_time, $cache_url ); return $cache_url; case 'file_path': return $cache_file; } } return $failure; } /* * If $cache_exp_secs is null, then use the default expiration time. * * If $cache_exp_secs is false, then get but do not save the data. */ public function get_cache_data( $cache_salt, $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '' ) { $cache_data = false; if ( false === $cache_exp_secs ) { return $cache_data; } if ( $this->p->debug->enabled ) { $this->p->debug->log( $cache_type . ' cache salt ' . $cache_salt ); } $cache_md5_pre = $cache_pre_ext ? $cache_pre_ext : $this->plugin_id . '_'; // Default is an empty string. $cache_id = $cache_md5_pre . md5( $cache_salt ); switch ( $cache_type ) { case 'wp_cache': $cache_data = wp_cache_get( $cache_id, __CLASS__ ); break; case 'transient': $cache_data = get_transient( $cache_id ); break; case 'file': $file_name = md5( $cache_salt ) . $cache_pre_ext; $cache_file = $this->base_dir . $file_name; $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs; if ( ! file_exists( $cache_file ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache file ' . $cache_file . ' does not exist' ); } } elseif ( ! is_readable( $cache_file ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache file ' . $cache_file . ' is not readable' ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Cache file %s is not readable.', $this->text_domain ), $cache_file ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } elseif ( filemtime( $cache_file ) < time() - $file_exp_secs ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( $cache_file . ' is expired' ); } @unlink( $cache_file ); } elseif ( ! $fz = filesize( $cache_file ) ) { // Check if file size is greater then 0. if ( $this->p->debug->enabled ) { $this->p->debug->log( $cache_file . ' is empty' ); } @unlink( $cache_file ); } elseif ( ! $fh = @fopen( $cache_file, 'rb' ) ) { // Check if we get a file handle. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'failed to open the cache file ' . $cache_file . ' for reading' ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Failed to open the cache file %s for reading.', $this->text_domain ), $cache_file ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } else { $cache_data = fread( $fh, $fz ); fclose( $fh ); } break; default: if ( $this->p->debug->enabled ) { $this->p->debug->log( 'unknown cache type: ' . $cache_type ); } break; } if ( $this->p->debug->enabled && false !== $cache_data ) { $this->p->debug->log( 'cache data retrieved from ' . $cache_type ); } return $cache_data; // return data or empty string } /* * If $cache_exp_secs is null, then use the default expiration time. * * If $cache_exp_secs is false, then get but do not save the data. */ public function save_cache_data( $cache_salt, $cache_data = '', $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '' ) { $data_saved = false; if ( empty( $cache_data ) || false === $cache_exp_secs ) { return $data_saved; } $obj_exp_secs = null === $cache_exp_secs ? $this->default_object_cache_exp : $cache_exp_secs; $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs; if ( $this->p->debug->enabled ) { $this->p->debug->log( $cache_type . ' cache salt ' . $cache_salt ); } $cache_md5_pre = $cache_pre_ext ? $cache_pre_ext : $this->plugin_id . '_'; // Default is an empty string. $cache_id = $cache_md5_pre . md5( $cache_salt ); switch ( $cache_type ) { case 'wp_cache': wp_cache_set( $cache_id, $cache_data, __CLASS__, $obj_exp_secs ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache data saved to ' . $cache_type . ' ' . $cache_id . ' (' . $obj_exp_secs . ' seconds)' ); } $data_saved = true; // Success. break; case 'transient': set_transient( $cache_id, $cache_data, $obj_exp_secs ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache data saved to ' . $cache_type . ' ' . $cache_id . ' (' . $obj_exp_secs . ' seconds)' ); } $data_saved = true; // Success. break; case 'file': $file_name = md5( $cache_salt ) . $cache_pre_ext; $cache_file = $this->base_dir . $file_name; if ( ! $file_exp_secs ) { // False or 0. if ( file_exists( $cache_file ) ) { @unlink( $cache_file ); } } elseif ( ! is_dir( $this->base_dir ) && ! mkdir( $this->base_dir ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'failed to create the ' . $this->base_dir . ' cache folder.' ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Failed to create the %s cache folder.', $this->text_domain ), $this->base_dir ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } elseif ( ! is_writable( $this->base_dir ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cache folder ' . $this->base_dir . ' is not writable' ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Cache folder %s is not writable.', $this->text_domain ), $this->base_dir ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } elseif ( ! $fh = @fopen( $cache_file, 'wb' ) ) { // Check if we get a file handle. if ( $this->p->debug->enabled ) { $this->p->debug->log( 'failed to open the cache file ' . $cache_file . ' for writing' ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Failed to open the cache file %s for writing.', $this->text_domain ), $cache_file ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } elseif ( fwrite( $fh, $cache_data ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'data saved to cache file ' . $cache_file ); } fclose( $fh ); $data_saved = true; // Success. } else { // Error writing to cache file. fclose( $fh ); @unlink( $cache_file ); if ( $this->p->debug->enabled ) { $this->p->debug->log( 'failed writing data to cache file ' . $cache_file ); } $notice_pre = sprintf( '%s error:', __METHOD__ ); $notice_msg = sprintf( __( 'Failed writing data to cache file %s.', $this->text_domain ), $cache_file ); $this->p->notice->err( $notice_msg ); SucomUtil::safe_error_log( $notice_pre . ' ' . $notice_msg ); } break; default: if ( $this->p->debug->enabled ) { $this->p->debug->log( 'unknown cache type: ' . $cache_type ); } break; } return $data_saved; // Return true or false. } public function clear_cache_data( $cache_salt, $cache_pre_ext = '', $url = '' ) { if ( 0 !== strpos( $cache_pre_ext, '.' ) ) { // Not a filename extension. $cache_md5_pre = $cache_pre_ext ? $cache_pre_ext : $this->plugin_id . '_'; // Default is an empty string. $cache_id = $cache_md5_pre . md5( $cache_salt ); if ( wp_cache_delete( $cache_id, __CLASS__ ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cleared object cache salt: ' . $cache_salt ); } } if ( delete_transient( $cache_id ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cleared transient cache salt: ' . $cache_salt ); } } } if ( $this->base_dir ) { // Just in case. if ( '' === $cache_pre_ext ) { // Default is an empty string. if ( ! empty( $url ) ) { $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the fragment. $url_path = wp_parse_url( $url_nofrag, PHP_URL_PATH ); if ( $cache_pre_ext = pathinfo( $url_path, PATHINFO_EXTENSION ) ) { $cache_pre_ext = '.' . $cache_pre_ext; } } } $file_name = md5( $cache_salt ) . $cache_pre_ext; $cache_file = $this->base_dir . $file_name; if ( file_exists( $cache_file ) ) { if ( @unlink( $cache_file ) ) { if ( $this->p->debug->enabled ) { $this->p->debug->log( 'cleared local cache file: ' . $cache_file ); } } } } } public function is_cached( $url, $cache_format = 'url', $cache_type = 'file', $cache_exp_secs = null, $cache_pre_ext = '' ) { $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the URL fragment. $url_path = wp_parse_url( $url_nofrag, PHP_URL_PATH ); if ( '' === $cache_pre_ext ) { // Default is an empty string. if ( $cache_pre_ext = pathinfo( $url_path, PATHINFO_EXTENSION ) ) { $cache_pre_ext = '.' . $cache_pre_ext; } } $cache_salt = __CLASS__ . '::get(url:' . $url_nofrag . ')'; $cache_file = $this->base_dir . md5( $cache_salt ) . $cache_pre_ext; $cache_url = $this->base_url . md5( $cache_salt ) . $cache_pre_ext; /* * Return immediately if the cache contains what we need. */ switch ( $cache_format ) { case 'raw': if ( false !== $this->get_cache_data( $cache_salt, $cache_type, $cache_exp_secs, $cache_pre_ext ) ) { return true; } case 'url': case 'file_path': if ( file_exists( $cache_file ) ) { $file_mod_time = filemtime( $cache_file ); $file_exp_secs = null === $cache_exp_secs ? $this->default_file_cache_exp : $cache_exp_secs; if ( false !== $cache_exp_secs && $file_mod_time > time() - $file_exp_secs ) { if ( 'url' === $cache_format ) { $cache_url = add_query_arg( 'moddate', gmdate( 'c', $file_mod_time ), $cache_url ); return $cache_url; } else return $cache_file; } } break; } return false; } /* * Maybe throttle connections for specific hosts, like YouTube for example. */ public function maybe_throttle_host( $url, $curl_opts ) { if ( ! empty( $curl_opts[ 'CURLOPT_CACHE_THROTTLE' ] ) ) { $url_nofrag = preg_replace( '/#.*$/', '', $url ); // Remove the fragment. $url_host = wp_parse_url( $url_nofrag, PHP_URL_HOST ); $cache_md5_pre = $this->plugin_id . '_!_'; // Preserved on clear cache. $cache_salt = __METHOD__ . '(url_host:' . $url_host . ')'; // Throttle by host. $cache_id = $cache_md5_pre . md5( $cache_salt ); while ( $cache_ret = get_transient( $cache_id ) ) { if ( $cache_ret + $curl_opts[ 'CURLOPT_CACHE_THROTTLE' ] > time() ) { sleep( 1 ); continue; } break; } set_transient( $cache_id, time(), $curl_opts[ 'CURLOPT_CACHE_THROTTLE' ] ); } } } }