theme = wp_get_theme();
$this->extensions = apply_filters( 'appcachify_extensions', array( 'jpg', 'jpeg', 'png', 'gif', 'svg', 'xml', 'swf' ) );
if ( file_exists( $this->theme->get_stylesheet_directory() . '/' . $this->offline_file ) )
$this->offline_mode = true;
// filter queued scripts and styles URLs to match our own
foreach( array( 'style', 'script' ) as $type )
add_filter( "{$type}_loader_src", array( $this, 'mod_src' ), 9, 2 );
}
/**
* Handles the appcache request on wp_enqueue_scripts
* Stops the output buffer, modifies headers and
* delivers the manifest page or manifest itself
*
* @return void
*/
public function request() {
global $wp, $post, $wp_query;
// prevent 404 header being sent by caching plugins
$wp_query->is_404 = false;
// prevent output to browser
$this->output_buffer = ob_get_clean();
if ( $wp->request == "manifest" ) {
header( 'HTTP/1.0 200 Ok' );
header( 'Content-type: text/html' );
header( 'Cache-Control: no-cache, must-revalidate' );
$this->manifest_page();
die;
}
if ( $wp->request == "manifest.appcache" ) {
header( 'HTTP/1.0 200 Ok' );
header( 'Content-type: text/cache-manifest' );
header( 'Cache-Control: max-age=3600, must-revalidate' );
$this->manifest();
die;
}
}
/**
* Captures requests to our manifest URLs
*
* @param string $template
*
* @return string
*/
public function template_redirect( $template ) {
global $wp, $post, $wp_query;
if ( is_404() && $wp->request == "offline" && $this->offline_mode ) {
// prevent 404 header being sent by caching plugins
$wp_query->is_404 = false;
header( 'HTTP/1.0 307 Temporary Redirect' );
header( 'Cache-Control: max-age=3600, must-revalidate' );
return get_query_template( '307' );
}
if ( is_404() && in_array( $wp->request, array( 'manifest', 'manifest.appcache' ) ) ) {
// start output buffer so we capture queued scripts
$this->output_buffer = ob_start();
// add late scripts hook to add registered scripts to appcache
add_action( 'wp_enqueue_scripts', array( $this, 'request' ), 783921321 );
}
return $template;
}
/**
* Returns the URL to the manifest page or the manifest itself
*
* @param bool $appcache If true fetches manifest URL
* @param bool $echo Whether to return or echo the URL
*
* @return string Manifest page or manifest URL
*/
public function manifest_url( $appcache = false, $echo = true ) {
$url = get_home_url() . '/manifest' . ( $appcache ? '.appcache' : '' );
if ( $echo )
echo $url;
return $url;
}
/**
* Placeholder page to reference the manifest file
*
* @return void
*/
public function manifest_page() {
echo '
';
}
/**
* Iframe referencing the manifest page
*
* @return void
*/
public function manifest_page_frame() {
echo '';
}
/**
* Attempts to resolve the path to a file from it's URL
*
* @param string $url
*
* @return string|bool File path if successful, false if not
*/
public function get_path_from_url( $url ) {
// remove query string & hash
$url = preg_replace( '/([^\?]+)\?.*$/', '$1', $url );
$url = preg_replace( '/([^\#]+)\#.*$/', '$1', $url );
// is it a local file
if ( strstr( $url, get_home_url() ) ) {
// content url/dir replacement
$file = str_replace( WP_CONTENT_URL, WP_CONTENT_DIR, $url );
if ( file_exists( $file ) )
return $file;
}
// is it from includes
if ( strstr( $url, '/wp-includes/' ) && defined( 'WPINC' ) ) {
$file = str_replace( includes_url(), ABSPATH . WPINC . '/', $url );
if ( file_exists( $file ) )
return $file;
}
return false;
}
/**
* Alters the asset URL ver query string to the modified time of the file to match appcache
*
* @param string $src URL of assets
* @param string $handle Identifier for script/style
*
* @return string
*/
public function mod_src( $src, $handle ) {
if ( $path = $this->get_path_from_url( $src ) ) {
$src = add_query_arg( array( 'ver' => filemtime( $path ) ), remove_query_arg( 'ver', $src ) );
}
return $src;
}
/**
* Returns an array of URLs from a WP_Dependcies instance
*
* @param WP_Dependencies $assets
*
* @return array Array of assets URLs
*/
public function get_assets( WP_Dependencies $assets ) {
$output = array();
foreach( $assets->queue as $handle ) {
$output = array_merge( $this->recurse_deps( $assets, $handle ), $output );
}
return array_filter( array_unique( $output ) );
}
/**
* Used to recurse through asset dependencies
*
* @param WP_Dependencies $assets
* @param string $handle The asset handle
*
* @return array
*/
public function recurse_deps( WP_Dependencies $assets, $handle ) {
$output = array();
$output[ $handle ] = preg_replace( '|^/wp-includes/|', includes_url(), $assets->registered[ $handle ]->src );
foreach( $assets->registered[ $handle ]->deps as $dep ) {
$output = array_merge( $this->recurse_deps( $assets, $dep ), $output );
}
return array_unique( $output );
}
/**
* Generates the actual manifest file content
*
* @return void
*/
public function manifest() {
global $wp_scripts, $wp_styles;
$cache = array();
$network = array();
$fallback = array();
// flag for when to refresh appcached scripts
$assets_updated = 0;
$assets_size = 0;
// get queued js & css
$cache += $this->get_assets( $wp_scripts );
$cache += $this->get_assets( $wp_styles );
if ( $this->offline_mode ) {
$network = array( '*' );
$fallback = array( '/ /offline/' );
}
$src_dir = $this->theme->get_stylesheet_directory();
$src_url = $this->theme->get_stylesheet_directory_uri();
// $assets = $this->process_dir( $src_dir, true );
$assets = $this->theme->get_files( $this->extensions, 10, false );
array_walk( $assets, function( &$item, $relative_path ) use ( $src_url, $src_dir ) {
if ( preg_match( '/screenshot\.(gif|png|jpg|jpeg|bmp)/', $relative_path ) )
$item = false;
else
$item = rtrim( $src_url, '/' ) . '/' . ltrim( $relative_path, '/' );
} );
$cache += $assets;
foreach( array( 'cache', 'network', 'fallback' ) as $section ) {
$$section = array_filter( array_unique( apply_filters( "appcache_{$section}", $$section ) ) );
}
// final cache busting modifications
foreach( $cache as &$url ) {
if ( $filename = $this->get_path_from_url( $url ) ) {
$filemtime = filemtime( $filename );
$assets_updated = $assets_updated < $filemtime ? $filemtime : $assets_updated;
$assets_size += filesize( $filename );
if ( preg_match( '/\.(css|js)$/', basename( $filename ) ) ) {
$url = add_query_arg( array( 'ver' => $filemtime ), $url );
}
}
// non protocol URLs seem to fail, attempt to fetch
if ( preg_match( '/^\/\//', $url ) ) {
$url = set_url_scheme( "https:{$url}", parse_url( $_SERVER[ 'REQUEST_URI' ], PHP_URL_SCHEME ) );
}
// w3tc CDN support
if ( function_exists( 'w3_instance' ) ) {
$dispatcher = w3_instance( 'W3_Dispatcher' );
$domain = $dispatcher->get_cdn_domain();
if ( $domain ) {
$url = str_replace( parse_url( get_home_url(), PHP_URL_HOST ), $domain, $url );
}
}
}
// concatenate URLs
foreach( array( 'cache', 'network', 'fallback' ) as $section ) {
$$section = implode( "\n", $$section );
}
// flag to alter when manifest should be refetched
$update = implode( "\n# ", array_filter( array(
'theme' => 'Theme: ' . $this->theme->get_stylesheet() . ' ' . $this->theme->display( 'version', false ),
'modified' => 'Modified: ' . date( "Y-m-d H:i:s", $assets_updated ),
'size' => 'Size: ' . number_format( $assets_size/1000, 0 ) . 'kb'
) ) );
$update = apply_filters( 'appcache_update_header', $update, $cache, $network, $fallback, $assets_size, $assets_updated );
echo "CACHE MANIFEST
# $update
";
if ( ! empty( $cache ) ) :
echo "
# Explicitly cached master entries.
CACHE:
$cache
";
endif;
if ( ! empty( $network ) ) :
echo "
# Resources that require the user to be online.
NETWORK:
$network
";
endif;
if ( ! empty( $fallback ) ) :
echo "
# Fallback resources if user is offline
FALLBACK:
$fallback
";
endif;
}
/**
* Scans a directory recursively and provides hooks to modify files, folders
* or both
*
* @param string $dir Directory path
* @param bool $recursive Whether to traverse directories recursively
*
* @return array|bool Array of files and directories or false if $dir not found
*/
public function process_dir( $dir, $recursive = false ) {
if ( is_dir( $dir ) ) {
for ( $list = array(), $handle = opendir( $dir ); ( false !== ( $file = readdir( $handle ) ) ); ) {
if ( ( $file != '.' && $file != '..' ) && ( file_exists( $path = $dir . '/' . $file ) ) ) {
if ( is_dir( $path ) && ( $recursive ) ) {
$list = array_merge( $list, $this->process_dir( $path, true ) );
} else {
$entry = array( 'filename' => $file, 'dirpath' => $dir );
$entry = apply_filters( 'process_dir_entry', $entry );
do if ( ! is_dir( $path ) ) {
$entry = apply_filters( 'process_dir_file', $entry );
break;
} else {
$entry = apply_filters( 'process_dir_directory', $entry );
break;
} while ( false );
$list[] = $entry;
}
}
}
closedir( $handle );
return $list;
} else
return false;
}
}
}