, CodeTRAX.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // Prevent direct access to this file. if ( ! defined( 'ABSPATH' ) ) { header( 'HTTP/1.0 403 Forbidden' ); echo 'This file should not be accessed directly!'; exit; // Exit if accessed directly } // Store plugin directory //define( 'ADDH_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); // Store plugin main file path //define( 'ADDH_PLUGIN_FILE', __FILE__ ); // Helper function that returns an array of supported post types when is_singular() function addh_get_supported_post_types_singular() { $supported_builtin_types = array('post', 'page', 'attachment'); //$public_custom_types = get_post_types( array('public'=>true, '_builtin'=>false, 'show_ui'=>true) ); $public_custom_types = get_post_types( array('public'=>true, '_builtin'=>false) ); $supported_types = array_merge($supported_builtin_types, $public_custom_types); // Allow filtering of the supported content types. $supported_types = apply_filters( 'addh_supported_post_types_singular', $supported_types ); return $supported_types; } // Helper function that returns an array of supported post types when is_archive() function addh_get_supported_post_types_archive() { $supported_builtin_types = array('post'); //$public_custom_types = get_post_types( array('public'=>true, '_builtin'=>false, 'show_ui'=>true) ); $public_custom_types = get_post_types( array('public'=>true, '_builtin'=>false) ); $supported_types = array_merge($supported_builtin_types, $public_custom_types); // Allow filtering of the supported content types. $supported_types = apply_filters( 'addh_supported_post_types_archive', $supported_types ); return $supported_types; } // Send headers to client function addh_send_headers( $headers_arr, $options ) { if ( empty($headers_arr) ) { return; } // First check if headers have already been sent and, if yes, generate friendly warning. if ( headers_sent() ) { $warning_message = 'Add-Headers tried to append its headers to the headers list, but the header list had already been sent to the client by other code. This is not a problem caused by the Add-Headers plugin. Moreover, the Add-Headers plugin can do nothing about this issue. Please investigate which plugin or theme sends the headers earlier than what is expected.'; trigger_error($warning_message, E_USER_WARNING); return; } // Clean up pre-existing headers if ( array_key_exists('remove_pre_existing_headers', $options) && $options['remove_pre_existing_headers'] === true ) { $current_headers = headers_list(); $supported_headers = array('ETag', 'Last-Modified', 'Expires', 'Cache-Control', 'Pragma'); foreach ($current_headers as $current_header ) { foreach ($supported_headers as $supported_header) { if ( strpos($current_header, $supported_header) === false ) { header_remove($supported_header); } } } } // Send the headers foreach ( $headers_arr as $header_name => $header_value ) { if ( ! empty($header_name) && ! empty($header_value) ) { header( sprintf('%s: %s', $header_name, $header_value) ); } } } // ETag // The generated ETag is unique for every post page. In order to generate it // ``$wp->query_vars`` are used among other properties so as to generate a // unique ETag even for archive on which the latest post is the same. function addh_generate_etag_header( $post, $mtime, $options ) { global $wp; if ( $options['add_etag_header'] === true ) { $to_hash = array( $mtime, $post->post_date_gmt, $post->guid, $post->ID, serialize( $wp->query_vars ) ); $header_etag_value = sha1( serialize( $to_hash ) ); // Generate a weak or strong ETag if ( $options['generate_weak_etag'] === true ) { return sprintf( 'W/"%s"', $header_etag_value ); } else { return sprintf( '"%s"', $header_etag_value ); } } } // Last-Modified function addh_generate_last_modified_header( $post, $mtime, $options ) { if ( $options['add_last_modified_header'] === true ) { $header_last_modified_value = str_replace( '+0000', 'GMT', gmdate('r', $mtime) ); return $header_last_modified_value; } } // Expires (Calculated from client access time, aka current time) function addh_generate_expires_header( $post, $mtime, $options ) { if ( $options['add_expires_header'] === true ) { // See also: $current_time_gmt = (int) gmdate('U'); $header_expires_value = str_replace( '+0000', 'GMT', gmdate('r', time() + $options['cache_max_age_seconds'] ) ); return $header_expires_value; } } // Cache-Control function addh_generate_cache_control_header( $post, $mtime, $options ) { if ( $options['add_cache_control_header'] === true ) { if ( intval($options['cache_max_age_seconds']) > 0 ) { $default_cache_control_template = 'public, max-age=%s'; $cache_control_template = apply_filters( 'addh_cache_control_header_format', $default_cache_control_template ); $header_cache_control_value = sprintf( $cache_control_template, $options['cache_max_age_seconds'] ); return $header_cache_control_value; } else { return 'no-cache, must-revalidate, max-age=0'; } } } // Pragma // This header is set to either `no-cache` or `cache` for HTTP 1.0 compatibility. // The same checks take place as for the Cache-Control header. // The addition of this header is controlled by the `add_cache_control_header` option. // No separate option should be required for this header. function addh_generate_pragma_header( $post, $mtime, $options ) { if ( $options['add_cache_control_header'] === true ) { if ( intval($options['cache_max_age_seconds']) > 0 ) { return 'public'; } else { return 'no-cache'; } } } /** * Generates headers in batch */ function addh_batch_generate_headers( $post, $mtime, $options ) { $headers_arr = array(); // ETag $headers_arr['ETag'] = addh_generate_etag_header( $post, $mtime, $options ); // Last-Modified $headers_arr['Last-Modified'] = addh_generate_last_modified_header( $post, $mtime, $options ); // Expires (Calculated from client access time, aka current time) $headers_arr['Expires'] = addh_generate_expires_header( $post, $mtime, $options ); // Cache-Control $headers_arr['Cache-Control'] = addh_generate_cache_control_header( $post, $mtime, $options ); // Pragma $headers_arr['Pragma'] = addh_generate_pragma_header( $post, $mtime, $options ); // Allow filtering of the generated headers $headers_arr = apply_filters( 'addh_headers', $headers_arr ); // Send headers addh_send_headers( $headers_arr, $options ); } /** * Sets headers on post object pages (posts, pages, attachments, custom * post types). * * In order to calculate the modified time, two time sources are used: * 1) the post object's modified time. * 2) the modified time of the most recent comment that is attached to the post object. * The most "recent" timestamp of the two is returned. */ function addh_set_headers_for_object( $options ) { // Get current queried object. $post = get_queried_object(); // Valid post types: post, page, attachment, public custom post types if ( ! is_object($post) || ! isset($post->post_type) || ! in_array( get_post_type($post), addh_get_supported_post_types_singular() ) ) { return; } // Check for password protected posts if ( post_password_required() ) { return; } // Retrieve stored time of post object $post_mtime = $post->post_modified_gmt; $post_mtime_unix = strtotime( $post_mtime ); // Initially set the $mtime to the post mtime timestamp $mtime = $post_mtime_unix; // If there are comments attached to this post object, find the mtime of // the most recent comment. if ( intval($post->comment_count) > 0 ) { // Retrieve the mtime of the most recent comment $comments = get_comments( array( 'status' => 'approve', 'orderby' => 'comment_date_gmt', 'number' => '1', 'post_id' => $post->ID ) ); if ( ! empty($comments) ) { $comment = $comments[0]; $comment_mtime = $comment->comment_date_gmt; $comment_mtime_unix = strtotime( $comment_mtime ); // Compare the two mtimes and keep the most recent (higher) one. if ( $comment_mtime_unix > $post_mtime_unix ) { $mtime = $comment_mtime_unix; } } } // Check for old content (only if a threshold is set) if ( absint($options['cache_old_content_threshold_seconds']) > 0 ) { // Find the time that determines whether a resource should be treated as 'old content'. // Basically this is the current GM time minus the 'old content threshold in seconds' as set in the option. $threshold_time_unix = absint(gmdate('U')) - absint($options['cache_old_content_threshold_seconds']); // Any content which has been last modified before that time should be treated as old // and the 'cache_old_content_max_age_seconds' option should be used as the caching // timeout, instead of 'cache_max_age_seconds'. // Here we check whether the mtime (last modified time) is older than the threshold time. if ( $mtime < $threshold_time_unix ) { // This content is old. Set the caching timeout to the caching timeout of old content. $options['cache_max_age_seconds'] = $options['cache_old_content_max_age_seconds']; } } addh_batch_generate_headers( $post, $mtime, $options ); } /** * Sets headers on archives */ function addh_set_headers_for_archive( $options ) { // WordPress archives list the posts that belong to the archive from // newest to oldest. We set the HTTP headers for the archive page based // on the first post of the archive (newest). // There is no need to check for pagination, since every page of the archive // has different posts. //global $post; // Using this is possibly a mistake. This should be term/author/date object of the archive. // Get our post object from the list of posts. global $posts; if ( empty($posts) ) { return; } $post = $posts[0]; // The post object we use for the HTTP headers is the latest post. // Here it is possible to filter this post object and use what you want. $post = apply_filters( 'addh_archive_post', $post ); // Valid post types: post if ( ! is_object($post) || ! isset($post->post_type) || ! in_array( get_post_type($post), addh_get_supported_post_types_archive() ) ) { return; } // Retrieve stored time of post object $post_mtime = $post->post_modified_gmt; $mtime = strtotime( $post_mtime ); // Check for old content (only if a threshold is set) if ( absint($options['cache_old_content_threshold_seconds']) > 0 ) { // Find the time that determines whether a resource should be treated as 'old content'. // Basically this is the current GM time minus the 'old content threshold in seconds' as set in the option. $threshold_time_unix = absint(gmdate('U')) - absint($options['cache_old_content_threshold_seconds']); // Any content which has been last modified before that time should be treated as old // and the 'cache_old_content_max_age_seconds' option should be used as the caching // timeout, instead of 'cache_max_age_seconds'. // Here we check whether the mtime (last modified time) is older than the threshold time. if ( $mtime < $threshold_time_unix ) { // This content is old. Set the caching timeout to the caching timeout of old content. $options['cache_max_age_seconds'] = $options['cache_old_content_max_age_seconds']; } } addh_batch_generate_headers( $post, $mtime, $options ); } /** * Main function. */ function addh_headers(){ // Early checks // // Note: These might not be necessary since conditional tags possibly // exclude any processing in the following cases. // if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { // Check for AJAX call. return; } elseif( defined('XMLRPC_REQUEST') && XMLRPC_REQUEST ) { // Check for XMLRPC request. return; } elseif( defined('REST_REQUEST') && REST_REQUEST ) { // Check for REST request. return; } elseif ( is_admin() ) { // Check if we are in the WP admin interface. return; } // Notes // // 1. WordPress by default generates the ETag and Last-Modified headers for feeds. // If 'add_etag_header' and/or 'add_last_modified_header' has been set to 'false', // then an ETag header and/or a Last-Modified header might appear in the response. // These are not added by Add-Headers, but by WordPress. To get rid of them, // you will have to enable the 'remove_pre_existing_headers' option, // which deletes any of the headers (supported by this plugin) before adding its own. // Options $default_options = array( 'add_etag_header' => true, 'generate_weak_etag' => false, 'add_last_modified_header' => true, 'add_expires_header' => true, 'add_cache_control_header' => true, 'cache_max_age_seconds' => 0, 'cache_max_age_seconds_for_search_results' => 0, 'cache_max_age_seconds_for_authenticated_users' => 0, 'cache_old_content_threshold_seconds' => 0, 'cache_old_content_max_age_seconds' => 0, 'remove_pre_existing_headers' => false, ); $options = apply_filters( 'addh_options', $default_options ); // Adjust `cache_max_age_seconds` for authenticated users. if ( is_user_logged_in() ) { $options['cache_max_age_seconds'] = $options['cache_max_age_seconds_for_authenticated_users']; } // Feeds // Note: At the time of writing, WordPress 3.8 feeds already have ETag and // Last-Modified headers. Here we reprocess them and add our own. // One problem with the default feed ETag, it is the same for RSS and Atom // and possibly other feed types, which is not the right way to do it. if ( is_feed() ) { // Process feeds as archives addh_set_headers_for_archive( $options ); } // Adds headers to: // - Post objects (posts, pages, attachments, custom post types) // - Static front page elseif ( is_singular() ) { addh_set_headers_for_object( $options ); } // Adds headers to: // - Category, Tag, other Taxonomy Term, custom post type archive, Author and Date-based archives. // - Search results // - Default front page displaying the latest posts // - Static page displaying the latest posts elseif ( is_archive() || is_search() || is_home() ) { if ( is_search() ) { $options['cache_max_age_seconds'] = $options['cache_max_age_seconds_for_search_results']; } addh_set_headers_for_archive( $options ); } } // Action hooks - order of running // * https://codex.wordpress.org/Plugin_API/Action_Reference // Also see action hooks: // * https://codex.wordpress.org/Plugin_API/Action_Reference/send_headers // * https://codex.wordpress.org/Plugin_API/Action_Reference/wp // * https://codex.wordpress.org/Plugin_API/Action_Reference/template_redirect // * https://codex.wordpress.org/Plugin_API/Action_Reference/posts_selection // // http://wordpress.stackexchange.com/a/20193 add_action('template_redirect', 'addh_headers');