Check DNS records in terminal

Currently I’m working on a big server migration. Where a lot of small sites get moved over. And a lot of the domain names are registered and managed at third parties.
So after each move I have to check a lot of DNS records to see if the move is final.

Usually I use the mxtoolbox which is great to check DNS records.
But I needed a more bulk option for this mass migration.

Enter the dig command

Dig commands

Check A record:

dig example.com A +short

Check AAAA record (ipv6)

dig example.com AAAA +short

Check CNAME

dig example.com CNAME +short

Check A, AAAA and CNAME all at once.

dig  example.com AAAA example.com CNAME example.com A +short

These commands return very compact ip’s or domainnames.
Which are easy to scan.

I’m not sure if these are cached.

WordPress Settings API save post

The WordPress Settings API (docs) is used to create settings pages and save settings in the options table. But I didn’t need a setting, I needed a settings page to create/update a post.

Of course I could have created my own custom settings page and not use the settings API. But the settings API does have a couple of advantages.

  • Nonces validation.
  • Still using a WordPress standard.
  • Saving and error notices by default and customizable.
  • Mix with regular options.

First off create a settings page like we are used to:

<?php

add_action( 'admin_menu', 'register_menu_item' );

function register_menu_item() {
	add_submenu_page(
		'options-general.php',
		__( 'Settings page title' ),
		__( 'Menu title' ),
		'manage_options',
		'page-slug',
		function () {
			require './templates/settings-page.php';
		}
	);
}

add_action( 'admin_init', 'register_settings' );

/**
 * Register settings to save.
 */
function register_settings() {
	register_setting(
		'fake-option',
		'fake-option',
		array( 'sanitize_callback' => 'save_post' )
	);
}

/**
 * This function will only be called when the "option" is valid to save.
 * Here we validate our own fields and create the post we want.
 * 
 * @param null|array $unused_fake_setting
 */
function save_post( $unused_fake_setting ) {
	// Nonces are validated before we get here.

	static $first_run = true; // this function can be called twice, but we only need it once.
	if ( ! $first_run ) {
		return;
	}
	$first_run = false; // Never run again, this request.

	if ( empty( $_POST['log_title'] ) ) {
		add_settings_error( 'fake-option', 'title-empty', __( "Log title can't be empty." ) );

		return;
	}
	$title = wp_unslash( $_POST['log_title'] );
	$title = sanitize_text_field( $title );

	// validate other fields...
    
    
    // save the post.
	wp_insert_post( array(
		'post_title' => $title
		// add other attributes.
	) );

	// success message, yes the function name is confusing.
	add_settings_error( 'wpjm-ch-save', 'term-added', __( 'Worker successfully connected', 'success' ) );

}

And the templates/settings-page.php file:

<div class="wrap">
    <h1><?php esc_html_e( 'Settings page title' ); ?></h1>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option' ); ?>
        <h2><?php esc_html_e( 'Add log' ); ?></h2>

        <table class="form-table" role="presentation">
            <tbody>
            <tr>
                <th scope="row"><label for="log_title"><?php esc_html_e( 'Log title' ); ?></label></th>
                <td><input name="log_title" id="log_title" type="url" class="regular-text" required/></td>
            </tr>
            <tr>
                <th scope="row">More fields</th>
                <td></td>
            </tr>
            </tbody>
        </table>
		<?php submit_button( __( 'Add log' ) ); ?>
    </form>
</div>

How it works

We basically add settings like normal, except we skip a few things
First off normally you would add a section using add_settings_section in the same function where we register the settings field(s). But as we don’t really render a settings field in the template, we don’t add it at all. The same goes for the do_settings_sections in the Template.

In the settings template we do have the regular settings_fields this makes sure all the nonce fields are printed. And the submit_button.

Finally the thing we do extra and where the magic happens.
In the sanitize_callback of the register_setting function we set the save_post function.
Normally this is for sanitizing the option you want to save. But in this case we are not saving a option, we are saving a post. In this function we validate all the data send by the form, sanitize the data and save a new post.

