Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: shortcode

  • Shortcode Example: Reviews

    Shortcode Example: Reviews

    Sometimes people want to have reviews mean ‘people leave reviews on my work.’ But the other kind of reviews are the ones where I review other peoples’ works. And for that, I found it helps to have some kind of standard.

    Let’s say I’m reviewing a TV show for overall quality but also overall gayness. That is, I want to be able to write up a post and then, at the bottom, put up a shortcode to say “This show is really good but has no gay characters.” To do that, I made a list of the important factors to distill:

    • Name: The name of the TV show
    • Summary: A short, 140 character summary of the overall show.
    • Queer: A 1-5 rating of how queer the show is
    • Rating: A 1-3 scale (yes, meh, no) for how good the show is overall
    • Warning: Is there a trigger warning (or CW) to be aware of

    The intent is to make it easy for someone to scroll down and find what they want to watch. Right?

    The Code

    A bit of warning. There are two specific to my site bits of design in here. First are the icons. While I’ve generalized them for you as Emoji, keep in mind you probably want to have your own style here. Second, I’m using Bootstrap, so I’ve leveraged some of their default code. You’ll want to tweak the CSS.

    class TV_Shortcodes {
    
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    		add_action( 'init', array( $this, 'init' ) );
    		add_filter( 'widget_text', 'do_shortcode' );
    	}
    
    	/**
    	 * Init
    	 */
    	public function init() {
    		add_shortcode( 'review', array( $this, 'review' ) );
    	}
    
    	/**
    	 * Reviews.
    	 */
    	public function review( $atts ) {
    
    		$attributes = shortcode_atts( array(
    			'title'   => 'Coming Soon',
    			'summary' => 'Coming soon ...',
    			'queer'   => '3',
    			'rating'  => 'meh',
    			'warning' => 'none',
    		), $atts );
    
    		$queer = (float) $attributes['queer'];
    		$queer = ( $queer < 0 )? 0 : $queer;
    		$queer = ( $queer > 5 )? 5 : $queer;
    
    		$worth = ( in_array( $attributes['worth'], array( 'yes', 'no', 'meh' ) ) )? $attributes['worth'] : 'meh';
    		switch ( $worth ) {
    			case 'yes':
    				$worth_icon = '??';
    				$worth_color = 'success';
    				break;
    			case 'no':
    				$worth_icon  = '??';
    				$worth_color = 'danger';
    				break;
    			case 'meh':
    				$worth_icon  = '?';
    				$worth_color = 'warning';
    				break;
    		}
    
    		// Get proper triger warning data
    		$warning = '';
    		$trigger = ( in_array( $attributes['trigger'], array( 'high', 'medium', 'low' ) ) )? $attributes['trigger'] : 'none';
    
    		if ( $trigger != 'none' ) {
    			$warn_image    = '⚠️';
    			switch ( $trigger ) {
    				case 'high':
    					$warn_color = 'danger';
    					break;
    				case 'medium':
    					$warn_color = 'warning';
    					break;
    				case 'low':
    					$warn_color = 'info';
    					break;
    			}
    
    			$warning = '<span data-toggle="tooltip" aria-label="Warning - This show contains triggers" title="Warning - This show contains triggers"><button type="button" class="btn btn-' . $warn_color . '"><span class="screener screener-warn ' . $warn_color . '" role="img">' . $warn_image . '</span></button></span>';
    		}
    
    		$output = '<div class="bd-callout"><h5 id="' . esc_attr( $attributes['title'] ) . '">Screener Review on <em>' . esc_html( $attributes['title'] ) . '</em></h5>
    		<p>' . esc_html( $attributes['summary'] ) . '</p>
    		<p><span data-toggle="tooltip" aria-label="How good is this show for queers?" title="How good is this show for queers?"><button type="button" class="btn btn-dark">Queer Score: ' . $queer . '</button></span> <span data-toggle="tooltip" aria-label="Is this show worth watching? ' . ucfirst( $worth ) . '" title="Is this show worth watching? ' . ucfirst( $worth ) . '"><button type="button" class="btn btn-' . $worth_color . '">Worth It? <span role="img" class="screener screener-worthit ' . lcfirst( $worth ) . '">' . $worth_icon . '</span></button></span> ' . $warning . '</p>
    		</div>';
    
    		return $output;
    
    	}
    }
    new TV_Shortcodes();
    

    The Future

    I’m thinking about changing the scores from numbers to stars, and adding in a link if the show has been added to the site. But it being a shortcode, it’s reasonably extensible.

    Enjoy, and export to your own review sites.

  • Always Validate Data

    Always Validate Data

    In the making of my REST API plugin’s extension, I had written out a way to select what happened on a specific date. While adding that kind of code into a shortcode was something I’d done before, I struggled to try and understand how I could do that with a widget in a way that was safe.

    On Data Validation

    The main function that does all the real work expects a date parameter in the format of MM-DD which it converts to YYYY-MM-DD so I can run date() calls on it later on. The reason that works is that the PHP date() function can parse the YYYY-MM-DD format on it’s own. To that end, I have to write the widget and shortcode in such a way that it’s impossible to pass on bad data.

    This is a matter of an age old problem: sanitize, escape, and validate.

    If you’ve written a plugin and think that sounds familiar, yes, it is a common sticking point of mine. You must sanitize your data before you save it. You must escape it before you display it. You should validate it to make sure you’re not letting people make silly mistakes.

    The ‘problem’ is that there are a lot of ways to do that, and the ultimate question is this “What is the data I’m trying to save supposed to look like?” If it’s a URL, you sanitize for a URL. If it’s a number, you sanitize for a number. That’s the easy part. It’s just really time consuming and difficult. Still. Sanitize early, escape late, and always validate.

    The Main Function Sanitizes and Validates

    Instead of only having the shortcode or the widget make sure the date was right, I decided to attack the matter further up the chain. Or down the chain, depending on your point of view. Both the shortcode and widget call the same master function so for that, I make sure that the value of $this_day is never anything than what I want:

    	public static function on_this_day( $this_day = 'today' ) {
    		$this_day = sanitize_text_field( $this_day );
    		if ( $this_day !== 'today' ) {
    			$month = substr( $this_day, 0, 2);
    			$day = substr( $this_day, 3, 2);
    			$this_day = ( checkdate ( $month, $day , date('Y') ) == true )? $this_day : 'today' ;	
    		}
    
    	[... the rest of the code continues on]
    	}
    

    By using checkdate(), I verify that the value is either a valid date or I force it to be ‘today’. This prevents someone from passing bad data in the shortcode. I sanitize it as well as validate, using sanitize_text_field() because there’s no reason to even allow non-safe data anywhere further in my code.

    The Shortcode Validates and Sanitizes

    The way a shortcode works in WordPress is you can pass attributes to it and the shortcode parses that for you. My shortcode is pretty basic: [on-this-day date="MM-DD"] But since a shortcode lives in the world of ‘users can enter whatever they want’, I have to play Captain Beaver Dam and stop them from using [on-this-day date="Elvis"] for example.

    I do this by sanitizing the attribute and then validating it’s the format I think it is:

    	public function on_this_day_shortcode( $atts = [] ) {
    		$attributes = shortcode_atts([
    			'date' => 'today',
    		], $atts);
    
    		$this_day = sanitize_text_field($attributes['date']);
    		if ( $this_day !== 'today' ) {
    			$month = substr( $this_day, 0, 2);
    			$day = substr( $this_day, 3, 2);
    			$this_day = ( checkdate ( $month, $day , date('Y') ) == true )? $this_day : 'today' ;
    		}
    		$onthisday = $this->on_this_day( $this_day );
    		return $onthisday;
    	}
    

    If you’re thinking that looks the same as what I do in the main widget, you’re right! I do it here to make sure I’m never sending bad data to the function. I do it in the function since I’m not actually psychic and people have done some pretty weird things. Also they may decide to extend my code one day and write something that calls the function.

    The Widget Validates and Sanitizes

    At this point, you’re expecting this, right? It’s actually more important that the widget do this than anything else. The code on my server won’t accept incorrectly formatted data, so the worst that happens is you get a bad data reply. The reason all this matters more for the widget though is that the widget stores data in the database.

    If you’re saving data and you’re not making it safe, you’re putting yourself at risk. Repeat that until it’s second nature.

    In my widget, there’s an update function that saves the data to the database.

    	function update( $new_instance, $old_instance ) {
    		$new_instance['title'] = strip_tags( $new_instance['title'] );
    
    		$new_instance['date'] = substr( $new_instance['date'], 0, 5);
    		$month = substr( $new_instance['date'], 0, 2);
    		$day = substr( $new_instance['date'], 3, 2);
    		if ( checkdate( $month, $day, date("Y") ) == false ) $new_instance['date'] = '';
    		$new_instance['date']  = strip_tags( $new_instance['date'] );
    
    		return $new_instance;
    	}
    

    You’ll notice that, yet again, I use checkdate() to make sure the date is a valid value. But I’m also using strip_tags() to remove any HTML. You could use wp_strip_all_tags() which is even more hardcore, but in this moment, they both work.

    The Data Is Cleaned Three Ways

    In a way, it’s overkill. I don’t need to validate the data in the shortcode really, because I’m doing that in the main function. I do need to validate in the widget, since I want to make sure I’m not saving bad data, and I don’t want people to see invalid data on the widget settings page. At the same time, by making sure everything is as sane and secure and valid, I’m limiting possible vulnerabilities down the road.

  • Copyright Years for WordPress

    Copyright Years for WordPress

    At the bottom of every page on my site is a little bit of info declaring copyright: “Copyright © 2017 Mika A. Epstein”

    How do I do that and not have to update all my site themes and widgets every year? With code, of course! I have both a function I could use in themes and a shortcode I could use anywhere a shortcode can be used.

    The Code

    There are two functions, the base code and the shortcode. The concept is that if you don’t put in a year (which is the start year for your copyright) it will only show the current year. If you do put in year, it forces it to be an integer and then does a couple checks. The checks were originally as follows:

    1. Is $year ‘auto’? Force this year.
    2. Is $year this year? Force this year.
    3. Is $year equal to 0? Force this year.
    4. Is $year greater than this year? Oh, silly human. Force this year.
    5. Is $year less than this year? Use the ‘start – end’ format

    The reason for this is practical. We’re sanitizing things as early as we can, and then we’re checking for the logical and illogical entries. If someone decides the year is ‘Bob’ then intval() throws a 0 and since 0 isn’t actually a valid year in the Gregorian calendar, then I can do a simple “if 0” check.

    But … I’m not so bold as to assume the only people who will want this are using the Gregorian calendar. To be more universal, I changed the code to make the first check for if the year was set to ‘auto’ (which defaults to this year), or if it was a non-number. If it’s not a number, you get forced this year. Otherwise, the code trusts you.

    function helf_auto_copyright( $year = 'auto' , $text = '' ){ 
    	$year = ( $year == 'auto' || ctype_digit($year) == false )? date('Y') : intval($year);
    	$text = ( $text == '' )? '&copy;' : sanitize_text_field( $text );
    
    	if( $year == date('Y') || $year > date('Y') ) $output = date('Y');
    	elseif( $year < date('Y') )  $output = $year . ' - ' . date('Y');	
    
    	echo $text . ' ' . $output;
    }
    
    function helf_auto_copyright_shortcode( $atts ) {
        $attributes = shortcode_atts( array(
            'year' => 'auto',
            'text' => '&copy;'
        ), $atts );
    
        return helf_auto_copyright( sanitize_text_field($attributes['year']), sanitize_text_field($attributes['text']) );
    }
    add_shortcode( 'copyright', 'helf_auto_copyright_shortcode' );
    

    The one failing here is I only account for the common era (or ‘AD’ for those who didn’t know we all switched to CE a while back). I’m sure this can be extended to BCE if so desired. Spitballing, I’d just use negative numbers, check for them and output ‘year BCE – year CE’ instead. But that’s a little much for this use case.

    Usage

    As a shortcode: Copyright 2016 - 2025

    As a function: helf_auto_copyright_shortcode( '2016', 'Copyright' );

    Both will output the same thing (as of 2017): Copyright 2016 – 2017

    And in 2018? It will magically update for you.

  • Displaying Taxonomy Count

    Displaying Taxonomy Count

    Monday we displayed post counts. Well, what about taxonomies? That’s a little more complicated, I’m afraid.

    Posts are easy. You pick a post type, you display the number of published posts, you walk away. Taxonomies though are a mixed bag. By default you have categories (category) and tags (post_tag) and inside them, you have terms. For example ‘Uncategorized’ is a category. The problem is that to check if a taxonomy exists (and display a post count), you have two have both the taxonomy name and the term name.

    While you think you could just write a loop ‘If the term name isn’t in categories, it’s in tags!’ the reality is that anyone can add any taxonomy and, worse, term names aren’t unique. It’s ironic here, because I desperately wanted term names and slugs to not be unique. I wanted to have a tag for ‘random’ and a category for ‘random’ and they all have the same slug names. So here I am now, realizing I’ve set myself up for disaster.

    The options are simple:

    1. Force people to use term and taxonomy
    2. Somehow be clever

    I went with option 2. Allow people to use term and taxonomy, but if they don’t, find the first instance and go for it.

    The Code

    // [numtax term="term_slug" taxonomy="tax_slug"]
    function numtax_shortcode( $atts ) {
    	$attr = shortcode_atts( array(
    		'term'     => '',
    		'taxonomy' => '',
    	), $atts );
    
    	// Early Bailout
    	if ( is_null($attr['term']) ) return "n/a";
    
    	$the_term = sanitize_text_field( $attr['term'] );
    	$all_taxonomies = ( empty( $attr['taxonomy'] ) )? get_taxonomies() : array( sanitize_text_field( $attr['taxonomy'] ) );
    
    	//$all_taxonomies = get_taxonomies();
    	foreach ( $all_taxonomies as $taxonomy ) {
    	    $does_term_exist = term_exists( $the_term, $taxonomy );
    
    	    if ( $does_term_exist !== 0 && $does_term_exist !== null ) {
    		    $the_taxonomy = $taxonomy;
    		    break;
    	    } else {
    		    $the_taxonomy = false;
    	    }
    	}
    
    	// If no taxonomy, bail
    	if ( $the_taxonomy == false ) return "n/a";
    
    	$to_count = get_term_by( 'slug', $the_term, $the_taxonomy );
    
    	return $to_count->count;
    
    }
    add_shortcode( 'numtax', 'numtax_shortcode' );
    

    There are two moments where I bail out early. If they forgot to put in a term, display “N/A”. The same if we get all the way to the end and there was no found taxonomy.

    Also if someone puts in a taxonomy, I treat it as an array in order to be lazy and not repeat myself. Good coding doesn’t repeat, so since I have to loop the array of found taxonomies in order to find the matchup, I may as well use it once to find the same data when I know what I have.

    I admit, I was really excited here since I finally got to use ternary operations. I’ve known how they work for ages, but I never had a moment where it was so obvious to use them.

  • Displaying Post Count

    Displaying Post Count

    If you need a count of all your posts in WordPress, there’s a pretty handy function called wp_count_posts() for that.

    For example, if you have a post-type of ‘characters’ and you wanted to show a count of that, you can do this:

    $to_count = wp_count_posts( 'characters' );
    printf( __('Total Characters: %s'), $count->publish );
    

    And that’s all well and good, but I was working on some SEO thoughts. In doing so, I ran into the recommendation that, for a ‘company’ type page, having a list of your posts was a bad idea. This made sense. After all, a site that is about non-traditional blog content shouldn’t start with a blog.

    To correct this, I restructured the front page to show, in order:

    1. An introduction to the site
    2. The 4 newest characters added
    3. The 4 newest shows added
    4. The latest 10 blog posts

    This keeps the blog information visible (which helps show that content is being updated) but also doesn’t drop people into a cold open. They can understand why they’re on the site, what they’re getting, and where to go.

    But part of telling people about the site meant I wanted to indicate the depth of information. When I used to run a MediaWiki site, I used {{NUMBEROFARTICLES}} to list the number of articles. It was a very obvious way to display activity and attentiveness. If a site had 500 articles, it was probably getting up there. If it had 1500, it probably knew what it was doing.

    That meant with WordPress what I wanted was a dynamic way to show the number of posts in a post type, like [numberofposts type="characters"] with the default being posts.

    The Code

    function numberofposts_shortcode( $atts ) {
    	$attr = shortcode_atts( array(
    		'type' => 'post',
    	), $atts );
    
    	$posttype = sanitize_text_field( $attr['type'] );
    	if ( post_type_exists( $posttype ) !== true ) $posttype = 'post';
    
    	$to_count = wp_count_posts( $posttype );
    
    	return $to_count->publish;
    
    }
    add_shortcode( 'numberofposts', 'numberofposts_shortcode' );
    

    What I chose to do here was check if the post type exists and, if not, force it to show posts. That way there will never be an error, though it may not show you what you expected if you put in ‘posts’ as your post type.

  • Looping Hugo

    Looping Hugo

    I’m ending the year with something non-WordPress. But it has a weird JSON related journey.

    I asked three questions at the end of my last post:

    1. How do I properly mimic a for-loop with range?
    2. Can I make a responsive gallery out of Hugo?
    3. Can I power a Hugo site with WordPress JSON?

    This is the answer to the first one. However in the solving, I made a breakthrough in my head about how one calls data from JSON and structures it.

    But I’m getting ahead of myself, becuase changing a for loop from Jekyll to Hugo was harder than I expected. Way harder.

    One of the things I check in some posts is what the rating is. I set a parameter called ‘rating’ and if it’s there, I ran a quick check to determine how many stars to show:

    <strong>Rating:</strong> {% if page.rating %}{% for i in (1..page.rating) %}<i style="color:gold;" class="fa fa-star" name="star"></i>{% endfor %}{% if 5 > page.rating %}{% assign greystar = 5 | minus: page.rating %}{% for i in (1..greystar) %}<i style="color:grey;" class="fa fa-star" name="star"></i>{% endfor %}{% endif %}{% else %}<em>Not Available</em>{% endif %}
    

    I was pretty damn proud of that loop. Check the rating, output a gold star for each number over 0 and a grey star for every number between the rating and 5. It’s simple but it’s quick.

    Transposing from Liquid to GoLang was not as hard as all that. {% %} became {{ }} and {% endfor %} became {{ end }} and, at first, that was the easy stuff. The majority of my logic was a one-to-one translation. Change page.rating to .Params.rating and so on and so forth.

    The order of the if’s was strange to me, in that it became {{ if eq A B }} (except when it wasn’t) and I was used to thinking {% if A == B %} — it was fairly easy to overcome. In short order, all my simple if-checks were done.

    But those damn loops! I had the ugliest “if A == 1, show 1 gold and 4 grey” configuration. And worse, I had cases where I was checking “If there’s an entry in the Filmography for this show, get the rating from that and not the page itself.” The Filmography file was a straight-forward JSON file, you see (told you JSON was involved).

    Back to the first problem. I knew if I wanted a shortcode to say “Get me a list of all pages, by date, where section is news” I did this:

    {{ range where $.Page.Site.Pages.ByDate "Section" "news" }}

    And that $.Page.Site.Pages.ByDate call changed depending on what template I was on and what I was calling. For a shortcode I had to pass though page to site to pages. On a template I could do .Data.Pages.ByTitle (no $ needed either). I’m still at the trial and error stage of figuring out which call and where, but I know I’ll get there. It’s just mastering new syntax and understanding where I’m calling from and what variables are available to me.

    That’s really okay. It took me years to visualize the interrelations with WordPress themes and functions and plugins, and even then sometimes I’ll output an array in ugly, unformatted ways just to read through and make sure I understand what I’ve got at my disposal. This is normal for most of us.

    And it was fairly clear that if I wanted a shortcode to call the data file and not a list of Section pages, there was code for that: $.Page.Site.Data.filmography (back to Filmography again). And that worked fine. Right up until I wanted to say “For all values where X equals Foo…” and I was back to my loop-headache.

    Now. GoLang does have a for loop!

    func main() {
        sum := 0
        for i := 0; i < 10; i++ {
            sum += i
        }
        fmt.Println(sum)
    }
    

    And I though that I could just use that. Nope! So I asked myself why did {{ for i in (1..Params.rating) }} fail?

    First of all, that ‘in’ should be := instead. But even so, when I ran it I got “ERROR: function “for” not defined.” That means there is no for function. And no loop. After looking at how I was iterating in other places, I did this:

    {{ range $index, $element := .Params.rating }} 
       ONE
    {{ end }}
    

    ERROR: 2015/12/12 template: theme/partials/rating.html:3:26: executing “theme/partials/rating.html” at <.Params.rating>: range can’t iterate over 3 in theme/partials/rating.html

    The 3, in that case, had to do with a rating of 3. This makes a little sense, since you’re supposed to use range to iterate over a map, array or slice. A number is none of those.

    Thankfully I found a ticket talking about adding a loop function to Hugo that was addressing what I was trying to do and finally I had an answer:

        {{ range $star, seq .Params.rating }}
            <i style="color:gold;" class="fa fa-star" name="star"></i>
        {{ end }}
    
        {{ $greystar := sub 5 .Params.rating }}
    
        {{ range $star, seq $greystar }}
            <i style="color:grey;" class="fa fa-star" name="star"></i>
        {{ end }}
    

    The idea there is that seq makes a sequence of the value of .Params.rating for me. If the value is 3, I get an array of [1,2,3] to work with. And that’s something range can itterate over!

    Except…

    at <sub 5 .Params.rating>: error calling sub: Can’t apply the operator to the values in theme/partials/rating.html

    Now the amusing thing here is that the code worked! Even if it wasn’t seeing .Params.rating as an integer, it did the math. I suspect it’s related to this old bug, where variables from front matter are strings and not integers. Except that he said it worked in YAML and that’s what I’m using.

    This was the most perplexing issue. How is a number not a number if it’s being numbered? And then I noticed that I was able to make a sequence, so clearly at that point it knew it was a number. And then I did this:

        {{ $goldstar := seq .Params.rating }}
        {{ $goldstar := len $goldstar }}
    

    And yes it works.

    I’m basically saying “Hey, how many ones are in the number X?” and saving that as a number. There’s no real reason I can comprehend why it failed to work, but there it is. I’ll file a bug as soon as I can figure out how to explain that in a way that doesn’t make me sound crazy.

    How did all this teach me about JSON? You’ll have to wait a few days.