Category Archives: WordPress Bits

WP-CLI: Require confirmation for search-replace

Worried about accidentally running a real search-replace command? Here’s how you can require confirmation for live-runs:

In your config file, add this:

require:
  - /path/to/search-replace-confirmation.php

In /path/to/search-replace-confirmation.php, add this:

<?php

$configurator = \WP_CLI::get_configurator();
$argv = array_slice( $GLOBALS['argv'], 1 );
list( $args, $assoc_args, $runtime_config ) = $configurator->parse_args( $argv );


$help          = array_search( 'help', $args );
$searchreplace = array_search( 'search-replace', $args );
$dry_run       = array_search( 'dry-run',  $assoc_args );

if (
    false !== $searchreplace && // is search-replace
    false === $dry_run && // isn't already dry run
    ( false === $help || $help > $searchreplace ) // isn't help command
) {
    WP_CLI::confirm( 'Proceed with live run?' );
}

Don’t shortcut hook replication

A short post in the same vein as my last post, Don’t name form fields “update” or “link”.

We’ve probably all done something like this in one of our plugins:

$thing_title = apply_filters( 'the_title', $thing_title );

We want our thing’s title to get the same treatment as any other post title, so we use core’s filter. No big deal, right? Not so much.

In core, it’s actually used like this:

return apply_filters( 'the_title', $title, $id );

Importantly, it has an additional argument, $id, passed to it. source

Like with the accidental hook duplication in my last post, this means that callbacks expecting 2 arguments will end up throwing errors when the poorly-duplicated hook only provides one. Make sure when duplicating core hooks, all the expected arguments are passed.

Don’t name form fields “update” or “link”

In WordPress, every form field on the post edit screen results in a hook named pre_post_{$field} on save. Here’s how:

That means a field named “update” results in a variable filter hook named pre_post_update with 1 argument passed.

However, there’s a core action with that name already, and it passes 2 arguments: https://xref.trepmal.com/wp/wp-includes/post.php.source/#l3371

Similarly with link, there’s a core filter by that name already: https://xref.trepmal.com/wp/wp-includes/link-template.php.source/#l163

The problem?

If I have a form field named “update”, and I (or any plugin I’m running) hook into the core action like this:

add_action( 'pre_post_update', 'myplugin_pre_post_update', 10, 2 );
function myplugin_pre_post_update( $post_id, $data ) {
    // do stuff
}

Then the variable hook will call myplugin_pre_post_update but only pass the 1 argument. As of PHP 7.1, this will cause a fatal error

Besides, names as generic as ‘update’ and ‘link’ run the risk of conflict with another plugin.

Possible solution

If changing the field names isn’t feasible, you can adjust your callback:

add_action( 'pre_post_update', 'myplugin_pre_post_update', 10, 2 );
function myplugin_pre_post_update( $post_id, $data = null ) {
    if ( is_null( $data ) ) {
        // null $data means we're on the wrong hook instance, bail!
        return;
    }
    // do stuff
}

Cookie-Nonce authentication for REST API cURL Requests

The WordPress REST API is quite a feature, but it can be a struggle to deal with authentication. One option is Basic Auth. But can we leverage the built-in cookie authentication?

If you look at rest_cookie_check_errors(), you’ll see where it’s checking for an authentication cookie and valid nonce. Using WP-CLI, we can carefully piece together a cURL command that passes those values appropriately. Remember, cookies and nonces have a limited lifetime, so be sure to generate new commands as time goes by.

Notes: Renaming and relocating directories in WordPress

This is not a guide (yet?). These are just notes for personal reference which may be expanded upon later.

Site owners/developers/administrators may find useful hints below, but please do not get mad at me if you break your site.


Methods were initially tested on a multisite-with-subdirectories installation, but are generally applicable to single and subdomain installations as well.

standard installation

familiar structure, e.g.

├── wp-config.php
└── wp-content/
    └── plugins/

nginx (subdirectory multisite)

if (!-e $request_filename) {
    rewrite ^(/[^/]+)?(/wp-.*)   $2                   last;
    rewrite ^(/[^/]+)?(/.*\.php) $2                   last;
}

(more…)

WP CLI: --skip-plugins

A few notes on skipping plugins with wp-cli.

Suppose you want to skip all except one, DONTSKIP:

