Category: WordPress

  • 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.

  • Guerilla Ad-Blocking: Taking Back the Web with WordPress

    Guerilla Ad-Blocking: Taking Back the Web with WordPress

    I have a new favorite WordPress plugin that I’ve just installed on my site:

    This plugin, by Stefan Bohacek, adds a notice to your site whenever a visitor comes that does not have an ad blocker installed:

    Using an ad blocker isn’t just about security and privacy, it also helps conserve your precious bandwidth. By blocking resource-intensive ads, it improves the loading speed of websites and saves you time.

    Additionally, an ad blocker protects your information from being harvested by advertisers, giving some peace of mind while browsing the web.

    You’ll also cut your “web carbon footprint” to a fraction of itself! Ugh, what a parasitic industry.

    My favorite is uBlock Origin, but I also love Pi-Hole for network-level blocking <3

  • Stopping WordPress User Registration Spam

    Stopping WordPress User Registration Spam

    I’ve had a rash of user registration spam lately, and even though I’m sure the site is secure, it’s just very annoying. So I’ve whipped up a quick little hook that I’ve thrown in my mu-plugins to give me the ability to add email hostnames to a blocklist and disable user registration from them:

    /**
     * Hook into the user registration process to deny registration to a blocklist of hostnames.
     *
     * @param string   $sanitized_user_login The sanitized username.
     * @param string   $user_email The user's email address.
     * @param WP_Error $errors Contains any errors with the registration process.
     *
     * @return void
     */
    function emrikol_blocklist_email_registration( string $sanitized_user_login, string $user_email, WP_Error $errors ): void {
    	// Validate the email address.
    	if ( filter_var( $user_email, FILTER_VALIDATE_EMAIL ) ) {
    		// Extract the email hostname from the user's email address and normalize it.
    		$email_parts  = explode( '@', $user_email );
    		$email_hostname = strtolower( $email_parts[1] );
    
    		$blocklist = array(
    			'email.imailfree.cc',
    			'mail.imailfree.cc',
    			'mailbox.imailfree.cc',
    		);
    
    		// Check if the email hostname is in the blocklist.
    		if ( in_array( $email_hostname, $blocklist ) ) {
    			$errors->add( 'email_hostname_blocked', __( 'Sorry, registration using this email hostname is not allowed.', 'emrikol' ) );
    		}
    	}
    }
    add_action( 'register_post', 'emrikol_blocklist_email_registration', 10, 3 );
    Code language: PHP (php)

    There’s lots of different ways you could extend this for yourself, like adding a hostname regex, a filter, or an admin screen to allow updates to the blocklist without having to make a code deploy.

  • Gathering database performance with WP-CLI

    Gathering database performance with WP-CLI

    Recently at work, my team was asked to help gather data about database server performance before and after an upgrade. To help with this, we collected a number of heavy database pages on some WordPress sites, dumped every query running to generate the page, and grabbed them to profile.

    I whipped up this quick and dirty WP-CLI command that will run a list of SQL queries 1,000 times and give you the minimum time, maximum time, average time, and standard deviation for each query.

    With this data, you can re-run the same queries again after a server change to determine if there has been any major SQL performance differences:

    /**
     * Profiles DB performance by running SQL queries and returning timing statistics.
     */
    public function db_profile( $args, $assoc_args ) {
    	$format = WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' );
    
    // Put all of your queries here!
    $site_sql_lines = <<<END
    SHOW TABLES LIKE 'wp_posts';
    END;
    
    	$lines = explode( PHP_EOL, $site_sql_lines );
    
    	global $wpdb;
    
    	$stats         = array();
    	$total_time_us = 0;
    	$runs          = 100;
    	$progress      = \WP_CLI\Utils\make_progress_bar( sprintf( 'Running %s Queries', number_format( count( $lines ) * $runs ) ), count( $lines ) * $runs );
    
    	for ( $run = 1; $run <= $runs; $run++ ) {
    		foreach ( $lines as $index => $line ) {
    			if ( ! isset( $stats[ $index ] ) || ! is_array( $stats[ $index ] ) ) {
    				$stats[ $index ] = array(
    					'query' => $line,
    					'runs'  => array(),
    				);
    			}
    
    			$start_time = hrtime( true );
    			$results    = count( $wpdb->get_results( $line, ARRAY_N ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
    			$end_time   = hrtime( true );
    			$time_us    = ( $end_time - $start_time ) / 1000;
    
    			$total_time_us += $time_us;
    
    			$stats[ $index ]['runs'][ $run ] = $time_us;
    			$progress->tick();
    		}
    	}
    
    	$data = array();
    
    	foreach ( $stats as $index => $stat ) {
    		$stats[ $index ]['stats'] = array(
    			'min' => min( $stats[ $index ]['runs'] ),
    			'max' => max( $stats[ $index ]['runs'] ),
    			'avg' => array_sum( $stats[ $index ]['runs'] ) / count( $stats[ $index ]['runs'] ),
    		);
    
    		$data[ $index ] = array(
    			'Query'  => $stat['query'],
    			'Min'    => number_format( (float) $stats[ $index ]['stats']['min'] / 1000, 3 ),
    			'Max'    => number_format( (float) $stats[ $index ]['stats']['max'] / 1000, 3 ),
    			'Avg'    => number_format( (float) $stats[ $index ]['stats']['avg'] / 1000, 3 ),
    			'StdDev' => number_format( (float) $this->stats_standard_deviation( $stats[ $index ]['runs'] ) / 1000, 3 ),
    		);
    
    		if ( 'csv' === $format ) {
    			$data[ $index ]['Runs'] = wp_json_encode( $stats[ $index ]['runs'] );
    		}
    	}
    
    	$progress->finish();
    
    	// Output data.
    	WP_CLI\Utils\format_items( $format, $data, array_keys( $data[0] ) );
    
    	// Output totals if we're not piping somewhere.
    	if ( ! WP_CLI\Utils\isPiped() ) {
    		WP_CLI::success(
    			sprintf(
    				'Total queries ran: %s, DB Time Taken: %s',
    				WP_CLI::colorize( '%g' . number_format( count( $lines ) * $runs, 0 ) . '%n' ),
    				WP_CLI::colorize( '%g' . $this->convert_to_human_readable( $total_time_us ) . '%n' ),
    			)
    		);
    	}
    }
    
    Code language: PHP (php)

    You can then run it and gather the data as a table, or as a CSV file (--format=csv) which is much more likely to be useful.

  • Limiting Featured Image Dimensions in WordPress

    Limiting Featured Image Dimensions in WordPress

    As a follow up to my last post about limiting file sizes during uploads, I had to come back to the problem with limiting image sizes for featured images. Not bytes this time, but pixel dimensions.

    Still being a bit of a block editor newb, this was an interesting challenge for me, and I was really surprised at how easy it was to implement. I basically googled around and found a few different things to put together that worked for me. The primary source was a post by Igor Benic on how to disable the publish button:

    After that, I discovered how to add notifications to the block editor, to tell when the image was too large, thanks to a post by David F. Carr:

    So what’s it look like? Well, it’s pretty simple. If you choose a featured image that’s too large for the settings, it will add a non-dismissible error notification to the editor:

    Then it will block you from hitting the publish button:

    So, finally the code. This is just enqueued in as a simple add_action() during the enqueue_block_editor_assets hook. I’m absolutely not a good JS developer, so please don’t judge me too harshly. Also, that means this could be riddled with bugs. Use at your own risk πŸ˜€

    var postLocked = false;
    wp.domReady( () => {
    	wp.data.subscribe( function() {
    		var imageId = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'featured_media' ); // Featured Image ID.
    
    		// If we have no image ID, and we already locked the post, we won't do anything.
    		if ( imageId ) {
    			blockEditorSettings = wp.data.select('core/block-editor').getSettings();
    
    			// Default to 1200px wide.
    			maxImageSize = 1200;
    			imageAttrs = wp.data.select('core').getMedia( imageId );
    
    			// Get the size for the "large image" and if it's available, use that instead.
    			if ( typeof blockEditorSettings !== 'undefined' && blockEditorSettings.hasOwnProperty( 'imageDimensions' ) ) {
    				maxImageSize = blockEditorSettings.imageDimensions.large.width;
    			}
    
    			if ( typeof imageAttrs !== 'undefined' && imageAttrs.hasOwnProperty( 'media_details') ) {
    				// Publish is not locked and width is too large.
    				if ( ! postLocked && imageAttrs.media_details.width > maxImageSize ) {
    					postLocked = true;
    					wp.data.dispatch( 'core/editor' ).lockPostSaving( 'featuredImageTooLarge' );
    
    					wp.data.dispatch('core/notices').createNotice(
    						'error', // Can be one of: success, info, warning, error.
    						wp.i18n.__( wp.i18n.sprintf( 'Featured image width must be less than %spx, currently %spx. Publishing is disabled.', maxImageSize, imageAttrs.media_details.width ) ),
    						{
    							id: 'featuredImageTooLarge', // Assigning an ID prevents the notice from being added repeatedly.
    							isDismissible: false, // Whether the user can dismiss the notice.
    						}
    					);
    				}
    
    			}
    		} else if ( postLocked ) {
    			postLocked = false;
    			wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'featuredImageTooLarge' );
    			wp.data.dispatch('core/notices').removeNotice( 'featuredImageTooLarge' );
    		}
    	} );
    } );Code language: JavaScript (javascript)
  • Half Baked Idea: Limiting WordPress Image Upload Sizes

    Half Baked Idea: Limiting WordPress Image Upload Sizes

    If you want to be able to limit images (or any attachments) from being uploaded into the media library if they are too large, you can use the wp_handle_upload_prefilter filter to do this.

    Below is a really basic example I whipped up in a few minutes that I shared with a customer not too long ago:

    /**
     * Limits the maximum size of images that can be uploaded.
     *
     * @param array $file {
     *     Reference to a single element from `$_FILES`.
     *
     *     @type string $name     The original name of the file on the client machine.
     *     @type string $type     The mime type of the file, if the browser provided this information.
     *     @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
     *     @type int    $size     The size, in bytes, of the uploaded file.
     *     @type int    $error    The error code associated with this file upload.
     * }
     *
     * @return array       Filtered file array, with a possible error if the file is too large.
     */
    function blarg_limit_max_image_upload_size( $file ) {
        if ( str_starts_with( $file['type'], 'image/' ) ) {
            $max_size = 1 * 1024; // 1kb.
    
            if ( $file['size'] > $max_size ) {
                // translators: %s: Human readable maximum image file size.
                $file['error'] .= sprintf( __( 'Uploaded images must be smaller than %s.' ), size_format( $max_size ) );
            }
        }
    
        return $file;
    
    }
    add_filter( 'wp_handle_upload_prefilter', 'blarg_limit_max_image_upload_size', 10, 1 );Code language: PHP (php)

    Good luck if you use any of my hot garbage in production πŸ˜€

  • Quick Tip: Get Size of Revisions in WordPress

    Quick Tip: Get Size of Revisions in WordPress

    One thing that you might not think of when watching the size of a large WordPress site grow, is unnecessary data in the database. With the introduction of the block editor years ago, there has been a large increase in the number of revisions a post makes when being edited.

    This can create a lot of revisions in the database if you’re not setting a limit.

    If you’d like to do a quick and dirty audit of your revision data, you can use a very ugly SQL query like this:

    SELECT COUNT( ID ) as revision_count, ( SUM( CHAR_LENGTH( ID ) ) + SUM( CHAR_LENGTH( post_author ) ) + SUM( CHAR_LENGTH( post_date ) ) + SUM( CHAR_LENGTH( post_date_gmt ) ) + SUM( CHAR_LENGTH( post_content ) ) + SUM( CHAR_LENGTH( post_title ) ) + SUM( CHAR_LENGTH( post_excerpt ) ) + SUM( CHAR_LENGTH( post_status ) ) + SUM( CHAR_LENGTH( comment_status ) ) + SUM( CHAR_LENGTH( ping_status ) ) + SUM( CHAR_LENGTH( post_password ) ) + SUM( CHAR_LENGTH( post_name ) ) + SUM( CHAR_LENGTH( to_ping ) ) + SUM( CHAR_LENGTH( pinged ) ) + SUM( CHAR_LENGTH( post_modified ) ) + SUM( CHAR_LENGTH( post_modified_gmt ) ) + SUM( CHAR_LENGTH( post_content_filtered ) ) + SUM( CHAR_LENGTH( post_parent ) ) + SUM( CHAR_LENGTH( guid ) ) + SUM( CHAR_LENGTH( menu_order ) ) + SUM( CHAR_LENGTH( post_type ) ) + SUM( CHAR_LENGTH( post_mime_type ) ) + SUM( CHAR_LENGTH( comment_count ) ) ) as post_size FROM wp_posts WHERE post_type='revision' GROUP BY post_type ORDER BY revision_count DESC;Code language: JavaScript (javascript)

    This will give you the number of revisions you have, and the approximate amount of data its using in the database:

    revision_count post_size
    441419 2842450412

    You can see now a very large WordPress site can amass a lot of unnecessary data in its database over a number of years. 2.8 Gigabytes of revisions is a lot of stuff if you’re never going to use them again.

  • Cool WordPress Plugins: Embed Extended

    Cool WordPress Plugins: Embed Extended

    If you saw my last post, you may have noticed some cool embeds. These are coming from the Embed Extended plugin. This plugin takes OpenGraph data and treats it more like oEmbed data for WordPress. It works great with the block editor as well!

  • 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.