Tag: wordpress

  • Query Caching (and a little extra)

    Query Caching (and a little extra)

    By default, WordPress does not cache WP_Query queries.  Doing so can greatly improve performance.  The way I do this is via the Advanced Post Cache plugin:

    By running this plugin (hopefully as an mu-plugin) with a persistent object cache, WP_Query calls, along with get_post() calls (only if suppress_filters is false) will be cached.

    Bonus!

    Now that we’re caching queries, here’s how I do a little extra caching to squeeze out a tiny bit more performance:

    <?php
    // By default Jetpack does not cache responses from Instagram oembeds.
    add_filter( 'instagram_cache_oembed_api_response_body', '__return_true' );
    
    // Cache WP Dashboard Recent Posts Query
    add_filter( 'dashboard_recent_posts_query_args', 'cache_dashboard_recent_posts_query_args', 10, 1 );
    function cache_dashboard_recent_posts_query_args( $query_args ) {
    	$query_args['cache_results'] = true;
    	$query_args['suppress_filters'] = false;
    	return $query_args;
    }
    
    // Cache WP Dashboard Recent Drafts Query
    add_filter( 'dashboard_recent_drafts_query_args', 'cache_dashboard_recent_drafts_query_args', 10, 1 );
    function cache_dashboard_recent_drafts_query_args( $query_args ) {
    	$query_args['suppress_filters'] = false;
    	return $query_args;
    }
    
    // Cache comment counts, https://github.com/Automattic/vip-code-performance/blob/master/core-fix-comment-counts-caching.php
    add_filter( 'wp_count_comments', 'wpcom_vip_cache_full_comment_counts', 10, 2 );
    function wpcom_vip_cache_full_comment_counts( $counts = false , $post_id = 0 ){
    	//We are only caching the global comment counts for now since those are often in the millions while the per page one is usually more reasonable.
    	if ( 0 !== $post_id ) {
    		return $counts;
    	}
    
    	$cache_key = "vip-comments-{$post_id}";
    	$stats_object = wp_cache_get( $cache_key );
    
    	//retrieve comments in the same way wp_count_comments() does
    	if ( false === $stats_object ) {
    		$stats = get_comment_count( $post_id );
    		$stats['moderated'] = $stats['awaiting_moderation'];
    		unset( $stats['awaiting_moderation'] );
    		$stats_object = (object) $stats;
    
    		wp_cache_set( $cache_key, $stats_object, 'default', 30 * MINUTE_IN_SECONDS );
    	}
    
    	return $stats_object;
    }
    
    // Cache monthly media array.
    add_filter( 'media_library_months_with_files', 'wpcom_vip_media_library_months_with_files' );
    function wpcom_vip_media_library_months_with_files() {
    	$months = wp_cache_get( 'media_library_months_with_files', 'extra-caching' );
    
    	if ( false === $months ) {
    		global $wpdb;
    		$months = $wpdb->get_results( $wpdb->prepare( "
    			SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
    			FROM $wpdb->posts
    			WHERE post_type = %s
    			ORDER BY post_date DESC
    			", 'attachment' )
    		);
    		wp_cache_set( 'media_library_months_with_files', $months, 'extra-caching' );
    	}
    
    	return $months;
    }
    
    add_action( 'add_attachment', 'media_library_months_with_files_bust_cache' );
    function media_library_months_with_files_bust_cache( $post_id ) {
    	if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
    		return;
    	}
    
    	// What month/year is the most recent attachment?
    	global $wpdb;
    	$months = $wpdb->get_results( $wpdb->prepare( "
    			SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
    			FROM $wpdb->posts
    			WHERE post_type = %s
    			ORDER BY post_date DESC
    			LIMIT 1
    		", 'attachment' )
    	);
    
    	// Simplify by assigning the object to $months
    	$months = array_shift( array_values( $months ) );
    
    	// Compare the dates of the new, and most recent, attachment
    	if (
    		! $months->year == get_the_time( 'Y', $post_id ) &&
    		! $months->month == get_the_time( 'm', $post_id )
    	) {
    		// the new attachment is not in the same month/year as the
    		// most recent attachment, so we need to refresh the transient
    		wp_cache_delete( 'media_library_months_with_files', 'extra-caching' );
    	}
    }Code language: HTML, XML (xml)

  • Auto-enable WP_DEBUG with a cookie

    Auto-enable WP_DEBUG with a cookie

    One of the most important things to do when working on new themes, plugins, or debugging issues in WordPress is to turn on WP_DEBUG.  According to the Codex:

    WP_DEBUG is a PHP constant (a permanent global variable) that can be used to trigger the “debug” mode throughout WordPress. It is assumed to be false by default and is usually set to true in the wp-config.php file on development copies of WordPress.

    It’s common to edit the wp-config.php file every time you want to turn this off and on, but another way to do it is via a secret cookie:

    if ( isset( $_COOKIE[ 'wp_secret_debug_cookie' ] ) ) {
    	define( 'WP_DEBUG', true );
    }Code language: PHP (php)

    I also like to pair my WP_DEBUG with these extra settings:

    if ( defined( 'WP_DEBUG' ) && true === WP_DEBUG ) {
    	define( 'WP_DEBUG_LOG', true );
    	define( 'SCRIPT_DEBUG', true );
    	define( 'WP_DEBUG_DISPLAY', true );
    	define( 'CONCATENATE_SCRIPTS', false );
    	define( 'SAVEQUERIES', true );
    }Code language: JavaScript (javascript)

    I set the cookie in an mu-plugin like this (Note: This code could definitely be improved)

    function emrikol_admin_debug() {
    	if ( is_super_admin() ) {
    		setcookie( 'wp_secret_debug_cookie', 'on', time() + 86400, '/', wp_parse_url( get_site_url(), PHP_URL_HOST ) );
    		setcookie( 'wp_secret_debug_cookie', 'on', time() + 86400, '/', wp_parse_url( get_home_url(), PHP_URL_HOST ) );
    		setcookie( 'wp_secret_debug_cookie', 'on', time() + 86400, '/', wp_parse_url( get_admin_url(), PHP_URL_HOST ) );
    
    		// Allow sites to set extra domains to add cookie to--good for subdomain multisite.
    		$extra_domains = apply_filters( 'emrikol_admin_debug_domain', false );
    		if ( false !== $extra_domains ) {
    			if ( is_array( $extra_domains ) ) {
    				foreach ( $extra_domains as $extra_domain ) {
    					setcookie( 'wp_secret_debug_cookie', 'on', time() + 86400, '/', $extra_domain );
    				}
    			} else {
    					setcookie( 'wp_secret_debug_cookie', 'on', time() + 86400, '/', $extra_domains );
    			}
    		}
    	}
    }
    add_action( 'init', 'emrikol_admin_debug', 10 );
    add_action( 'admin_init', 'emrikol_admin_debug', 10 );Code language: PHP (php)
  • Gutenberg, Code, and Highlighting

    Gutenberg, Code, and Highlighting

    One of the great things about Gutenberg is the ability to compartmentalize different types of content within blocks.  One of the blocks that I’ve been using a lot of recently is the code block.  This block by default will render something like this:

    #include "stdio.h"
    int main()
    {
       // printf() displays the string inside quotation
       printf("Hello, World!");
       return 0;
    }Code language: PHP (php)

    While this is acceptable, it’s not very pretty.  I used to use the SyntaxHighlighter EvolvedUnfortunately this doesn’t work perfectly with Gutenberg at the moment, and I was hoping for something in a block.  Luckily I found this…

    UPDATE: SynxtaxHighlighter Evolved now works with Gutenberg, but I still like how code-syntax-block works with the core block and isn’t a block of its own.

    Marcus Kazmierczak has made a plugin to extend the core code block to allow syntax highlighting:

    #include <stdio.h>
    int main()
    {
       // printf() displays the string inside quotation
       printf("Hello, World!");
       return 0;
    }Code language: PHP (php)

    I really like this and I think it compliments Gutenberg nicely 🙂

  • Securing WordPress Plugins with more Plugins

    Securing WordPress Plugins with more Plugins

    I’ve written before about disabling plugin deactivation in WordPress, but I’ve finally used that knowledge in practice–on this site.

    The Problem

    Let’s say you’re going along your day, developing things, and fixing things, and making the world a better place when all of a sudden you get a call from a client that their website is broken!!  After a bit of panicking and digging around, it turns out that they’ve been “optimizing” things by disabling random, but critical, plugins on the site.

    The Solution

    One way that you might fix this is to not install the plugins via the WordPress UI and require() them either in the theme directory, or as an mu-plugin.  The downside with this is that you lose the ability to easily and auto-update the plugins (if you’re okay with that).  You also the ability to easily see what plugins are active and installed in the admin UI.

    The way I’ve got around this is to create this helper function inside an mu-plugin that allows the plugins to be installed and managed in the UI, but not disabled:

    <?php
    /**
     * Secures a plugin from accidental disabling in the UI.
     *
     * If a plugin is necessary for a site to function, it should not be disabled.
     * This functionc can also optionally "force" activate a plugin without having to
     * activate it in the plugin UI.  Forcing activation will cause it to skip all
     * core plugin activation hooks.
     *
     * @param string  $plugin              Plugin file to secure.
     * @param boolean $force_activation    Optional. Whether to force load the plugin. Default false.
     */
    function emrikol_secure_plugin( $plugin, $force_activation = false ) {
    	$proper_plugin_name = false;
    
    	// Match if properly named: wp-plugin (wp-plugin/wp-plugin.php).
    	if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin . '/' . $plugin . '.php' ) && is_file( WP_PLUGIN_DIR . '/' . $plugin . '/' . $plugin . '.php' ) ) {
    		$proper_plugin_name = $plugin . '/' . $plugin . '.php';
    	} else {
    		// Match if improperly named: wp-plugin/cool-plugin.php.
    		if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin ) && is_file( WP_PLUGIN_DIR . '/' . $plugin ) ) {
    			$proper_plugin_name = $plugin;
    		}
    	}
    
    	if ( false !== $proper_plugin_name ) {
    		if ( true === $force_activation ) {
    			// Always list the plugin as active.
    			add_filter( 'option_active_plugins', function( $active_plugins ) use ( $proper_plugin_name ) {
    				// Crappy hack to prevent infinite loops.  Surely there's a better way.
    				global $emrikol_is_updating_active_plugins;
    
    				if ( true === $emrikol_is_updating_active_plugins ) {
    					unset( $emrikol_is_updating_active_plugins );
    					return array_unique( $active_plugins );
    				}
    
    				if ( ! in_array( $proper_plugin_name, $active_plugins, true ) ) {
    					$active_plugins[]                   = $proper_plugin_name;
    					$emrikol_is_updating_active_plugins = true;
    
    					update_option( 'active_plugins', array_unique( $active_plugins ) );
    				}
    				return array_unique( $active_plugins );
    			}, 1000, 1 );
    		}
    
    		// Ensure the plugin doesn't get disabled somehow.
    		// TODO: Diff arrays.  Only run if the plugin is being removed.
    		add_filter( 'pre_update_option_active_plugins', function ( $active_plugins ) use ( $proper_plugin_name ) {
    			if ( ! in_array( $proper_plugin_name, $active_plugins, true ) ) {
    				$active_plugins[] = $proper_plugin_name;
    			}
    			return array_unique( $active_plugins );
    		}, 1000, 1 );
    
    		// Remove the disable button.
    		$plugin_basename = plugin_basename( $proper_plugin_name );
    		add_filter( "plugin_action_links_$plugin_basename", function( $links ) use ( $proper_plugin_name, $force_activation ) {
    			if ( isset( $links['deactivate'] ) ) {
    				$links['deactivate'] = sprintf(
    					'<span class="emrikol-secure-plugin wp-ui-text-primary">%s</span>',
    					$force_activation ? 'Plugin Activated via Theme Code' : 'Plugin Secured via Theme Code'
    				);
    			}
    			return $links;
    		}, 1000, 1 );
    	}
    }
    Code language: HTML, XML (xml)

    It’s not perfect, but it’s working for me right now.  Like, right now on this site as you’re reading this. I’ve added it as an mu-plugin like so:

    <?php
    require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/secure-plugins.php' );
    
    emrikol_secure_plugin( 'akismet' );
    emrikol_secure_plugin( 'amp' );
    emrikol_secure_plugin( 'jetpack' );
    emrikol_secure_plugin( 'wp-super-cache/wp-cache.php' );Code language: HTML, XML (xml)

    As you can see, this completely removes the “Deactivate” link in the UI:

    The emrikol_secure_plugin() function takes two arguments:

    • The plugin to secure.  This can either be the plugin slug (ex. jetpack) or the full plugin path if the plugin doesn’t follow standard naming conventions (wp-super-cache/wp-cache.php)
    • A boolean, defaults to false.  If it is true the plugin will be forced to activate without user intervention.  This can be used to activate a plugin on a new install without having to manually enable it in the UI or via WP-CLI
  • CSS & JS Concatenation in WordPress

    CSS & JS Concatenation in WordPress

    At WordPress.com VIP one of the features we have on our platform is automated concatenation of Javascript and CSS files when registered through the core WordPress wp_enqueue__*() functions.

    We do this using the nginx-http-concat plugin:

    This plugin was written to work with nginx, but the server running derrick.blog is Apache.  I’ve worked around this and have nginx-http-concat running fully in WordPress, with added caching.

    The bulk of the plugin is this file, which does all of the work of caching and calling the nignx-http-concat plugin:

    <?php
    // phpcs:disable WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.ValidatedSanitizedInput, WordPress.VIP.FileSystemWritesDisallow, WordPress.VIP.RestrictedFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, WordPress.WP.AlternativeFunctions.json_encode_json_encode
    if ( isset( $_SERVER['REQUEST_URI'] ) && '/_static/' === substr( $_SERVER['REQUEST_URI'], 0, 9 ) ) {
    	$cache_file      = WP_HTTP_CONCAT_CACHE . '/' . md5( $_SERVER['REQUEST_URI'] );
    	$cache_file_meta = WP_HTTP_CONCAT_CACHE . '/meta-' . md5( $_SERVER['REQUEST_URI'] );
    
    	if ( file_exists( $cache_file ) ) {
    		if ( time() - filemtime( $cache_file ) > 2 * 3600 ) {
    			// file older than 2 hours, delete cache.
    			unlink( $cache_file );
    			unlink( $cache_file_meta );
    		} else {
    			// file younger than 2 hours, return cache.
    			if ( file_exists( $cache_file_meta ) ) {
    				$meta = json_decode( file_get_contents( $cache_file_meta ) );
    				if ( null !== $meta && isset( $meta->headers ) ) {
    					foreach ( $meta->headers as $header ) {
    						header( $header );
    					}
    				}
    			}
    			$etag = '"' . md5( file_get_contents( $cache_file ) ) . '"';
    
    			ob_start( 'ob_gzhandler' );
    			header( 'x-http-concat: cached' );
    			header( 'Cache-Control: max-age=' . 31536000 );
    			header( 'ETag: ' . $etag );
    
    			if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
    				if ( strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) < filemtime( $cache_file ) ) {
    					header( 'HTTP/1.1 304 Not Modified' );
    					exit;
    				}
    			}
    
    			echo file_get_contents( $cache_file ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
    			$output = ob_get_clean();
    			echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
    			die();
    		}
    	}
    	ob_start( 'ob_gzhandler' );
    	require_once 'nginx-http-concat/ngx-http-concat.php';
    
    	$output = ob_get_clean();
    	$etag  = '"' . md5( file_get_contents( $output ) ) . '"';
    	$meta   = array(
    		'headers' => headers_list(),
    	);
    
    	header( 'x-http-concat: uncached' );
    	header( 'Cache-Control: max-age=' . 31536000 );
    	header( 'ETag: ' . $etag );
    
    	if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
    		if ( strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) < filemtime( $cache_file ) ) {
    			header( 'HTTP/1.1 304 Not Modified' );
    			exit;
    		}
    	}
    
    	file_put_contents( $cache_file, $output );
    	file_put_contents( $cache_file_meta, json_encode( $meta ) );
    	echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
    	die();
    }
    Code language: HTML, XML (xml)

    This little bit of code in wp-config.php is what calls the above file, before WordPress even initializes, to make this as speedy as possible:

    define( 'WP_HTTP_CONCAT_CACHE', dirname(__FILE__) . '/wp-content/cache/http-concat-cache' );
    require_once dirname(__FILE__) . '/wp-content/mu-plugins/emrikol-defaults/config-nginx-http-concat.php';Code language: PHP (php)

    Finally, in an mu-plugin these lines enable the nginx-http-concat plugin:

    require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/nginx-http-concat/cssconcat.php' );
    	require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/nginx-http-concat/jsconcat.php' );Code language: PHP (php)

    All of this could definitely be packed into a legit plugin, and even leave room for other features, such as:

    • An admin UI for enabling/disabling under certain condition
    • A “clear cache” button
    • A cron event to regularly delete expired cache items

    As it is now though, I’m just leaving it be to see how well it works.  Wish me luck 🙂

  • New Beginning

    I’m trying to force myself to do a few new things:

    • Blog on a schedule
    • Learn new stuff

    Because of that, I’m starting over on derrick.blog and playing around here.  I’ll be testing new WordPress stuff here, especially Gutenberg.

  • Gutenberg and Markdown

    Gutenberg and Markdown

    Update:

    Jetpack now has its own built-in Markdown block:

    I’d highly recommend using it instead 🙂


    Original post below:

    I’ve been playing around with Gutenberg a tiny bit recently and have realized that, at least in my case, it kind of eats the Jetpack Markdown module and doesn’t offer full markdown support.

    I honestly know nothing about writing blocks for Gutenberg, but luckily GitHub user nuzzio does 🙂

    This recent, but now closed PR to Jetpack was pretty much everything necessary to build a working block.

    I’ve put my code up on GitHub as a WordPress plugin:

    Like I said, I don’t know what I’m doing–so consider this plugin super beta.

  • Disabling plugin deactivation in WordPress

    Disabling plugin deactivation in WordPress

    The problem came up recently about how to make sure plugins activated in the WordPress plugin UI don’t get deactivated if they are necessary for a site to function.  I thought that was an interesting thought puzzle worth spending 15 minutes on, so I came up with this function as a solution:

    function dt_force_plugin_active( $plugin ) {
    add_filter( 'pre_update_option_active_plugins', function ( $active_plugins ) use ( $plugin ) {
    // Match if properly named: wp-plugin (wp-plugin/wp-plugin.php).
    $proper_plugin_name = $plugin . '/' . $plugin . '.php';
    <pre><code>    if (
            file_exists( WP_PLUGIN_DIR . '/' . $proper_plugin_name )
            &amp;amp;&amp;amp; is_file( WP_PLUGIN_DIR . '/' . $proper_plugin_name )
            &amp;amp;&amp;amp; ! in_array( $proper_plugin_name, $active_plugins, true )
        ) {
            $active_plugins[] = $proper_plugin_name;
            return array_unique( $active_plugins );
        }
    
        // Match if improperly named: wp-plugin/cool-plugin.php.
        if (
            file_exists( WP_PLUGIN_DIR . '/' . $plugin )
            &amp;amp;&amp;amp; is_file( WP_PLUGIN_DIR . '/' . $plugin )
            &amp;amp;&amp;amp; ! in_array( $plugin, $active_plugins, true )
        ) {
            $active_plugins[] = $plugin;
            return array_unique( $active_plugins );
        }
    
        return array_unique( $active_plugins );
    }, 1000, 1 );Code language: PHP (php)

    Which can be activated in your theme’s functions.php like so:

    dt_force_plugin_active( 'akismet' ); or dt_force_plugin_active( 'wordpress-seo/wp-seo.php' );

    The only downside that I’ve seen so far is that you still get the Plugin deactivated. message in the admin notices.

  • Auto-Upgrading users in WordPress

    Auto-Upgrading users in WordPress

    I made a small site recently where I wanted all newly registered users from a specific email domain to automatically be administrators (this is a terrible idea, don’t do it).  The user registration was restricted by Single-Sign-On and 2-Factor Authentication, so I felt relatively safe doing this, especially since it was only a “for fun” project.

    The interesting bit of code that upgraded users to admins is as follows:

    add_action( 'user_register', 'upgrade_email_to_admin', 10, 1 );
    function upgrade_email_to_admin( $user_id ) {
    $user = get_user_by( 'ID', $user_id );
    if ( false !== $user ) {
    $email = $user-&gt;data-&gt;user_email;
    
       // Only example.com please.
        if ( false === strpos( $email, '@example.com' ) ) {
            return;
        }
    
        $roles = $user-&amp;gt;roles;
    
        if ( ! in_array( 'administrator', $roles, true ) ) {
            $user_update = array();
            $user_update['ID'] = $user_id;
            $user_update['role'] = 'administrator';
            wp_update_user( $user_update );
        }
    }
    Code language: PHP (php)

    This is 100% insecure, please do not do this 🙂