Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • Admin Alerts on Posts

    Admin Alerts on Posts

    As the new WordPress.org plugin directory rolls out, I know a lot of people are fussing over the front end. I’m not for a very practical reason. I’m working on making the backend functional and educational.

    Information Is Key

    A lot of the information about what a various post status means is something I understand from experience. My goal is to expand the team as broadly as possible and in order to do that, I must document. But documentation is more than just writing up a doc and telling people to follow it.

    People need contextual reminders of what the status of a ‘thing’ is to understand what they must do next. With that in mind, I leveraged some code by Obenland to make reviewer alerts better. Originally, you see, our plan was to let plugin developers edit things from the admin panel. Everything’s moving to the front end except for the reviewers you see.

    On Beyond Plugins

    That’s all well and good, but what if I want to do it on my own sites? What if I wanted to have a site message for the editors on my site, so they’d know what posts are ‘done’ and which need some love? What if I wanted to encourage my fellow editors to edit a post I’d worked on?

    I made my plan, based off the old stub template from MediaWiki, because that was always a good way to know if a post did or didn’t need some love.

    1. I needed a way to ‘flag’ a post as a stub automatically
    2. I needed it to display on the page being edited
    3. I would like it to display on the front end but only to logged in users (for now)

    Stub Qualifications

    In order to flag a post automatically, I needed to come up with criteria or qualifications that would mark a post as a stub. Obviously content was part of it, but measuring the quality of the content was going to be difficult.

    My initial criteria for a stub was the following:

    • Under 100 words in post content
    • Empty meta value for “Worth it details”

    Obviously that second item is unique to my situation, but having it meant I could measure engagement by how much someone knew about a show to enter the data. That criteria seemed too strict though. So I tweaked them to be an OR and not an AND. No matter what, if there was under 100 words it was getting flagged. And no matter what, if there was no meta for worthiness, it would be flagged.

    • Info: No meta value for Worth It
    • Warning: No meta value for Worth It and post content under 200 words
    • Alert: No meta value for Worth It and post content under 100 words

    WP Admin Code

    The original code that all this is based on is from WordPress.org (not the WordPress code but the stuff that runs the domain).

    add_action( 'edit_form_after_title', 'MYSITE_admin_notices' );
    function MYSITE_admin_notices() {
    	$message    = '';
    	$type       = 'updated';
    	$post       = get_post();
    
    	switch ( $post->post_type ) {
    		case 'post_type_shows':
    
    			$content    = get_post_field( 'post_content', $post->ID );
    			$word_count = str_word_count( strip_tags( $content ) );
    			$worthit    = get_post_meta( $post->ID, 'worthit_details', true );
    
    			if ( $worthit < '1' ) {
    				$type     = 'notice-info';
    				$message  = 'Is this show worth watching? We don\'t know. Halp!';
    				$dashicon = 'heart';
    
    				if ( $word_count < '100' ) {
    					$type     = 'notice-error';
    					$message  = 'We clearly know nothing about this show. Help!';
    					$dashicon = 'warning';
    				} elseif ( $word_count < '200' ) {
    					$type     = 'notice-warning';
    					$message  = 'This post is a stub. Please edit it and make it more awesome.';
    					$dashicon = 'info';
    				}
    			}
    			break;
    	}
    
    	if ( $message ) {
    		printf( '<div class="notice %1$s"><p><span class="dashicons dashicons-%2$s"></span> %3$s</p></div>', esc_attr( $type ), esc_attr( $dashicon ), esc_html( $message ) );
    	}
    
    }
    

    The reason it uses edit_form_after_title and prints the error instead of add_settings_error is that I don’t actually want these alerts to be dismissible and I only want it to show on post editor pages.

    The Output

    This post needs some attention

    There’s some improvements to be made, but this is a start.

  • Multi Faceted Connections

    Multi Faceted Connections

    That’s a pun because I’m using FacetWP y’all.

    As you probably know by now, I have a site that has a lot of weird data. And one of the problems I ran into with FacetWP and my site was that I saved data in a serialized manner.

    Simple Arrays

    Since characters can have multiple actors, the data is saved in a serialized array like this: a:1:{i:0;s:13:"Lucy Lawless";}

    I want to be able to search for all the characters Lucy Lawless plays, even if someone else also played the role, so to do that I need to tell FacetWP to save the data twice, an entry for each actor. To do that, I use this code:

    add_filter( 'facetwp_index_row', 'filter_facetwp_index_row', 10, 2 );
    function filter_facetwp_index_row( $params, $class ) {	
    	// Actors
    	// Saves one value for each actor
    	if ( 'char_actors' == $params['facet_name'] ) {
    		$values = (array) $params['facet_value'];
    		foreach ( $values as $val ) {
    			$params['facet_value'] = $val;
    			$params['facet_display_value'] = $val;
    			$class->insert( $params );
    		}
    		return false; // skip default indexing
    	}
    	return $params;
    }
    

    That saves two entries in the FacetWP table like this:

    An example of two actors for one character

    Complex Arrays

    Buuuuut I also have a complex array where I list a show’s airdates: a:2:{s:5:"start";s:4:"1994";s:6:"finish";s:4:"2009";}

    Now that doesn’t look too weird, I know, but the problem is I wanted to be able to compare the start and end dates, so you could get a list of, say, all shows that were on air between 1950 and 1960 (two, by the way). In order to do that, I had to break the array apart into not only two values, but two separate sources!

    In order to make that work, I do this:

    // Airdates
    // Splits array value into two sources
    if ( 'show_airdates' == $params['facet_name'] ) {
    	$values = (array) $params['facet_value'];
    
    	$start = ( isset( $values['start'] ) )? $values['start'] : '';
    	$end   = ( isset( $values['finish'] ) )? $values['finish'] : date( 'Y' );
    
    	$params['facet_value']         = $start;
    	$params['facet_display_value'] = $start;
    	$class->insert( $params );
    
    	$params['facet_source']        = 'cf/lezshows_airdates_end';
    	$params['facet_name']          = 'show_airdates_end';
    	$params['facet_value']         = $end;
    	$params['facet_display_value'] = $end;
    	$class->insert( $params );
    	
    	return false; // skip default indexing
    }
    

    That gives me two database entries like so:

    An example of two values in two separate sources

    The reason this is done is because I have a facet that compares the datasets for lezshow_airdates_end with lezshow_airdates and if the numbers are between them, that’s what it shows.

    And this works because of this filter:

    // Filter Facet sources
    add_filter( 'facetwp_facet_sources', function( $sources ) {
        $sources['custom_fields']['choices']['cf/lezshows_airdates_end'] = 'Airdates End';
        return $sources;
    });
    

    That creates a new custom field based on the values in cf/lezshows_airdates_end so I can compare between the two. And with a snazzy slider, I can do this:

    Aired Between ... as a slider

  • I’ll Know A Duck When I See It

    I’ll Know A Duck When I See It

    After I complained about the new SEO scam, someone pointedly argued it wasn’t spam. And it wasn’t a scam.

    It is and it’s both.

    What Is Spam?

    By it’s most basic definition, spam is an irrelevant or otherwise inappropriate message, sent on the internet, to a large group of people.

    With that definition in hand, someone who interrupts a Slack or IRC meeting to tell a joke is spamming. At the same time, Tweeting inanities is not unless you cut into a conversation thread. And the different being that Twitter is always irrelevant so any comment there is expected to be appropriately inappropriate.

    Spam is More Than Spam

    The issue is that spam has expanded to be more than just that simple blast of junk you didn’t care about. Spam now includes things like being added to an email list you didn’t want to join. And it includes people trying to rip you off.

    A scam is an attempt to get something from you. The end goal of a lot of spam is to scam you out of money, so the intersection there is pretty high. It always has been. The result of a spambot is to convince you to do something you didn’t want, in order to get something you have. But the target of scams is to out and out separate you from your money.

    If It Looks Like a Duck …

    You’ve probably heard of the duck test.

    If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

    When people read an email from some Nigerian prince, they know it’s spam because they’ve seen things like it before. But they also know it’s a scam because they’ve been taught that no one offers something for nothing.

    Unsolicited Emails Are Ducks

    If you get an email you didn’t ask for, from someone you’ve never heard of, offering something that’s too good to be true (like ‘free backlinks’), it’s a duck.

    If you look at the emails from these people who offer to help you fix your site and improve your links to broken locations, it’s a duck. It’s a scam, it’s spam, and you should delete it. Don’t even ask.

  • FacetWP: Making Sorting Suck Less

    FacetWP: Making Sorting Suck Less

    Sorting data in WordPress is generally done in the most basic of ways. You want to see all posts that are in a specific category, you go to example.com/category/drinks/ and there you are. But if you want to see everything in the category ‘drinks’ with the tag ‘bourbon’ and the custom taxonomy of ‘ingredients’ and a value of ‘mint’ AND ‘simple syrup’ to get the recipe for a mint julep, then you have a pretty crazy complex query.

    Enter FacetWP

    FacetWP is a premium plugin that, for $79 a year, handles all that crazy sorting for you. And yes, it’s worth it.

    FacetWP introduces advanced filtering to WordPress, which lets you do things like get that list of all drinks made with bourbon that include a simple syrup, in a dynamic way! It’s incredibly fast, since it’s using ajax and javascript, and as long as you have enough server memory to index all the data in the first place, it’s faster than reloading a new category page.

    Downsides

    In order to be that fast, you do not get pretty URLs. Let’s say you have your drinks category at `example.com/category/drinks’ and you want to list all those things. Your URL will look like this:

    example.com/category/drinks/?fwp_alcohol=bourbun&fwp_ingredients=simple+syrup%2Cmint

    The realistic reason they don’t try to make it ‘pretty’ is that it would create a lot more rewrite rules than would be sustainable, if you have a lot of facets. The number of checks would slow your site down, and that would kind of suck.

    Compatibility Notes

    If you use CMB2 you’ll need FacetWP + CMB2.

    If you use Genesis themes, there are two tricks. First, you’ll want to use the following function to add FacetWP’s CSS to your theme:

    function YOURTHEMENAME_facetwp_class( $atts ) {
        $atts['class'] .= ' facetwp-template';
        return $atts;
    }
    add_filter( 'genesis_attr_content', 'YOURTHEMENAME_facetwp_class' );
    

    Second, if you’re like me and you use a lot of custom loops, they may not behave as expected. If you call the loop multiple times on a page (which is bad behavior in the first place and I know it), FacetWP has a bit of trouble knowing what javascript to apply to what section. That should be expected, and once I cleaned it up, it worked great.

    Should you use it?

    If you have a lot of complex intersectional queries to sort through, yes.

    If you need dynamic result updates, yes.

    It works.

  • Sharing WordPress Content with any PHP App

    Sharing WordPress Content with any PHP App

    Last week I explained how I shared my WordPress content with Hugo. Now, that was all well and good, but there is an obvious way I can do this when I don’t have a tool that understands dynamic content?

    I’ve got PHP so the answer is “You bet your britches, baby!”

    The First Version

    If you just want to grab the URL and output the post content, you can do this:

    $curl = curl_init();
    curl_setopt_array( $curl, array(
    	CURLOPT_FAILONERROR    => true,
    	CURLOPT_CONNECTTIMEOUT => 30,
    	CURLOPT_TIMEOUT        => 60,
    	CURLOPT_FOLLOWLOCATION => false,
    	CURLOPT_MAXREDIRS      => 3,
    	CURLOPT_SSL_VERIFYPEER => false,
    	CURLOPT_RETURNTRANSFER => true,
    	CURLOPT_URL            => 'https://example.com/wp-json/wp/v2/pages/12345'
    ));
    
    $result = curl_exec( $curln);
    curl_close( $curl );
    
    $obj = json_decode( $result );
    
    echo $obj->content->rendered;
    

    Now this does work, and it works well, but it’s a little basic and it doesn’t really sanitize things. Plus I would be placing this code in multiple files per site (not everyone themes as nicely as WordPress, ladies and gentlemen). So I wanted to write something that was more easily repeatable.

    The Advanced Code

    With that in mind, I whipped up a PHP file that checks and validates the URL, makes sure it’s a wp-json URL, makes sure it’s JSON, and then spits out the post content.

    <?php
    
    /* This code shows the content of a WP post or page.
     *
     * To use, pass the variable ?url=FOO
     *
     */
    
    if (!$_GET || !$_GET["url"]) return;
    
    include_once( "StrictUrlValidator.php" );
    
    $this_url = (string) filter_var( $_GET['url'], FILTER_SANITIZE_URL );
    
    if (strpos( $this_url, 'wp-json') == FALSE ) return;
    
    function do_curl ( $url ) {
    	$curl = curl_init();
    
    	curl_setopt_array( $curl, array(
    		CURLOPT_FAILONERROR    => true,
    		CURLOPT_CONNECTTIMEOUT => 30,
    		CURLOPT_TIMEOUT        => 60,
    		CURLOPT_FOLLOWLOCATION => false,
    		CURLOPT_MAXREDIRS      => 3,
    		CURLOPT_SSL_VERIFYPEER => false,
    		CURLOPT_RETURNTRANSFER => true,
    		CURLOPT_URL            => $url
    	) );
    
    	return curl_exec( $curl );
    	curl_close( $curl );
    }
    
    if ( StrictUrlValidator::validate( $this_url, true, true ) === false ) {
    	$return = "ERROR: Bad URL";
    } else {
    	$obj = json_decode( do_curl ( $this_url ) );
    
    	if ( json_last_error() === JSON_ERROR_NONE ) {
    		$return = $obj->content->rendered;
    	} else {
    		$return = "ERROR: Bad JSON";
    	}
    }
    
    echo $return;
    

    You can see I have some complex and some basic checks in there. The URL validation is done via a PHP Library called StrictUrlValidator. If I was using WordPress, I’d have access to esc_url() and other nifty things, but since I’m running this out in the wild, I make do with what I have.