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; } } }