Tag: filter

  • Silly Ideas: Cache WordPress Excerpts

    Silly Ideas: Cache WordPress Excerpts

    I recently worked on profiling a customer site for performance problems, and one of the issues that I had seen was that calls to get_the_excerpt() were running HORRIBLY slow due to filters.

    I ended up writing a really hacky workaround to cache excerpts to help reduce the burden they had on the page generation time, but we ended up not using this solution. Instead we found another filter in a plugin that removed the painfully slow stuff happening in the excerpt.

    So below is one potential way to cache your excerpt, but be warned it’s only barely tested:

    /**
     * Checks the excerpt cache for a post and returns the cached excerpt if available.
     * If the post is password protected, the original post excerpt is returned.
     *
     * @param string  $post_excerpt The original post excerpt.
     * @param WP_Post $post         The post object.
     *
     * @return string The post excerpt, either from the cache or the original.
     */
    function blarg_check_excerpt_cache( string $post_excerpt, WP_Post $post ): string {
    	// We do not want to cache password protected posts.
    	if ( post_password_required( $post ) ) {
    		return $post_excerpt;
    	}
    
    	$cache_key   = $post->ID;
    	$cache_group = 'blarg_cached_post_excerpt';
    	$cache_data  = wp_cache_get( $cache_key, $cache_group );
    
    	if ( false !== $cache_data ) {
    		remove_all_filters( 'get_the_excerpt' );
    		add_filter( 'get_the_excerpt', 'blarg_check_excerpt_cache', PHP_INT_MIN, 2 );
    		$post_excerpt = $cache_data;
    	} else {
    		add_filter( 'get_the_excerpt', 'blarg_cache_the_excerpt', PHP_INT_MAX, 2 );
    	}
    
    	// At this point, do not modify anything and return.
    	return $post_excerpt;
    }
    add_filter( 'get_the_excerpt', 'blarg_check_excerpt_cache', PHP_INT_MIN, 2 );
    
    /**
     * Caches the post excerpt in the WordPress object cache.
     *
     * @param string  $post_excerpt The post excerpt to cache.
     * @param WP_Post $post         The post object.
     *
     * @return string The cached post excerpt.
     */
    function blarg_cache_the_excerpt( string $post_excerpt, WP_Post $post ): string {
    	$cache_key   = $post->ID;
    	$cache_group = 'blarg_cached_post_excerpt';
    	wp_cache_set( $cache_key, $post_excerpt, $cache_group );
    
    	return $post_excerpt;
    }
    
    /**
     * Deletes the cached excerpt for a given post.
     *
     * @param int|WP_Post $post The post ID or WP_Post object.
     *
     * @return void
     */
    function blarg_delete_the_excerpt_cache( int|WP_Post $post ): void {
    	if ( $post instanceof WP_Post ) {
    		$post_id = $post->ID;
    	} else {
    		$post_id = $post;
    	}
    
    	$cache_key   = $post_id;
    	$cache_group = 'blarg_cached_post_excerpt';
    	wp_cache_delete( $cache_key, $cache_group );
    }
    add_action( 'clean_post_cache', 'blarg_delete_the_excerpt_cache', 10, 1 );
    Code language: PHP (php)

    This should automatically clear out the cache any time the post is updated, but if the excerpt gets updated in any other way you may need to purge the cache in other ways.

    Good luck with this monstrosity!

  • Code Sweep: A Simple Approach to a Neater WordPress User List

    Code Sweep: A Simple Approach to a Neater WordPress User List

    Feel like clearing out your spam users? With the snippet below we can make your job much easier!

    /**
     * Adds a new column to the user management screen for displaying the number of comments.
     *
     * @param array $columns The existing columns in the user management screen.
     *
     * @return array The modified columns array with the new 'comments_count' column added.
     */
    function emrikol_add_comments_column( array $columns ): array {
    	$columns['comments_count'] = esc_html__( text: 'Comments', domain: 'default' );
    	return $columns;
    }
    add_filter( 'manage_users_columns', 'emrikol_add_comments_column' );
    
    /**
     * Displays the number of comments for a user in the custom column.
     *
     * @param string $output       The value to be displayed in the column.
     * @param string $column_name  The name of the custom column.
     * @param int    $user_id      The ID of the user.
     *
     * @return string              The updated value to be displayed in the column.
     */
    function emrikol_show_comments_count( string $output, string $column_name, int $user_id ): string {
    	if ( 'comments_count' == $column_name ) {
    		$args           = array(
    			'user_id' => $user_id,
    			'count'   => true,
    		);
    		$comments_count = get_comments( args: $args );
    		return number_format_i18n( number: $comments_count );
    	}
    
    	return $output;
    }
    add_action( 'manage_users_custom_column', 'emrikol_show_comments_count', 10, 3 );
    Code language: PHP (php)

    This will add a “Comments” count to the WordPress user list so you can easily determine which users you can delete:

    What a sad state this blarg is in…

  • Meet The Plugin That Lists All Your Multisite’s Sites: Multisite Site List

    Meet The Plugin That Lists All Your Multisite’s Sites: Multisite Site List

    Have you ever found yourself in this common situation?

    “I’ve got this lovely WordPress Multisite, but I’d like to list every site on it in a post or page!”

    Of course you have–we’ve all been there.

    Well, let me introduce you to my newest plugin: site-list

    Installing the Plugin

    Installing it is a breeze, and using it is as easy as pie—assuming you find pie easy. Just clone the git repository, grab the code, upload it to your WordPress site, and enable it. Don’t forget to constantly check back in the repository for updates that will probably never come because I didn’t upload this to the WordPress.org Plugin Repository.

    Example Output

    This plugin does one thing: It adds a new block called “Multisite Site List” that lists the site in a multisite in an unordered list, like this:

    <ul class="ms-sites-list">
    	<li>
    		<a href="https://example.com/">My Website</a>
    		<p>The Super Awesome Site Tagline</p>
    	</li>
    	<li>
    		<a href="https://example.com/better-site">My Even Better Website</a>
    		<p>The Better Site Tagline</p>
    	</li>
    	<li>
    		<a href="https://example.com/garbage-site">My Blarg</a>
    		<p>Why are you even reading this?</p>
    	</li>
    </ul>Code language: HTML, XML (xml)

    Customizing the Output

    “But Derrick, what if I want to hide a site, or change something?”

    Why would you? This is perfect as-is. *sigh* Oh, you want to be the captain of your ship? Fine, here’s a filter for you:

    /**
     * Filters the list of sites retrieved by the `get_sites()` function.
     *
     * This filter allows developers to modify the list of sites before
     * they are processed and rendered in the `ms_sites_list_block_render()` function.
     * It can be useful for modifying, adding, or removing sites based on custom criteria
     * or to inject additional information into the sites' data.
     *
     * @param array $sites An array of `WP_Site` objects representing each site in the WordPress multisite network.
     */
    $sites = apply_filters( 'site_list_get_sites', get_sites() );Code language: PHP (php)

    Happy now?

    So yeah, this is a thing. Do what you want with it, and enjoy.

  • Quick Tip: Script Debugging in WordPress

    Quick Tip: Script Debugging in WordPress

    If you’re debugging core WordPress scripts, one thing you might run into is dealing with cached copies of the script. Due to how script-loader.php enqueues the core files, their versions are “hard coded” and short of editing script-loader.php as well, there’s a way to fix this via a filter:

    add_filter( 'script_loader_src', function( $src, $handle ) {
         if ( false !== strpos( $src, 'ver=' ) ) {
             $src = remove_query_arg( 'ver', $src );
             $src = add_query_arg( array( 'ver', rawurlencode( uniqid( $handle ) . '-' ) ), $src );
         }
         
         <span style="background-color: inherit; color: rgb(248, 248, 242); font-size: inherit; letter-spacing: -0.015em;">return $src;</span>
     }, -1, 2 );Code language: PHP (php)

    This will apply a unique ver argument to the core scripts on each refresh, so no matter what you’re editing you should get the most recent version from both any page cache you may have and also the browser cache (🤞).

    Also, don’t forget to enable SCRIPT_DEBUG if you’re hacking away at core scripts to debug issues.

    I couldn’t find a good related image, so enjoy this delicious toilet paper.

  • 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