wp user list --skip-plugins=$(wp plugin list --field=name | grep -v ^DONTSKIP$ | tr  '\n' ',')

If this will be repeated, you’d benefit from saving the ‘skip-list’ to a text file instead of running the nested command each time. This also allows you to update the skip-list relatively easily, if you don’t mind tighly squeezed comma-separated lists.

wp plugin list --field=name | grep -v ^DONTSKIP$ | tr  '\n' ',' > skipplugins.txt
wp user list --skip-plugins=`cat skipplugins.txt`

If it’s a permanent skip-list, save the keystrokes and processes by putting it in a config file (different options depending on how global you want that change: http://wp-cli.org/config/)

 #~/.wp-cli/config.yml
skip-plugins:
 - skip-me
 - skip-me-too

isset() Assumptions

register_setting() can be really handy, but take note! The first time your option gets saved, it’ll pass through the santize callback twice. With most input, it won’t matter; and if you’re explicit, it won’t matter either. But under the right conditions, it will.

Suppose your register_setting() looks like this:

register_setting( $option_group, 'checkbox', 'sanitize' );

And you have a pair of checkboxes like this:

<input type="checkbox" name="checkbox[one]" value="1" />
<input type="checkbox" name="checkbox[two]" value="1" />

And finally, your sanitize callback looks like this

function sanitize( $input ) {
	$input['one'] = isset( $input[ 'one' ] ) ? true : false;
	$input['two'] = isset( $input[ 'two' ] ) ? true : false;
	return $input;
}

See the problem?

On the very first save, both $input['one'] and $input['two'] will be true, no matter what. Say checkbox one is unchecked, when it fist passes through, $input['one'] is set to false – as expected. But when the input is passed through sanitize the second time, $input['one'] is set (to false) and thus the $input['one'] is changed to true.

demo

So the moral of the story is: be more explicit, don’t make assumptions.

Fun with WP-CLI

A random collection of things you perhaps didn’t know you could do with WP-CLI.

Maybe you’ve imported an image before, but did you know you can import a whole directory just as easily? For example, if the directory is named ‘images’:

wp media import images/*

Sometimes you need to run a command based on a result set from another command. In many cases, you can do that by nesting the one in the other. For example, if you want to change the password for all users with the ‘author’ role.

wp user update $(wp user list --role=author --field=ID) --user_pass=password

Or go crazy and regenerate media for the featured image of all sticky posts.

wp media regenerate $(wp eval 'foreach( wp_parse_id_list( get_option("sticky_posts") ) as $id ) { echo get_post_thumbnail_id($id). " "; }')

Trying to run a command for each site in a multisite doesn’t have to be a chore. A little bash script can speed things along.

#!/bin/bash

for url in $(wp site list --field=url)
do
	wp theme activate twentyfourteen --url=$url
done

Save that to a file, and run with bash: bash filename

You can do that in a one-liner as well, but that can make it harder to see what you’re doing, especially with more comprehensive commands.

for url in $(wp site list --field=url); do wp theme activate twentyfourteen --url=$url; done

Function: wp_list_pluck()

Suppose you want to get some information about some posts in this format: an array of post titles, keyed by the post ID. How would you do it? Spoiler alert: Skip the first 2 options

Let’s assume you’re fetching some posts via get_posts(). For simplicity I won’t be passing an arguments to that function, but go ahead if you want to 🙂

$posts = get_posts();

Option 1

$post_titles = array();

foreach( $posts as $p ) {
	$post_titles[ $p->ID] = $p->post_title;
}

Not bad, but surely there’s a WordPress-y way to do that.

Option 2

You might have come across the function wp_list_pluck() which is really neat for pulling a particular value by key from an array of arrays or objects.

$post_titles = wp_list_pluck( $posts, 'post_title' );

This works for getting the titles, but leaves us without the IDs as keys. You might extrapolate that to this

$post_titles = array_combine( wp_list_pluck( $posts, 'ID' ), wp_list_pluck( $posts, 'post_title' ) );

Now we have what we want, but that seems clunky.

Option 3 (the best!)

Since WordPress 4.0, wp_list_pluck() takes a third argument that makes it behave more like the native PHP function (since 5.5) array_column(). This third argument allows you to specify a key whose value should be used as the key of the returned array.

$post_titles = wp_list_pluck( $posts, 'post_title', 'ID' );