Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: php

  • The Importance of Correct Casting

    The Importance of Correct Casting

    So PHP 7.4 is rolling out, WP is hawking it, and you know your code is fine so you don’t worry. Except you start getting complaints from your users! How on earth could that be!? Your plugin is super small and simple, why would this happen? You manage to get your hands on the error messages and it’s weird:

    NOTICE: PHP message: PHP Warning:  Illegal string offset 'TERM' in /wp-content/plugins/foobar/search-form.php on line 146
    NOTICE: PHP message: PHP Warning:  ksort() expects parameter 1 to be array, string given in /wp-content/plugins/foobar/search-form.php on line 151
    NOTICE: PHP message: PHP Warning:  Invalid argument supplied for foreach() in /wp-content/plugins/foobar/search-form.php on line 153
    NOTICE: PHP message: PHP Warning:  session_start(): Cannot start session when headers already sent in /wp-content/plugins/foobar/config.php on line 12
    

    But that doesn’t make any sense because your code is pretty straight forward:

    [...]
    
    $name_array = '';
    if( !empty( $term_children )){
        foreach ( $term_children as $child ) {
            $term = get_term_by( 'id', $child, $taxonomy );
    
            $name_array[ $term->name ]= $child;
        }
    
        if( !empty( $name_array ) ){
    
            ksort( $name_array );
    
            foreach( $name_array as $key => $value ) {
        [...]
    

    Why would this be a problem?

    Error 1: Illegal string offset

    The first error we see is this:

    Illegal string offset 'TERM' in /wp-content/plugins/foobar/search-form.php on line 146

    We’re clearly trying to save things into an array, but the string offset actually means you’re trying to use a string as an array.

    An example of how we might force this error is as follows:

    $fruits_basket = array(
        'persimmons' => 1, 
        'oranges'    => 5,
        'plums'      => 0,
    );
    
    echo $fruits_basket['persimmons']; // echoes 1
    
    $fruits_basket = "a string";
    
    echo $fruits_basket['persimmons']; // illegal string offset error
    $fruits_basket['peaches'] = 2; // this will also throw the same error in your logs
    

    Simply, you cannot treat a string as an array. Makes sense, right? The second example (peaches) fails because you had re-set $fruits_basket to a string, and once it’s that, you have to re-declare it as an array.

    But with our error, we can see line 146 is $name_array[ $term->name ]= $child; and that should be an array, right?

    Well. Yes, provided $name_array is an array. Hold on to that. Let’s look at error 2.

    Error 2: ksort expects an array

    The second error is that the function wanted an array and got a string:

    NOTICE: PHP message: PHP Warning: ksort() expects parameter 1 to be array, string given in /wp-content/plugins/foobar/search-form.php on line 151

    We use ksort() to sort the order of an array and here it’s clearly telling us “Buddy, $name_array isn’t an array!” Now, one fix here would be to edit line 149 to be this:

    if( !empty( $name_array ) && is_array( $name_array ) ){
    

    That makes sure it doesn’t try to do array tricks on a non-array, but the question is … why is that not an array to begin with? Hold on to that again, we want to look at the next problem…

    Error 3: Invalid argument

    Now that we’ve seen the other two, you probably know what’s coming here:

    NOTICE: PHP message: PHP Warning: Invalid argument supplied for foreach() in /wp-content/plugins/foobar/search-form.php on line 153

    This is foreach() telling us that the argument you passed isn’t an array. Again.

    What Isn’t An Array?

    We’re forcing the variable $name_array to be an array on line 146. Or at least we thought we were.

    From experience, using $name_array[KEY] = ITEM; was just fine from PHP 5.4 up through 7.3, but as soon as I updated a site to 7.4, I got that same error all over.

    The issue was resolved by changing line 141 to this: $name_array = array();

    Instead of defaulting $name_array as empty with'', I used the empty array() which makes it an array.

    An alternative is this: $name_array = (array) '';

    This casts the variable as an array. Since the array is meant to be empty here, it’s not really an issue either way.

    Backward Incompatibility

    Where did I learn this? I read the PHP 7.4 migration notes, and found it in the backward incompatibility section.

    Array-style access of non-arrays

    Trying to use values of type nullboolintfloat or resource as an array (such as $null["key"]) will now generate a notice.

    The lesson here is that PHP 7.4 is finally behaving in a strict fashion, which regards to data types. Whenever a non-array variable is being used like an array, you get an error because you didn’t say “Mother May I…” it was an array.

    Whew.

    Now to be fair, this was a warning previously, but a lot of us (hi) missed it.

    So. Since WordPress is pushing PHP 7.4, go check all your plugins and themes for that and clean it up. Declare an array or break.

    Oh and that last error? Headers already sent? Went away as soon as we fixed the variable.

  • PHP Spaceship

    PHP Spaceship

    It’s no Jefferson Starship, but in PHP 7 there’s an awesome new comparison operator called ‘Spaceship.’

    Dragon Fly

    Operators are those things we use to tell PHP what ‘operations’ to perform on variables and values. Comparison operators are used to compare two values (like a number or string). And Spaceship — <=> — is a cool new one.

    It’s a combined comparison operator and returns:

    • 0 if values on either side are equal
    • 1 if value on the left is greater
    • -1 if the value on the right is greater

    Red Octopus

    So why would anyone use it?

    Let’s say you have an array of characters with lists of when they died. But the characters are ordered alphabetically. Sara Lance comes after Lexa, even though Sara died first. And let’s say you want to take the list of all the characters who died and put them in the order of death, not their names.

    To do this in PHP, you would use usort or uasort to ‘user sort’ the array. The A in the sort is if you have an associative array, and you want to keep ‘name’ and ‘date of death’ connected when you sort. Which you do.

    Spitfire

    In PHP 5, the sort would look like this:

    uasort( $death_list_array, function($a, $b) {
    	$return = '0';
    	if ( $a['died'] &lt; $b['died'] ) $return = '-1';
    	if ( $a['died'] &gt; $b['died'] ) $return = '1';
    	return $return;
    }
    

    Which isn’t bad, but it’s messy right?

    Earth

    In PHP 7, the sort would look like this:

    uasort( $death_list_array, function($a, $b) {
    	$return = $a['died'] &lt;=&gt; $b['died'];
    	return $return;
    }
    

    Way nicer, and faster, isn’t it?

    Freedom at Point Zero

    Why not use it all the time? Because it’s PHP 7+ only, and for some reason not all servers have PHP 7 as the command line version of PHP. Which means if you use it, sometimes wp-cli commands don’t run.

    However. If you have PHP 7 all across the board, then use the starship and save yourself some flying time.

  • Date, Time, and PHP

    Date, Time, and PHP

    I had a case where I needed to convert time from the format of the US centric dd/MM/YYYY to something a little more sane.

    A Very Bad (and Stupid) Way

    Since I was starting from a set position of 18/07/2017 (for example), the first idea I had was to use explode to do this:

    $date = "18/07/2017";
    $datearray = explode("/", $date);
    $newdate = $date[1].'-'.$date[0].'-'.$date[2]
    

    I did say this was stupid, right? The reason I’d want to do that, though, is that gets it into a DD-MM-YYYY format, which can be parsed a little more easily. Unless you’re an American. Which means this idea is useless.

    A Less Bad, But Still Bad, Way

    What about making it unix timestamp?

    $date = "18/07/2017";
    $date_parse = date_parse_from_format( 'm/d/Y' , $date);
    $date_echo = mktime( $date_parse['hour'], $date_parse['minute'], $date_parse['second'], $date_parse['month'], $date_parse['day'], $date_parse['year'] );
    

    Now I have a unix timestamp, which I can output any which way I want! At this point, if I’m not sure what version of PHP people are using, as long as it’s above 5.2, my code will work. Awesome.

    But we can do better.

    A Clever Way

    Instead of all that, I could use date_parse_from_format:

    $date = "18/07/2017";
    $date_parse = date_create_from_format('m/j/Y', $date );
    $date_echo[] = date_format($date_parse, 'F d, Y');
    

    And this will convert “18/07/2017” into “18 July, 2017”

    Which is far, far more understandable.

    So Which Is Best?

    If it’s your own code on your own server, I’d use date_create_from_format to control the output as you want.

    If this is code you want to run on any server, like in a WordPress plugin, use the unix timestamp, and then use date_i18n to localize:

    date_i18n( get_option( 'date_format' ), $date_echo );
    
  • The Unbearable Uniqueness of Functions

    The Unbearable Uniqueness of Functions

    PHP 7.2 is on the horizon and one of the battles that we’re still facing is how to name our functions and classes. In WordPress, this is especially hard because we have to consider how our plugins and themes will be used in the global landscape.

    Everything Must Be Unique

    If you’ve written a self-contained PHP app, you know that you can’t give two functions the same name. So you have function enqueue_scripts and function enqueue_styles and function embed_content but you can’t declare them twice. Names must be unique within an app.

    When you consider the fact that all plugins and themes are third-party add-ons to WordPress, the naming situation gets messier. You can’t, for example, use function get_the_excerpt() because WordPress is using it. So you have to introduce prefixes to your plugin (let’s call it “Caffeinated”) like this: function caff_get_the_excerpt()

    That works well until someone creates an add-on to your plugin, call it “Decaffeinated”, and they need to use function decaff_get_the_excerpt() which isn’t so bad, but “Tea Caffeinated” has to either use a prefix of tea_caff_ or caff_tea_ and the more you travel down the road, the messier it gets.

    Prevent Collisions

    The whole point of this exercise in naming is, you see, to prevent collisions between your code and the parent projects you’re working with. You can’t use __ as a function prefix, for example, because that’s magical in PHP land. You can’t use wp_ or wordpress_ in WordPress, because … well. You’re not WordPress.

    But preventing collisions gets long and messy and convoluted and you end up with prefixes like edd_stbe_ which is unique but can be difficult to remember what it’s for. That’s why, thankfully, we have the option of a little more selective naming.

    Selective Uniqueness

    Back in PHP 5, the object model was rewritten to allow for better performance and more features. And this allows us to make Classes which contain our functions, named whatever we want:

    class Caffeinated {
        function get_the_excerpt() { ... }
    }
    new Caffeinated();
    

    This is extended to the add-on plugins, So we could have class Decaffeinated and class Caffeinated_Tea but within the classes, our functions are more simply named. Also we can call functions from one class into another, like Tea could call Caffeinated::get_the_excerpt() and easily filter that function.

    Why is this good? Well it lets you keep the function names simple, but the class name distinctive and thus easier to remember.

    Simple Names are Better

    One of the cardinal rules about domain names is that shorter is better. The same goes for functions and classes, to a degree. As Otto likes to say, a well named function doesn’t really need documentation. And one of the ways you can get a well named function is by putting it in a well named class. You can also use Namespaces.

    Namespaces are magical. By adding namespace Caffeinated; to your plugin file, for example, you can then have a classname of class Settings and not have to worry about collisions, because your class name is prefixed automatically by the namespace. This extends to functions, as you can us function admin for admin settings in the Settings class, in the Caffeinated namespace. And to call that? Use Caffeinated\Settings::admin();

    There’s a lot more you can do with Namespaces, of course, but using them to stop your code from crashing into other people’s, while still keeping names obvious, unique, and memorable? Well they’re pretty nifty.

    Remember: What’s Your Global?

    At the end of the day, the big thing to remember is what your globals are.

    • Outside of a class or namespace, a function is a global.
    • Outside of a namespace, a class is a global.
    • Outside of a namespace, a namespace is global.

    Yes, you can have sub-namespaces. But the point is that top-level whatever you pick, be it a function, class, or namespace, must be named uniquely and distinctly and, in my opinion, relative to the code it’s in. Naming things generically ends in tears as they’ll probably conflict.

  • A Fully Functional Alexa Skill

    A Fully Functional Alexa Skill

    I’ve been talking a lot (a lot) about my Amazon Alexa skill.

    First of all, it’s done. It’s published and working. You can check it out on Amazon.com.

    Secondly, a lot of people are like me and need a real, concrete, working example. So I’ve decided to post the entirety of my Amazon Alexa Skill.

    This file, which is actually found on a site in /lwtv-plugin/rest-api/alexa-skills.php consists of two endpoints: the flash briefing and the skill. The skill is called “Bury Your Queers” and while I suspect that part of the code is the least ‘useful’ to people, it’s also good to see the real code.

    Here, then, is the original approved and certified WordPress code for an Alexa Skill:

    The Code

    <?php
    /*
    Description: REST-API - Alexa Skills
    
    For Amazon Alexa Skills
    
    Version: 1.0
    Author: Mika Epstein
    */
    
    if ( ! defined('WPINC' ) ) die;
    
    /**
     * class LWTV_Alexa_Skills
     *
     * The basic constructor class that will set up our JSON API.
     */
    class LWTV_Alexa_Skills {
    
    	/**
    	 * Constructor
    	 */
    	public function __construct() {
    		add_action( 'rest_api_init', array( $this, 'rest_api_init') );
    	}
    
    	/**
    	 * Rest API init
    	 *
    	 * Creates callbacks
    	 *   - /lwtv/v1/flash-briefing
    	 */
    	public function rest_api_init() {
    
    		// Skills
    		register_rest_route( 'lwtv/v1', '/alexa-skills/briefing/', array(
    			'methods' => 'GET',
    			'callback' => array( $this, 'flash_briefing_rest_api_callback' ),
    		) );
    
    		// Skills
    		register_rest_route( 'lwtv/v1', '/alexa-skills/byq/', array(
    			'methods' => [ 'GET', 'POST' ],
    			'callback' => array( $this, 'bury_your_queers_rest_api_callback' ),
    		) );
    
    
    	}
    
    	/**
    	 * Rest API Callback for Flash Briefing
    	 */
    	public function flash_briefing_rest_api_callback( $data ) {
    		$response = $this->flash_briefing();
    		return $response;
    	}
    
    	/**
    	 * Rest API Callback for Bury Your Queers
    	 * This accepts POST data
    	 */
    	public function bury_your_queers_rest_api_callback( WP_REST_Request $request ) {
    
    		$type   = ( isset( $request['request']['type'] ) )? $request['request']['type'] : false;
    		$intent = ( isset( $request['request']['intent']['name'] ) )? $request['request']['intent']['name'] : false;
    		$date   = ( isset( $request['request']['intent']['slots']['Date']['value'] ) )? $request['request']['intent']['slots']['Date']['value'] : false;
    
    		$validate_alexa = $this->alexa_validate_request( $request );
    
    		if ( $validate_alexa['success'] != 1 ) {
    			$error = new WP_REST_Response( array( 'message' => $validate_alexa['message'], 'data' => array( 'status' => 400 ) ) );
    			$error->set_status( 400 );
    			return $error;
    		}
    
    		$response = $this->bury_your_queers( $type, $intent, $date );
    		return $response;
    	}
    
    
    	function alexa_validate_request( $request ) {
    
    		$chain_url = $request->get_header( 'signaturecertchainurl' );
    		$timestamp = $request['request']['timestamp'];
    		$signature = $request->get_header( 'signature' );
    
    	    // Validate that it even came from Amazon ...
    	    if ( !isset( $chain_url ) )
    	    	return array( 'success' => 0, 'message' => 'This request did not come from Amazon.' );
    
    	    // Validate proper format of Amazon provided certificate chain url
    	    $valid_uri = $this->alexa_valid_key_chain_uri( $chain_url );
    	    if ( $valid_uri != 1 )
    	    	return array( 'success' => 0, 'message' => $valid_uri );
    
    	    // Validate certificate signature
    	    $valid_cert = $this->alexa_valid_cert( $request, $chain_url, $signature );
    	    if ( $valid_cert != 1 )
    	    	return array ( 'success' => 0, 'message' => $valid_cert );
    
    	    // Validate time stamp
    		if (time() - strtotime( $timestamp ) > 60)
    			return array ( 'success' => 0, 'message' => 'Timestamp validation failure. Current time: ' . time() . ' vs. Timestamp: ' . $timestamp );
    
    	    return array( 'success' => 1, 'message' => 'Success' );
    	}
    
    	/*
    		Validate certificate chain URL
    	*/
    	function alexa_valid_key_chain_uri( $keychainUri ){
    
    	    $uriParts = parse_url( $keychainUri );
    
    	    if (strcasecmp( $uriParts['host'], 's3.amazonaws.com' ) != 0 )
    	        return ( 'The host for the Certificate provided in the header is invalid' );
    
    	    if (strpos( $uriParts['path'], '/echo.api/' ) !== 0 )
    	        return ( 'The URL path for the Certificate provided in the header is invalid' );
    
    	    if (strcasecmp( $uriParts['scheme'], 'https' ) != 0 )
    	        return ( 'The URL is using an unsupported scheme. Should be https' );
    
    	    if (array_key_exists( 'port', $uriParts ) && $uriParts['port'] != '443' )
    	        return ( 'The URL is using an unsupported https port' );
    
    	    return 1;
    	}
    
    	/*
    	    Validate that the certificate and signature are valid
    	*/
    	function alexa_valid_cert( $request, $chain_url, $signature ) {
    
    		$md5pem     = get_temp_dir() . md5( $chain_url ) . '.pem';
    	    $echoDomain = 'echo-api.amazon.com';
    
    	    // If we haven't received a certificate with this URL before,
    	    // store it as a cached copy
    	    if ( !file_exists( $md5pem ) ) {
    		    file_put_contents( $md5pem, file_get_contents( $chain_url ) );
    		}
    
    	    $pem = file_get_contents( $md5pem );
    
    	    // Validate certificate chain and signature
    	    $ssl_check = openssl_verify( $request->get_body() , base64_decode( $signature ), $pem, 'sha1' );
    
    	    if ($ssl_check != 1 ) {
    		    return( openssl_error_string() );
    		}
    
    	    // Parse certificate for validations below
    	    $parsedCertificate = openssl_x509_parse( $pem );
    	    if ( !$parsedCertificate ) return( 'x509 parsing failed' );
    
    	    // Check that the domain echo-api.amazon.com is present in
    	    // the Subject Alternative Names (SANs) section of the signing certificate
    	    if(strpos( $parsedCertificate['extensions']['subjectAltName'], $echoDomain) === false) {
    	        return( 'subjectAltName Check Failed' );
    	    }
    
    	    // Check that the signing certificate has not expired
    	    // (examine both the Not Before and Not After dates)
    	    $validFrom = $parsedCertificate['validFrom_time_t'];
    	    $validTo   = $parsedCertificate['validTo_time_t'];
    	    $time      = time();
    
    	    if ( !( $validFrom <= $time && $time <= $validTo ) ) {
    	        return( 'certificate expiration check failed' );
    	    }
    
    	    return 1;
    	}
    
    	/**
    	 * Generate the Flash Briefing output
    	 *
    	 * @access public
    	 * @return void
    	 */
    	public function flash_briefing() {
    
    		$query = new WP_Query( array( 'numberposts' => '10' ) );
    		if ( $query->have_posts() ) {
    			while ( $query->have_posts() ) {
    				$query->the_post();
    
    				$response = array(
    					'uid'            => get_the_permalink(),
    					'updateDate'     => get_post_modified_time( 'Y-m-d\TH:i:s.\0\Z' ),
    					'titleText'      => get_the_title(),
    					'mainText'       => get_the_title() . '. ' . get_the_excerpt(),
    					'redirectionUrl' => home_url(),
    				);
    
    				$responses[] = $response;
    			}
    			wp_reset_postdata();
    		}
    
    		if ( count( $responses ) === 1 ) {
    			$responses = $responses[0];
    		}
    
    		return $responses;
    
    	}
    
    	/**
    	 * Generate Bury Your Queers
    	 *
    	 * @access public
    	 * @return void
    	 */
    	public function bury_your_queers( $type = false, $intent = false, $date = false ) {
    
    		$whodied    = '';
    		$endsession = true;
    		$timestamp  = ( strtotime( $date ) == false )? false : strtotime( $date ) ;
    		$helptext   = 'You can find out who died on specific dates by asking me questions like "who died" or "who died today" or "who died on March 3rd" or even "How many died in 2017." If no one died then, I\'ll let you know.';
    
    		if ( $type == 'LaunchRequest' ) {
    			$whodied = 'Welcome to the LezWatch TV Bury Your Queers skill. ' . $helptext;
    			$endsession = false;
    		} else {
    			if ( $intent == 'AMAZON.HelpIntent' ) {
    				$whodied = 'This is the Bury Your Queers skill by LezWatch TV, home of the world\'s greatest database of queer female on TV. ' . $helptext;
    				$endsession = false;
    			} elseif ( $intent == 'AMAZON.StopIntent' || $intent == 'AMAZON.CancelIntent' ) {
    				// Do nothing
    			} elseif ( $intent == 'HowMany' ) {
    				if ( $date == false || $timestamp == false ) {
    					$data     = LWTV_Stats_JSON::statistics( 'death', 'simple' );
    					$whodied  = 'A total of '. $data['characters']['dead'] .' queer female characters have died on TV.';
    				} elseif ( !preg_match( '/^[0-9]{4}$/' , $date ) ) {
    					$whodied    = 'I\'m sorry. I don\'t know how to calculate deaths in anything but years right now. ' . $helptext;
    					$endsession = false;
    				} else {
    					$data     = LWTV_Stats_JSON::statistics( 'death', 'years' );
    					$count    = $data[$date]['count'];
    					$how_many = 'No queer female characters died on TV in ' . $date . '.';
    					if ( $count > 0 ) {
    						$how_many = $count .' queer female ' . _n( 'character', 'characters', $count ) . ' died on TV in ' . $date . '.';
    					}
    					$whodied  = $how_many;
    				}
    			} elseif ( $intent == 'WhoDied' ) {
    				if ( $date == false || $timestamp == false ) {
    					$data    = LWTV_BYQ_JSON::last_death();
    					$name    = $data['name'];
    					$whodied = 'The last queer female to die was '. $name .' on '. date( 'F j, Y', $data['died'] ) .'.';
    				} elseif ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])$/' , $date ) ) {
    					$whodied    = 'I\'m sorry. I don\'t know how to calculate deaths in anything but days right now. ' . $helptext;
    					$endsession = false;
    				} else {
    					$this_day = date('m-d', $timestamp );
    					$data     = LWTV_BYQ_JSON::on_this_day( $this_day );
    					$count    = ( key( $data ) == 'none' )? 0 : count( $data ) ;
    					$how_many = 'No queer females died';
    					$the_dead = '';
    					if ( $count > 0 ) {
    						$how_many  = $count . ' queer female ' . _n( 'character', 'characters', $count ) . ' died';
    						$deadcount = 1;
    						foreach ( $data as $dead_character ) {
    							if ( $deadcount == $count && $count !== 1 ) $the_dead .= 'And ';
    							$the_dead .= $dead_character['name'] . ' in ' . $dead_character['died'] . '. ';
    							$deadcount++;
    						}
    					}
    					$whodied = $how_many . ' on '. date('F jS', $timestamp ) . '. ' . $the_dead;
    				}
    			} else {
    				// We have a weird request...
    				$whodied = 'I\'m sorry, I don\'t understand that request. Please ask me something else.';
    				$endsession = false;
    			}
    		}
    		$response = array(
    			'version'  => '1.0',
    			'response' => array (
    				'outputSpeech' => array (
    					'type' => 'PlainText',
    					'text' => $whodied,
    				),
    				'shouldEndSession' => $endsession,
    			)
    		);
    
    		return $response;
    
    	}
    
    }
    new LWTV_Alexa_Skills();
    

    Some Explanations

    The functions bury_your_queers and bury_your_queers_rest_api_callback are the important ones. The flash briefing is there because I was tired of Amazon being picky about embedded media in RSS feeds.

    The way bury_your_queers_rest_api_callback works is it takes the request data to generate the type of request, the intent, and the date information. Then it passes the full request data to alexa_validate_request which is the part you’ll really want.

    That function, alexa_validate_request, is what’s validating that the request came from Amazon, that it’s got a legit certificate from Amazon, and that the request was made in the last 60 seconds. While all those checks kick back an error, the development tools from Amazon will not show you them. Yet. I’m hoping they will in the future so we can more easily debug, but it was a lot of blind debugging. Not my favorite.

    Some Custom Code

    In the bury_your_queers function, I make some calls to other code not included:

    • LWTV_BYQ_JSON::last_death()
    • LWTV_BYQ_JSON::on_this_day( $this_day )

    Those both reference another rest API class in a different file. What’s important here is not what the data is, but that I’m calling those functions and getting an array back, and using that to fill in my reply. For example, here we have the call for ‘last death’:

    $data    = LWTV_BYQ_JSON::last_death();
    $name    = $data['name'];
    $whodied = 'The last queer female to die was '. $name .' on '. date( 'F j, Y', $data['died'] ) .'.';
    

    From this you can infer that the array kicked back has key for name and died. And in fact, if you look at the JSON output, you’ll see if has that and a bit more. I’m just extracting what is required. The same is true of the other function, LWTV_BYQ_JSON::on_this_day, to which I’m passing a parameter of a date.

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