Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

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

  • A Case Sensitive Headache

    A Case Sensitive Headache

    Like most people, I pull my site down to develop locally. After all, it’s safer. But also like most people, my live machine is a linux box and my personal machine is not. In fact, it’s a Mac, running whatever the latest OS X is. But don’t worry, today’s drama happens on Windows as well.

    You see, I downloaded my local data, imported my database, changed my URLs to local, and stared at a page that had the wrong data. Or rather, the right data and the wrong image. The page for “Sam” was showing me a photo for another character named “Sam.” But on my live site it was fine.

    Commence the Debugging

    The first thing I did was copy the image to a new folder to try and re-upload it. Only that didn’t work out the way I thought it would. You see, I knew the filename was Sam.jpg so I tried to copy that over:

    cp ~/sites/example.com/wp-content/uploads/2018/05/Sam.jpg ~/Downloads

    Only when I went to look, it was again the bad image. In fact, when I looked at all the files in the folder, there was only one file named ‘sam’ when there should be two

    A list of files in local and live sites. The one of the left is local, and it's missing a bunch of files.
    Local site is on the left, live is on the right

    Things were starting to become clear. Next I copied it and renamed it sam.jpg and got an error:

    Error: The name "sam" with extension ".jpg" is already taken. Please choose a different name.

    I’ve got it now. Mac doesn’t understand the different between upper and lower case. It’s not case sensitive.

    Bad News: No Easy Fix

    I’m really sorry about this. There isn’t an easy fix.

    First of all, if you wanted to fix your main hard drive, you would have to reformat it. That’s a pain in the ass. Second? Your Mac will not be happy and things will break. For example, the Steam app doesn’t like it if your Mac is case sensitive. Seriously, I have no idea why. But it’s what it expects.

    This means the ‘fix’ is to partition your hard drive. Since I’m using AFPS, I opted to make a volume instead of a partition, which feels pretty much the same but it’s not. I opted for AFPS with case sensitive and encrypted because I’m generally a neurotic. Making a volume is very fast, and once I was done, I copied everything in my ~/Sites/ folder over to /Volumes/websites/ (where websites is the name of my new volume).

    And now it looks great!

    Mac OS screenshot showing the files are now case sensitive! Yay!

    Except… I still only see one in terminal. I freaked out for a moment and then I realized that while the GUI is smart enough to know that Sam and sam are both S’s, terminal put the capital letters above the lowercase. So all the S’s came before all the s’s.

    Miss Piggy bashing her head into a table. Which is how I felt about now.

    The last step was to move my copy of Chassis over to the new websites volume, spin it back up, and finally it was working properly.

    The Moral?

    Don’t use case-sensitive filenames. Or filenames with special characters like á or í because operating systems are stupid.

    No seriously, that’s it. Haven’t you wondered why people advocate we all use all lower-case for filenames in our code repositories? Because different operating systems are stupid. They don’t always talk properly to each other, they have different ideas of right and wrong, and they’re never going to agree. If you work with multiple operating systems, aim at the lowest common denominator, pick a style, and stick the hell to it.

    That’s your moral.