Created
December 27, 2021 15:02
-
-
Save kovshenin/818b86c318e8541b30ca0780d24f2449 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Pressjitsu Remote HTTP Cache | |
* | |
* Many plugins like to perform remote HTTP requests for front-end visits, | |
* sometimes without even employing transient caching. We think that *all* HTTP | |
* requests should run in the background, and thus be always served from cache, | |
* even if the cache is stale. | |
* | |
* Configure cachable hosts via the pj_http_cache_hosts filter. | |
*/ | |
class Pj_Remote_Http_Cache { | |
/** | |
* An array of expired request keys. | |
*/ | |
private static $expired; | |
/** | |
* An array of request keys to ignore (avoids recursion) | |
*/ | |
private static $ignore; | |
/** | |
* An array of hosts to cache. | |
*/ | |
private static $hosts; | |
/** | |
* Seconds before a cached item is considered expired. | |
*/ | |
private static $ttl; | |
/** | |
* Runs immediately when plugin is parsed. | |
*/ | |
public static function load() { | |
if ( ! empty( $_GET['pj-http-cache-update'] ) ) { | |
self::update(); | |
die(); | |
} | |
self::$expired = array(); | |
self::$ignore = array(); | |
self::$ttl = apply_filters( 'pj_http_cache_ttl', 10 ); | |
self::$hosts = apply_filters( 'pj_http_cache_hosts', array( | |
'connect.garmin.com', | |
) ); | |
add_action( 'pre_http_request', array( __CLASS__, 'pre_http_request' ), 10, 3 ); | |
add_action( 'init', array( __CLASS__, 'schedule_events' ) ); | |
} | |
/** | |
* Create a key from a request URL and WP_Http arguments array. | |
* | |
* @param string $url Request URL passed to wp_remote_* functions. | |
* @param array $args Request arguments passed to wp_remote_*. | |
* | |
* @return string The resulting hash/key. | |
*/ | |
private static function get_key( $url, $args ) { | |
return md5( $url . '::' . serialize( $args ) ); | |
} | |
/** | |
* Filters HTTP requests and servers from cache if available. | |
* | |
* @param mixed $return False to proceed with the request as intended, return immediately otherwise. | |
* @param array $args Request arguments passed to wp_remote_* functions. | |
* @param string $url The request URL passed to wp_remote_*. | |
* | |
* @return mixed False to proceed with a request as it normally would, or an already existing response from cache. | |
*/ | |
public static function pre_http_request( $return, $args, $url ) { | |
// Don't cache requests when doing AJAX. | |
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) | |
return $return; | |
// Don't cache requests when doing cron. | |
if ( defined( 'DOING_CRON' ) && DOING_CRON ) | |
return $return; | |
$host = parse_url( $url, PHP_URL_HOST ); | |
if ( ! in_array( $host, self::$hosts ) ) | |
return false; | |
$key = self::get_key( $url, $args ); | |
// Check the array to avoid recursion, since with a cache miss this function | |
// will (actually) fire a remote request to cache it. | |
if ( in_array( $key, self::$ignore ) ) | |
return false; | |
$cache_key = 'pj-http-cache-' . $key; | |
$cache = get_option( $cache_key, false ); | |
// Response is not in cache. | |
if ( false === $cache ) { | |
self::$ignore[] = $key; | |
$response = wp_remote_request( $url, $args ); | |
add_option( $cache_key, array( 'timestamp' => time(), 'response' => $response ), '', 'no' ); | |
return $response; | |
} | |
// Response is in cache. | |
$response = $cache['response']; | |
// Cache has expired, so spawn an update. | |
if ( time() - $cache['timestamp'] > self::$ttl ) { | |
if ( empty( self::$expired ) ) | |
add_action( 'shutdown', array( __CLASS__, 'spawn_update' ) ); | |
self::$expired[ $key ] = array( 'url' => $url, 'args' => $args ); | |
} | |
return $response; | |
} | |
/** | |
* Spawn cache update, runs during shutdown, runs an HTTP request that | |
* runs all the other HTTP requests. | |
*/ | |
public static function spawn_update() { | |
if ( empty( self::$expired ) ) | |
return; | |
// Add keys in need of a refreshment. | |
foreach ( self::$expired as $key => $data ) { | |
$update_key = 'pj-http-up-' . $key; | |
// Already updating this one. | |
if ( false !== get_transient( $update_key ) ) { | |
unset( self::$expired[ $key ] ); | |
continue; | |
} | |
$timeout = isset( $data['timeout'] ) ? $data['timeout'] + 10 : 10; | |
set_transient( $update_key, $data, $timeout ); | |
} | |
// Anything left to refresh? | |
if ( empty( self::$expired ) ) | |
return; | |
// Booyah! | |
$url = home_url( '/?pj-http-cache-update=1' ); | |
wp_remote_post( $url, array( 'timeout' => 0.01, 'blocking' => false, 'body' => array( | |
'keys' => array_keys( self::$expired ), | |
) ) ); | |
} | |
/** | |
* Runs in the background, updates requests caches by given keys. | |
*/ | |
public static function update() { | |
if ( empty( $_POST['keys'] ) || ! is_array( $_POST['keys'] ) ) | |
return; | |
$keys = wp_unslash( $_POST['keys'] ); | |
foreach ( $keys as $key ) { | |
$cache_key = 'pj-http-cache-' . $key; | |
$update_key = 'pj-http-up-' . $key; | |
$data = get_transient( $update_key ); | |
if ( false === $data ) | |
continue; | |
// Bogus data, where did it come from? | |
if ( empty( $data['url'] ) || empty( $data['args'] ) ) { | |
delete_transient( $update_key ); | |
continue; | |
} | |
// Perform the request and update the cached response. | |
$response = wp_remote_request( $data['url'], $data['args'] ); | |
update_option( $cache_key, array( 'timestamp' => time(), 'response' => $response ) ); | |
delete_transient( $update_key ); | |
} | |
} | |
/** | |
* Runs during init, schedules events and event actions. | |
*/ | |
public static function schedule_events() { | |
if ( ! wp_next_scheduled( 'pj_http_cache_gc' ) ) | |
wp_schedule_event( time(), 'hourly', 'pj_http_cache_gc' ); | |
add_action( 'pj_http_cache_gc', array( __CLASS__, 'gc' ) ); | |
} | |
/** | |
* Garbage Collection. | |
* | |
* We don't want cached requests to live forever, especially when developers are | |
* smart and add random numbers to their requests. Gargabe collection clears up the | |
* database of cached requests that last occurred over 24 hours ago. | |
*/ | |
public static function gc() { | |
global $wpdb; | |
$delete = array(); | |
$options = $wpdb->get_col( "SELECT `option_name` FROM `$wpdb->options` WHERE `option_name` | |
LIKE 'pj-http-cache-%' ORDER BY `option_id` ASC LIMIT 100;" ); | |
foreach ( $options as $option_name ) { | |
if ( ! preg_match( '#^pj-http-cache-(.+)$#', $option_name, $matches ) ) | |
continue; | |
$key = $matches[1]; | |
$value = get_option( $option_name, false ); | |
if ( ! empty( $value['timestamp'] ) && time() - $value['timestamp'] > 24 * HOUR_IN_SECONDS ) | |
$delete[] = $key; | |
} | |
// Delete all these options and transients. | |
foreach ( $delete as $key ) { | |
delete_option( 'pj-http-cache-' . $key ); | |
delete_transient( 'pj-http-up-' . $key ); | |
} | |
} | |
} | |
Pj_Remote_Http_Cache::load(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment