wp_localize_script(): What can you do with that third arg?

Let’s start with this, the param descriptions for wp_localize_script():

 * @param string $handle      Script handle the data will be attached to.
 * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable.
 *                            Example: '/[a-zA-Z0-9_]+/'.
 * @param array $l10n         The data itself. The data can be either a single or multi-dimensional array.
 * @return bool True if the script was successfully localized, false otherwise.
 */
function wp_localize_script( $handle, $object_name, $l10n ) {

source

So you’re passing an array, right? Good.

If you’re not, tsk tsk. But let’s take a look at the good and the bad…

Array. Expected usage, you’re okay here:

wp_localize_script( 'jquery', 'exampleObject1', [
    'key' => 'value',
] );
// var exampleObject1 = {"key":"value"};

wp_localize_script( 'jquery', 'exampleObject2', [
    'value',
] );
// var exampleObject2 = ["value"];

String. Hit and miss:

wp_localize_script( 'jquery', 'exampleObject3', 'string' );
// var exampleObject3 = "string";

Above works by coincidence. This shouldn’t be relied upon.

wp_localize_script( 'jquery', 'exampleObject4', '' );
// var exampleObject4 = "";

Above throws a warning:

PHP Warning: Cannot assign an empty string to a string offset

Reduced example of the difference:

$l10n = 'string';
$l10n[0] = $l10n; // okay

$l10n = '';
$l10n[0] = $l10n; // warning. There's no character at the zeroth place to replace

Scalar. Miss (maybe hit because PHP is wacko):

wp_localize_script( 'jquery', 'exampleObject5', true );
// var exampleObject5 = true;

Above throws warning:

Cannot use a scalar value as an array

wp_localize_script( 'jquery', 'exampleObject6', false ); // no error
// var exampleObject6 = [""];

However, the above does not.

Reduced example, though it won’t really clarify things:

$true = true;
$false = false;

var_dump( is_scalar( $true ) ); // true
var_dump( is_scalar( $false ) ); // true

$true[] = 'adsf'; // Cannot use a scalar value as an array 
$false[] = 'jkl'; // okay why not

var_dump( $true ); // true
var_dump( $false ); // [ 0 => 'jkl' ] 

Why? No really, I’m asking.


So listen to the docs, and use an array.

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 );

if ( 'help' === $args[0] ) {
    // no need to run any checks for help commands
    return;
}

if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run' ) ) {
    // no need to run any checks if we're already dry-running
    return;
}

if ( 'search-replace' === $args[0] ) {
        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.

Example

Run wp-cli command

$ wp curl-rest --user=admin 

Output:

curl -H 'X-WP-NONCE: ooO0o00o00' --cookie \
  'wordpress_logged_in_wwWwwwWwwwWWwwwwwWwwwWWwwWWWwwwww=admin|xxxxxxxxxx|YYyyYyyyyYyYyYYYYYyyyYYyyyyYyyYYyyyyyyYYyyY|zZzzzzzZzZzzzzzZZzzzZzzzZzzZZzzzzzzZZzZzzzZZZzzZzZzzzZZzzzzzZzzz' \
  'http://local.wordpress.local/wp-json/wp/v2/settings'

Run curl command

$ curl -H 'X-WP-NONCE: ooO0o00o00' --cookie \
  'wordpress_logged_in_wwWwwwWwwwWWwwwwwWwwwWWwwWWWwwwww=admin|xxxxxxxxxx|YYyyYyyyyYyYyYYYYYyyyYYyyyyYyyYYyyyyyyYYyyY|zZzzzzzZzZzzzzzZZzzzZzzzZzzZZzzzzzzZZzZzzzZZZzzZzZzzzZZzzzzzZzzz' \
  'http://local.wordpress.local/wp-json/wp/v2/settings'

Output:
(prettified, I recommend the jq command line tool)

{
  "title": "wordpress.local",
  "description": "Just another WordPress site",
  "url": "http://local.wordpress.local",
  "email": "[email protected]",
  "timezone": "",
  "date_format": "F j, Y",
  "time_format": "g:i a",
  "start_of_week": 1,
  "language": "",
  "use_smilies": true,
  "default_category": 1,
  "default_post_format": "0",
  "posts_per_page": 10,
  "default_ping_status": "open",
  "default_comment_status": "open"
}

view raw

readme.md

hosted with ❤ by GitHub


<?php
if ( !defined( 'WP_CLI' ) ) return;
/**
*/
class trepmal_cURL_REST extends WP_CLI_Command {
/**
* Generate cURL command for REST API
*
* ## EXAMPLES
*
* # Generare command for fetching all administrators
* $ wp curl-rest –user=1 wp/v2/users?role=administrator
*
* @synopsis [<route>] –user=<user>
*
*/
function __invoke( $args, $assoc_args ) {
$args = wp_parse_args( [
0 => 'wp/v2/settings'
], $args );
list( $route ) = $args;
$route = trim( $route, '/');
if ( substr( $route, 0, 5 ) !== 'wp/v2' ) {
$route = "wp/v2/$route";
}
if ( ! $user_id = get_current_user_id() ) {
WP_CLI::error( 'Please pass –user' );
}
$expiration = time() + DAY_IN_SECONDS;
$manager = WP_Session_Tokens::get_instance( $user_id );
$token = $manager->create( $expiration );
$logged_in_cookie = wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token );
$_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie; // for nonce creation
$url = rest_url( $route );
$nonce = wp_create_nonce( 'wp_rest' );
$cookie = LOGGED_IN_COOKIE . "={$logged_in_cookie}";
$nonce_header = "X-WP-NONCE: $nonce";
$curl = "curl -H '$nonce_header' –cookie '$cookie' '$url'";
$split_curl = _split_str_by_whitespace( $curl, 100 );
$curl = implode( "\\\n ", $split_curl );
WP_CLI::line( $curl );
}
}
WP_CLI::add_command( 'curl-rest', 'trepmal_cURL_REST' );

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;
}

Continue reading Notes: Renaming and relocating directories in WordPress

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