And when the data is validated and saved we send a success notice with the add_settings_error function. Yes, we send a success notice with the **_error function. It can be done with the final argument, by setting the type to ‘success’.

Handling multiple custom options.

In the project that sparked this post I need to create a post, but also list those posts with a delete button.
What I found useful was creating a second register_setting, put them in two separate <form> tags on the template and let one handle the creating and the other the deleting.

<?php
function register_settings() {
	register_setting(
		'fake-option-create',
		'fake-option-create',
		array( 'sanitize_callback' => 'save_post' )
	);
	register_setting(
		'fake-option-delete',
		'fake-option-delete',
		array( 'sanitize_callback' => 'delete_post' )
	);
}
function save_post()  {
    // handles saveing/creating.
}
function delete_post() {
    // handles deleting.
}
?>


<div class="wrap">
    <h1><?php esc_html_e( 'Settings page title' ); ?></h1>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option-create' ); ?>
        HTML FORM HERE
		<?php submit_button( __( 'Add log' ) ); ?>
    </form>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option-delete' ); ?>
        LIST POSTS HERE WITH CHECKBOXES
		<?php submit_button( __( 'Delete logs' ) ); ?>
    </form>
</div>

Other tweaks.

?php_version=7.4&platform=wordpress&platform_version=5.6&software=free&software_version=15.6.2&days_active=30plus&user_language=en_US

  • Create, delete and updates posts (and post meta)
  • The same for user(meta and tax(meta)
  • External API calls.
  • Mix in regular use of the settings API.
  • World peace.

Installing WordPress Coding standards Globally

When possible I like to code using the WordPress coding Standards. I always set my PHPstorm coding style to WordPress. And when I start a new plugin I implement the PHPCodeSniffer rules for WordPress.
In the most ideal scenario you can include these in your composer settings, but that’s not always possible so it’s handy to have them installed globally.

To install run these 2 commands:

composer global require --dev squizlabs/php_codesniffer wp-coding-standards/wpcs phpcompatibility/phpcompatibility-wp dealerdirect/phpcodesniffer-composer-installer wptrt/wpthemereview
composer global update --dev --with-dependencies

Now all you need is installed. In your global composer directory. You can get that running: composer config home -g
By default is should return `~/.config/composer`

To test the phpcs with the global dir I run: ~/.config/composer/vendor/bin/phpcs -i
The output should look like this:

Result of command is "The installed coding standards are PSR12, PEAR, PSR2, PSR1, Squiz, MySource, Zend, PHPCompatibility, PHPCompatibilityParagonieRandomCompat, PHPCompatibilityParagonieSodiumCompat, PHPCompatibilityWP, WordPress-Docs, WordPress, WordPress-Extra, WordPress-Core and WPThemeReview"
Installed phpcs rulesets

Running this whole command will get annoying and will cluter up the terminal. So we will put the bin directory in the global $PATH
The bin directory we already got. (but we can also get using composer global config bin-dir --absolute)

To your ~/.bashrc add:

export PATH="$HOME/.config/composer/vendor/bin:$PATH"

Open a new terminal and test the command phpcs -i it should now work globally and is ready to run on individual projects.

Sources:

Shadow Posttype

The case I was dealing with. There is a posttype “person” and it had the normal templates, a archive-person.php and single-person.php with a bunch of metadata.
Which where visible on example.com/person/john-doe and the archive example.com/persons.
So far nothing special.

Every “person” was a -well- person. Some persons where also a “Judge”. A judge was an extra metabox on the person edit page.
And it needed it’s own pages on the frontend. example.com/judge/john-doe and example.com/judges.

Adding the rewrites

<?php
function judge_rewrite_rule() {
	/**
	 * Single judge
	 * 
	 * set is_judge to 1
	 * set person to post slug
	 */
	add_judge_rewrite_rule( '^judge/([^/]+)/?', 'index.php?is_judge=1&person=$matches[1]', 'top' );

	/**
	 * Archive for judges
	 */
	add_rewrite_rule( '^judges/?', 'index.php?is_judge=1&post_type=person', 'top' );
}
add_action( 'init','rewrite_rule' );

function judge_rewrite_tag() {
	/**
	 * make the query_vars aware of `is_judge`
	 */
	add_rewrite_tag( '%is_judge%', '([0-9]+)' );
}
add_action( 'init', 'judge_rewrite_tag' );

The main 2 functions to look out for are add_rewrite_rule and add_rewrite_tag.
I register the query_var is_judge and set it when the url is /judges/ or /judge/john-doe

After adding this do flush the permalinks. Just go to the settings for permalinks and press save. No need to change anything.

Registering the templates

<?php
/**
 * Set the correct theme/template-file.php
 * the shadow single & archive.
 *
 * @param string $template
 *
 * @return string
 */
function assign_judge_template( $template ) {
	if ( 1 !== (int) get_query_var( 'is_judge' ) ) {
		return $template; // we only check for templates when the judge set.
	}

	// assign the correct template.
	if ( is_single() ) {
		return get_template_directory() . '/single-person-judge.php';
	}
	if ( is_post_type_archive( 'person' ) ) {
		return get_template_directory() . '/archive-person-judge.php';
	}


	return $template; // fallback.
}
add_filter( 'template_include', 'assign_judge_template', 10, 1 );

When the is_judge is set set a different template. In this case this these templates are in the theme folder. You could also set templates from a plugin. The naming is can be anything but I picked single-person-judge.php so the next person can easier find the relation to the post type.

Setting the correct posts for the judge archive

Now the single will work as you would expect. But the archive page will still include all persons, not just the judges. Lets fix that.

<?php
/**
 * @param WP_Query $query
 */
function judge_pre_get_posts( $query ) {
	if ( 1 !== (int) $query->get( 'is_judge' ) || $query->is_single() ) {
		// only do this check if `is_judge` is set.
		// for single's this check is not needed.
		return;
	}
	// the meta_query
	$meta_is_judge = [
	    'relation' => 'AND',
		[ 'key'     => 'is_judge', 'compare' => 'EXISTS',],
		[
            'key'     => 'is_judge',
            'value'   => '1',
            'compare' => '=',
		],
	];

	// This part is to make sure you don't override other possible existing meta_queries
	$existing_meta   = $query->get( 'meta_query' );
	if ( empty( $existing_meta ) ) {
		$query->set( 'meta_query', $meta_is_judge );
	} else {
		$query->set( 'meta_query', [ 'relation' => 'AND', $existing_meta, $meta_is_judge, ] );
	}

}

add_action( 'pre_get_posts', 'judge_pre_get_posts' , 10, 1 );

If you’re familiar with the pre_get_posts hook this should not be to hard.
We select all posts that have the post_meta is_judge set to 1.
The way I add the meta_query seems a bit excessive, but this makes sure you never override a possible existing meta_query which an other filter might have added.

Now every Judge should be listed on example.com/judge and every individual judge should be visible on example.com/judge/john-doe. Only thing left to do is link to the page.

Linking to the judge url

As we want a person to be visible on both example.com/person/john-doe and example.com/judge/john-doe we can’t override the default permalink. So the best next thing is to create a helper function for calling the judge link.
This function should be used where you need to link to example.com/judge/john-doe, like on the judge archive page.

<?php
/**
 * Helper function
 *
 * @param int|WP_Post|null $post
 *
 * @return string, if not a judge, return normal permalink
 */
function get_judge_permalink( $post = null ) {
	$post = get_post( $post );
	if ( is_null( $post ) ) {
		return null;
	}

	// if the meta `is_judge` is not set, return the default person permalink.
	// for your use cases you might want to return something different.
	if ( '1' !== get_post_meta($post->ID, 'is_judge', true ) ) {
		return get_the_permalink( $post );
	}

	return home_url( 'judge/' . $post->post_name );
}

The function can be used in the same way as the regular get_the_permalink. get_judge_permalink(); the post argument is optional, and can also be a post_id.

Closing thoughts

In the template files, I suggest adding a very clear comment at the top, which explains where the rewrite functions are. An other person will otherwise have a hard time to find where this is created.

The list of judges will now automatically appear on the /judge/ archive page. If you need to create this list on a custom WP_query just add the query_var is_judge, example:

<?php
$q_args = array(  
	'post_type' => 'person',
	'is_judge' => 1, // this will get picked up by the `judge_pre_get_posts` filter
	// other arguments
);
$custom_query = new WP_Query($q_args);
// do what you want

Gravity Forms give editors access

By default Gravity forms isn’t accessible by editors. There are ways to do this with plugins like members.
As some might guess I prefer wp-cli for this:

wp cap add editor gravityforms_create_form gravityforms_edit_forms gravityforms_view_entries gravityforms_export_entries gravityforms_delete_entries graviyforms_delete_forms gravityforms_edit_entries gravityforms_view_entry_notes gravityforms_edit_entry_notes --grant
Difference in rights after setting capabilities

Running this will pretty much give all rights except the settings of Gravifty Forms.
It will allow creatign, editing & deleting forms. But also view entries and export them.

Full list of Gravity Forms Capabilities

Nano Shortcuts

I’ve know about nano and some it’s shortcuts. Today I explored them a bit more deeply.
So a list of shortcuts I find useful:

  • ctrl+K Cut the current line and put it in the nano clipboard (it’s not the same as the general clipboard)
  • ctrl+U Paste the line
  • ctrl+W Open search, type and hit enter. For the next match press alt+W
  • ctrl+Q Search backwards. For the next backward match press alt+Q
  • alt+U Undo
  • alt-E Redo
  • alt+C show the line number
  • alt-G Go to line number

Sources:

  • https://www.nano-editor.org/dist/latest/cheatsheet.html
  • https://askubuntu.com/a/672091/208343

PHP namespaces grouped

Consider the following code:

<?php
namespace a\b\c;
use function is_string;
use function get_class;

class main {
    protected $string;
    public function __construct( $string ) {
        if ( is_string( $string )) {
            $this->string = $string;
        } else {
            $this->string = get_class( $this );
        }
    }
}

Nothing fancy. Just a class in a namespace using two functions from the global namespace (is_string & get_class).
Those two functions are imported from the global namespace as that will give a small performance boost.

But if you have 20-30 build in PHP functions that list will get very long….

Luckily you can merge them:

use function is_string, get_class;

For now I’m not sure I’ll always import build in PHP functions, the boost is small. And it’s annoying to keep track of.

sumcheck a whole directory

For some reason files changed on a server. Site down, always fun.
Restored a backup all good. This site did not have git on the server. But I still wanted to monitor the files for changes.

The one I landed on was:

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} + | sort -k 2 | md5sum

Let’s dissect

What does this command do step by step

In the current directory and sub directory, list all files (not directories)

find ./ -type f

Limit it to php files

find ./ -type f -name "*.php"

Exclude the files in the caching directory, a bit weird syntax but it’s the one.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*"

For each file found run the command md5sum making a sum per file.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} +

Next we sort the output based on filepath+name.
We sort because find might return file order inconsistently.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} + | sort -k 2

Finally we create the grand total sumcheck based on all other sumchecks.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} + | sort -k 2 | md5sum

Sources:

  • https://stackoverflow.com/a/1658554/933065
  • https://unix.stackexchange.com/a/35834

WordPress the_date skipping same days

Lets take a look at this very basic loop.

<?php
$query_name = new WP_Query();

if ( $query_name->have_posts() ) :
    while ($query_name->have_posts()): $query_name->the_post();
        the_date();
        the_title();
        the_content();
    endwhile;
endif;

Nothing special right? Will by default just display the date, title and content of the first 10 posts.
But if 2 posts are published on the same day it will skip display that posts date.

When looking at the source code of the_date it compares the date of the previous post with the current using is_new_day.
I guess it can make sense in some scenario’s but too me it’s a bit weird by default.

To prevent this just use

<?php
echo get_the_date();