Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

  • Image Attribution

    Image Attribution

    Did you know you could add fields to the media uploader?

    In my case, I was at a convention and a fellow reporter (welcome to my new weird life) muttered she wished it was easier to have just a photo ‘credit’ line when she uploaded media from networks. I asked what system she used to run her site and when she said WordPress, I gave her my biggest smile.

    Image Filters

    There are two things we need to filter here.

    1. Add our attribution field to the editor
    2. Save the attribution data

    That’s it. WordPress handles the rest.

    add_filter( 'attachment_fields_to_edit', 'halfelf_add_attachment_attribution', 10000, 2);
    add_action( 'edit_attachment', 'halfelf_save_attachment_attribution' );
    
    function halfelf_add_attachment_attribution( $form_fields, $post ) {
    	$field_value = get_post_meta( $post->ID, 'HALFELF_attribution', true );
    	$form_fields[ 'HALFELF_attribution' ] = array(
    		'value'    => $field_value ? $field_value : '',
    		'label'    => __( 'Attribution' ),
    		'helps'    => __( 'Insert image attribution here (i.e. "NBCUniversal" etc)' )
    	);
    	return $form_fields;
    }
    
    function halfelf_save_attachment_attribution( $attachment_id ) {
    	if ( isset( $_REQUEST['attachments'][$attachment_id]['lwtv_attribution'] ) ) {
    		$attribution = $_REQUEST['attachments'][$attachment_id]['HALFELF_attribution'];
    		update_post_meta( $attachment_id, 'HALFELF_attribution', $attribution );
    	}
    }
    

    End Result?

    It shows up a little lower down than I’d like (I’d prefer it to be up where the URL is) but it works:

    An example of Image Attribution

    Oh and yes, I emailed her the code as a stand-alone plugin. Her IT person was thrilled.

  • Protect My Plugins, Please

    Protect My Plugins, Please

    I have a site that literally requires a specific plugin always be active for things to work. It has a lot of very complicated code, and two things must never ever happen to this:

    1. It should never be disabled
    2. It should never be updated by WordPress

    Well. Okay. Let’s do that.

    A Caveat

    If you don’t have a way in place to update and secure the plugin you’re hiding, DON’T DO THIS. Once you hide it, people won’t know to update it and, in fact, won’t be able to. Because you’re also preventing traditional updates. In my case, I have a git repository that triggers a push (plus some other magic).

    Again. Unless you’ve got something set up to handle updates, don’t do this.

    Now let’s do this.

    Never Disable Me (by Hiding)

    To do this, we need to know the folder name and filename of the plugin, such as ‘my-plugin/my-plugin.php. If you want to hide the plugin you've put the code in, you can useplugin_basename( FILE )` instead, which is what I’ve done.

    add_action( 'pre_current_active_plugins', 'mysite_hide_my_plugins' );
    function mysite_hide_my_plugins() {
    	global $wp_list_table;
    
    	$hide_plugins = array(
    		plugin_basename( __FILE__ ),
    	);
    	$curr_plugins = $wp_list_table->items;
    	foreach ( $curr_plugins as $plugin => $data ) {
    		if (in_array( $plugin, $hide_plugins ) ) {
    			unset( $wp_list_table->items[$plugin] );
    		}
    	}
    }
    

    If you were writing a plugin to hide a lot of things, you would change the array to list all of them. But since this is a ‘hide myself’ deal, it’s easier to put it in the plugin itself.

    The added bonus to making the file path dynamic is that if someone gets clever and renamed the folder or file, it would still work. Neener.

    Stop My Updates

    Now this one again is easier if you put it in the plugin you’re disabling updates for, because again you can use plugin_basename( __FILE__ ) to detect the plugin. If you’re not, then you’ll need to make an array of names. But that should get you started.

    add_filter( 'http_request_args', 'mysite_disable_my_plugin_update', 10, 2 );
    function mysite_disable_my_plugin_update( $return, $url ) {
    	if ( 0 === strpos( $url, 'https://api.wordpress.org/plugins/update-check/' ) ) {
    		$my_plugin = plugin_basename( __FILE__ );
    		$plugins   = json_decode( $return['body']['plugins'], true );
    		unset( $plugins['plugins'][$my_plugin] );
    		unset( $plugins['active'][array_search( $my_plugin, $plugins['active'] )] );
    		$return['body']['plugins'] = json_encode( $plugins );
    	}
    	return $return;
    }
    

    What Does It Look Like?

    Nothing, really. You’ve hidden everything. Congratulations. Now you don’t have to worry about admins or WordPress updating your plugin. Or worse.

  • Query Vars, Post Titles, and Yoast SEO

    Query Vars, Post Titles, and Yoast SEO

    Most people who use WordPress use query vars every single day without knowing. Back in the old days, we used to have websites with URLs like example.com/?p=123 and we called those ‘ugly’ permalinks. This encouraged everyone to make pretty ones, like example.com/2018/post-name/ instead. What many people don’t realize is that those ?p=123 calls are query variables.

    WordPress, and most modern CMS tools, take that pretty permalink, translate it back to a query variable, and go get the post ID with 123. The variable p (which stands for post) has a value of 123. It makes sense.

    And you can make your own.

    Pretty URLs Are Better

    If you have a choice of telling people to visit example.com/?p=123 or example.com/2018/post-name/, I feel confident most of you will pick the latter. And if you’re writing code and you need a consistent location to call, would you rather try to assume that everyone’s using wp-content and look for example.com/wp-content/plugins/myplugin/my-file.php or would you rather use example.com/myplugin/?

    Now. This post is not about how to make those awesome virtual pages. There are some good tutorials like Metabox.io’s to get you started there. This is about when you have a slightly different scenario.

    Using Query Variables in Titles

    For a number of reasons, I have a page built out to manage my custom query calls. This allows me to edit the content of the page like it was, well, a page! Anyone can edit the page content, change the information, and then everything else is magically generated, it can be easily tweaked to fit my theme, and it basically works for me.

    What doesn’t work well is that all the pages have the same title. And that, as we all know, is bad SEO. I don’t want all my pages to have the same title, and it’s confusing for users because they go to example.com/my-thing/sub-thing/ and example.com/my-thing/other-thing/ so logically the page title should be different, right?

    Changing the display title on the page isn’t too terrible. I have this code at the top of the page to grab the query variable:

    $my_thing = ( isset($wp_query->query['my_thing'] ) )? $wp_query->query['my_thing'] : 'main' ;
    

    And then when I echo the title, I do this:

    <h1 class="entry-title">My Thing
    	<?php 
    		$title = ( 'main' !== $my_thing )? ' on ' . ucfirst( $my_thing ) : 's';
    		echo $title;
    	?>
    </h1>
    

    Which means my titles are either “My Things” or “My Thing on That”. If I had multiple ‘words’ in my query variables (separated by a – of course) then I’d use a string replace and capitalize each word with ucwords().

    But Page Titles aren’t Title Titles

    The problem is the page title isn’t the … well … title title. Have you ever looked at your browser bar? Assuming you don’t have 94 tabs open at once…

    A browser with a lot of tabs open, to the point you may have no idea what's in any of them...

    Then your browser would look a little like this:

    A sane number of tabs open, where you can read the title of each tab

    Imagine if they all said “My Things | Half-Elf on Tech”? Well that would suck. It sucks for you reading, it sucks for screenreaders, and if you have Yoast SEO, it’s quite easy to fix.

    Custom Yoasty Variables and Titles

    Step one is to revisit something I did two years ago, making a custom Yoast SEO variable. This time I want to make a variable for %%mything%%:

    wpseo_register_var_replacement( '%%mything%%', array( $this, 'yoast_retrieve_mything_replacement' ), 'basic', 'The type of thing page we\'re on.' );
    

    And the code will be to grab the query variable and parse it:

    function yoast_retrieve_mything_replacement( ) {
    	$my_thing = get_query_var( 'my_thing', 'none' );
    	$return   = ( 'none' !== $my_thing )? 'on ' . ucfirst( $my_thing ) : '';
    	return $return;
    }
    

    Step 2 is going into the page, popping open the snippet editor, and making the title this:

    The Yoast Snippet

    And then? Pour a beer and watch some sports.

  • Caching Dismissible Alerts With localStorage

    Caching Dismissible Alerts With localStorage

    There are a lot of reasons to want to have a temporary, but dismissible, alert on the front end of your website. And there are a million different ways to do that. The trick is … how do you do that and make your site remain cacheable?

    Web Storage (Maybe) Is the Answer

    I preface this with a warning. I’m going to be using Local Storage, and that is not something I generally advocate. Web storage (aka DOM storage) is the practice of storing data in the browser. You can do this in a few other ways, such as sessions or cookies. But both of those can be detrimental to caching. After all, PHP Sessions and cookies tell systems like Varnish and Nginx “Don’t cache this.” They’re also persistent, so if I get a cookie once, I keep it until it expires or I delete it.

    On the other hand, web storage is outside the website entirely, that is my server has no idea about them, and it doesn’t really impact caching at all. Varnish, Nginx, and all those sorts of server-based services don’t care about it, and if implemented properly, there’s no problem. You can store the data locally or per-session, which means ‘forever’ or ‘for as long as this browser window is open.’

    Don’t Use localStorage (Most of the Time)

    That all sounded awesome. So then why are there so many articles advocating you not use localStorage? Well there are some massive caveats:

    1. It’s pure javascript, so PHP doesn’t easily get it
    2. If you store sensitive data in it, you’re a numpty
    3. It can ‘only’ store 5 megs
    4. The content has no expiration
    5. You can only use string data
    6. All your localStorage is loaded on every single page load
    7. Any other javascript can see it and play with it

    That’s starting to sound squidgy, right? Randall Degges has a good writeup of the drawbacks.

    Well good news here. This use is possibly the only time I’ll ever advocate it’s use. I’m going to us it here because it works with caching, it works with most browsers (IE8+), and the worst case scenario here is that people will always see my alert.

    Pull Up With Bootstrap

    I’m using Bootstrap on this particular site, and it makes my life hella easy because they built in dismissible alerts. But the gotcha? Those alerts aren’t persistent. Which means if I want an alert to go away forever, then I’m SOL.

    Except I’m not. Bootstrap includes a way to run an ‘action’ on dismiss, which means I could do something like this:

    jQuery(document).ready(function($) {
        var gdpr = localStorage.getItem('gdpr-alerted') || '';
    
        if (gdpr = 'yes') {
            $('#myAlert').alert('close');
        }
    
        $('#myAlert').on('closed.bs.alert', function () {
           localStorage.setItem('gdpr-alerted','yes');
        })
    }
    

    What this does is it sets a variable (gdpr) based on the content of my item in localStorage (gdpr-alerted). If that value is yes, it closes the alert. Otherwise, it sets it to yes when someone closes the alert.

    That actually works just fine, but it has a weird blip if the javascript is loaded in the footer, where you would see my alert for a split second before the page finished loading. So I decided to go another way, and factor in some expirations.

    The (GDPR Related) Code

    First up the PHP:

    function my_gdpr_footer(){
    	if ( !is_user_logged_in() ) {
    		?>
    		<div id="GDPRAlert" class="alert alert-info alert-dismissible fade collapse alert-gdpr" role="alert">
    			This site uses cookies for stuff. Keep browsing and we keep tracking. Wanna know more? <a href="/terms-of-use">Read our Terms of Use</a>.
    			<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    		</div>
    		<?php
    	}
    }
    add_action( 'wp_footer', 'my_gdpr_footer', 5 );
    
    function my_scripts() {
    	wp_enqueue_script( 'bootstrap', get_template_directory_uri() . '/inc/bootstrap/js/bootstrap.min.js', array( 'jquery' ), '4.1.1', 'all', true );
    	wp_enqueue_script( 'lwtv-gdpr', get_template_directory_uri() . '/inc/js/gdpr.js', array( 'bootstrap' ), '1.0', 'all', true );
    }
    add_action( 'wp_enqueue_scripts', 'my_scripts' );
    

    Now my sneaky JS:

    // jQuery dismissable - sets localStorage a year
    
    jQuery(document).ready(function($) {
    	var gdpr = localStorage.getItem('gdpr-alerted') || '';
    	var year = new Date().setFullYear(new Date().getFullYear() + 1);
    
    	if (gdpr < new Date()) {
    		$('#GDPRAlert').addClass('show');
    		localStorage.removeItem('gdpr-alerted');
    	}
    
    	$('#GDPRAlert').on('closed.bs.alert', function () {
    		localStorage.setItem('gdpr-alerted',year);
    	})
    });
    

    The reason this works is that I remove the show class from the PHP and by default assume it shouldn’t show. Then in the javascript, I set up the value as ‘a year from now’ instead of ‘yes’, and check. If the localStorage value is less than ‘now’, then it’s expired and we should show the class (and delete the storage). Otherwise, it’s the same old same old.

    But Should We?

    That’s the big question though. Is this the best way to go about it? The next best choice would be IndexedDB, however that is not supported by ‘enough’ browsers yet, so yes. For now, this is the best choice.

  • It’s Just Math: WP-CLI Edition

    It’s Just Math: WP-CLI Edition

    Remember how I talked about doing math on a post when it was saved?

    Well what if I wanted to run that at another time? Like what if I knew I needed to update one show and I didn’t want to go in and save the post?

    I can to this with WP-CLI:

    WP CLI Magic

    class WP_CLI_MySite_Commands extends WP_CLI_Command {
    	/**
    	 * Re-run calculations for specific post content.
    	 * 
    	 * ## EXAMPLES
    	 * 
    	 *		wp mysite calc actor ID
    	 *		wp mysite calc show ID
    	 *
    	*/
    	
    	function calc( $args , $assoc_args ) {
    
    		// Valid things to calculate:
    		$valid_calcs = array( 'actor', 'show' );
    		
    		// Defaults
    		$format = ( isset( $assoc_args['format'] ) )? $assoc_args['format'] : 'table';
    
    		// Check for valid arguments and post types
    		if ( empty( $args ) || !in_array( $args[0], $valid_calcs ) ) {
    			WP_CLI::error( 'You must provide a valid type of calculation to run: ' . implode( ', ', $valid_calcs ) );
    		}
    
    		// Check for valid IDs
    		if( empty( $args[1] ) || !is_numeric( $args[1] ) ) {
    			WP_CLI::error( 'You must provide a valid post ID to calculate.' );
    		}
    
    		// Set the post IDs:
    		$post_calc = sanitize_text_field( $args[0] );
    		$post_id   = (int)$args[1];
    
    		// Last sanitity check: Is the post ID a member of THIS post type...
    		if ( get_post_type( $post_id ) !== 'post_type_' . $post_calc . 's' ) {
    			WP_CLI::error( 'You can only calculate ' . $post_type . 's on ' . $post_type . ' pages.' );
    		}
    
    		// Do the thing!
    		// i.e. run the calculations
    		switch( $post_calc ) {
    			case 'show':
    				// Rerun show calculations
    				MySite_Show_Calculate::do_the_math( $post_id );
    				$score = 'Score: ' . get_post_meta( $post_id, 'shows_the_score', true );
    				break;
    			case 'actor':
    				// Recount characters and flag queerness
    				MySite_Actor_Calculate::do_the_math( $post_id );
    				$queer = ( get_post_meta( $post_id, 'actors_queer', true ) )? 'Yes' : 'No';
    				$chars = get_post_meta( $post_id, 'actors_char_count', true );
    				$deads = get_post_meta( $post_id, 'actors_dead_count', true );
    				$score = ': Is Queer (' . $queer . ') Chars (' . $chars . ') Dead (' . $deads . ')';
    				break;
    		}
    
    		WP_CLI::success( 'Calculations run for ' . get_the_title( $post_id ) . $score );
    	}
    }
    

    What the What?

    The one for shows is a lot simpler. I literally call that do_the_math() function with the post ID and I get back a number. Then I output the number and I’m done. If I wanted it to run for all shows, I could use WP-CLI to spit out a list of all the IDs and then pass them to the command one at a time. Or I could write one that does ‘all’ posts. Which I may

    But the point is that I now can type wp mysite calc show 1234 and if post ID 1234 is a show, it’ll run.

  • jQuery Tablesorter

    jQuery Tablesorter

    So you want to sort tables on your WordPress site, without learning a whole mess of code.

    Good news. There’s a plugin called Table Sorter that can do this and in pretty much just works. Except… There are cases where you’re outputting data in a theme (or a plugin) and you can’t use that plugin to do it.

    Don’t panic, we’ve got you.

    Enqueue The Right Tablesorter

    I’m aware there’s a tablesorter.com – Don’t use it. It’s out of date at the site is possibly hacked. Instead, grab Tablesorter from RobG (aka mottie). Rob is still updating this plugin and debugging it, so it’s a better bet that the various other forks.

    You’ll enqueue this the ‘normal’ way:

    wp_enqueue_script( 'tablesorter', plugins_url( 'jquery.tablesorter.js', __FILE__ ), array( 'jquery' ), '2.30.5', false );
    

    There’s a second part to the enqueue though. You see you also need to tell it what to sort. That is, tell the javascript what to pay attention to.

    That’s done by using a class and an ID: <table id="myTable" class="tablesorter"></table>

    If you’re using the plugin I mentioned above, you only have to do the latter, because it accounts things differently but, properly, you should be using the ID. Then you have to insert this javascript:

    $(function() {
      $("#myTable").tablesorter();
    });
    

    Which is actually wrong. For WordPress. Again, no panicking!

    jQuery(document).ready(function($){
      $("#myTable").tablesorter();
    });
    </script>
    

    See? That was easy. If you wanted to be more WordPressy, you do this:

    wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#nationsTable").tablesorter(); });' );
    

    You were expecting more?

    That’s really it. I do some extra weird stuff, since I call it on one page only (statistics) and that pages uses query variables so you can have /statistics/nations/ without me needing to make multiple sub pages, and it looks like this:

    	function enqueue_scripts() {
    
    		if ( is_page( array( 'statistics' ) ) ) {
    			$statistics = get_query_var( 'statistics', 'none' );
    			wp_enqueue_script( 'tablesorter', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/jquery.tablesorter.js' , array( 'jquery' ), '2.30.5', false );
    			wp_enqueue_style( 'tablesorter', plugin_dir_url( dirname( __FILE__ ) ) . 'assets/css/theme.bootstrap_4.css' );
    
    			switch( $statistics ) {
    				case 'nations':
    					wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#nationsTable").tablesorter({ theme : "bootstrap", }); });' );
    					break;
    				case 'stations':
    					wp_add_inline_script( 'tablesorter', 'jQuery(document).ready(function($){ $("#stationsTable").tablesorter({ theme : "bootstrap", }); });' );
    					break;
    			}
    		}
    	}
    

    Oh right that also demonstrates a theme!

    Tablesorter lets you use themes like Bootstrap 4.x so your tables can be all matchy-matchy.

    But at this point, it should be enough to get your tables sorted.