Skip to content

Instantly share code, notes, and snippets.

@kovshenin
Created December 27, 2021 15:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kovshenin/818b86c318e8541b30ca0780d24f2449 to your computer and use it in GitHub Desktop.
Save kovshenin/818b86c318e8541b30ca0780d24f2449 to your computer and use it in GitHub Desktop.
<?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