Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: debug

  • Interlude: Gutenberg Moves Fast

    Interlude: Gutenberg Moves Fast

    I’m taking a pause on my plugin posts to talk about Gutenberg.

    I really love Gutenberg. I’m not kidding! I find it far more enjoyable to write (stories) in plain apps (I used Apple Pages because it syncs between laptop and iPad, yes, I am often using my iPad to write my novel, yes, I will let the world know when it’s done). But when I write for the web, it’s a more visual medium, and Gutenberg is fantastic to represent what I’m writing as it will be properly seen by all!

    But.

    Gutenberg moves fast. Hella fast. So fast it can leave you in the dust, and it has a critical flaw that I feel has been stifling it’s growth and usage among developers.

    JS isn’t your Momma’s PHP

    This is obvious. Javascript ain’t PHP. PHP is a simple language that can be coerced into doing complex things if you understand basic algebra. Surprise! Everyone who considers themselves good at PHP? You’ve mastered the concepts of algebra! Alegrba is one of the easier ‘complex’ mathematic concepts to wrap your head areoud. You get “if a + b = c and a = 10 and c = 11 then b = 1” and you win!

    Javascript though, it’s a little more like calculus and trig, in that you have to understand the formulas a little deeper, and they have that thing where not just numbers and letters appear, but weird symbols.

    [nb: Like all analogies, this falls apart at scale, don’t read too much into it.]

    For the thousands of developers who whet their teeth on PHP, jumping into JS feels like you’re a first-year high schooler in senior maths! It’s scary, it’s complicated, and worst of all … it isn’t actually documented at the micro level, because it’s generally compiled.

    Micro vs Macro / Interpreted vs Compiled

    The macro scale is, more or less, the big picture of what your code is supposed to do. Micro would be each individual element. For PHP, you can clearly identify both the macro (the overall function) and the micro (each teeny process in that function). This is less so for JS because the languages are different.

    There are two primary types of code languages. PHP is what we call an interpreted language, because while the PHP binary is a compiled app, what you write is interpreted by the compiler. Basic JS (like jQuery) is also an interpreted language!

    Compiled languages need a “build” step – they need to be manually compiled first. And if that suddenly made you think “Wait, Gutenberg is JS but I have to build it!” then you have spotted the quirk! The JS we use in Gutenberg is actually JSX!

    JSX was designed for React (which is what we use to build in Gutenberg) and while it may contain some plain Javascript, it’s impossible to use the code without React. That’s why we have the build process, it takes the JSX, compiles it into JS, and saves it to a file.

    The Compilation Downfall

    This is where it gets messy … messier.

    When there’s an error in PHP, we get the error message either on the page or in our logs, depending on how we set up our environment. I personally pipe things to debug.log and just keep that file up as I bash on things. Those errors tend to be incredibly helpful!

    $mastodon not defined on /path/to/file.php:123

    In that example, I know “Ooops, I’m calling the variable $mastodon on line 123 of file.php and forgot to declare it!” Either I need an isset() check or (in this case) I brain farted and copied a line but forgot to rename the variable so I was setting $tumblr twice. Mea culpa, pop in, edit, save, done.

    On the other hand, I was testing out some blocks and modernizing them a little when suddenly … the block didn’t load. I got the WP notice of the block had an error. You’ve probably seen this if you’re a dev:

    Example of an error which says "This block has encountered an error and cannot be previewed"

    or this:

    An error: This block contains unexpected or invalid content.

    And if you’re like me, you used foul language and wondered ‘well… now what.’

    Enter the Console

    Unlike PHP, the errors don’t go to a nice debug.log file, it goes to your in-browser console. This is because, again, PHP is being directly interpreted on the server, and the server happily converts the PHP to HTML and Bob’s your uncle.

    JS (and JSX in this case) aren’t processed by the server. They’re processed on the fly in the browser. If you’ve ever wondered why too much JS, or bad JS, cause your browser to hang, that’s why. We moved the processing from the server (PHP) to the browser. On top of that, it’s also why JS content isn’t really cachable by traditional methods! But that’s another story.

    In this case, I got the first error (cannot be previewed) and being somewhat savvy with the world of Gutes, I popped open the console and saw this gem:

    wp.blockEditor.RichText value prop as children type is deprecated

    The rest of the message was warning me that the thingy would be removed in WP 6.3, and it had a link to ‘help’ resolve it. Spoilers? It didn’t. But take a deep breath. Let’s debug.

    Debugging Gutenberg

    The first issue was that the error came on a page with multiple blocks. I happened to be using a custom plugin I wrote that contains about 6 blocks, you see, so I opened a new page on localhost and added each block, one at a time, until I determined the issue was my incredibly simple spoiler block.

    How simple is this block? It’s basically a custom formatted paragraph, so everyone could use the same design without having to remember the exact colours. I could have made it a ‘reusable block’ on the site but, at the time, I wanted the practice.

    Next I went to that link, which was for “Introducing Attributes and Editable Fields“. I admit, I was a little confused, since I was already using attributes and editable fields! But I did the logical thing and searched that page for the word ‘children.’ My thought process was that if something was being deprecated, it would have a warning right?

    Gif from Blazing Saddles, where Dom DeLuise is a director and walks up to an actor who made a mistake. He uses his bullhorn to scream WRONG! at the man, and bops him in the head with the bullhorn.

    Okay, maybe I was looking in the wrong place. This error is specific to RichText so I clicked on the link to read the RichText Reference and again, looked for “children.” Nothing. Zip. Nada. I followed the link for the more in-depth details on GitHub and still nothing.

    At this point, I ranted on Mastodon because I was chapped off. I also popped open the Gutenberg Deprecations page, and looked for “children” but all I could find was a message to use children!

    RichText explicit element format removed. Please use the compatible children format instead.

    Logically there should be a note that “children is deprecated, please use…” but there is not.

    Now, here is where I accidentally stumbled on a fix, but after I made my fix is when I found the Github issue about this!

    If you are still using “children” or “node” sources in the block attribute definition, like so:

    content: {
     	type: 'array',
     	source: 'children',
     	selector: 'p',
     }
    

    Then change it to use the “html” source instead to get rid of the deprecation warning:

    content: {
     	type: 'string',
     	source: 'html',
     	selector: 'p',
     }
    

    And in fact, that was the correct fix.

    Here’s the Flaw

    None of that was properly documented.

    The link to ‘help’ fix the error didn’t mention the specific error, it talked about attributes at the MACRO level. I was (obviously) already using attributes, else I wouldn’t have had that error at all.

    There is no proper documentation that could help someone fix the issue on their own UNLESS they happened to be trawling through all the issues on GitHub.

    As I put it to my buddy, the reasons developers are salty about Gutenberg are:

    1. It changes pretty much every release
    2. There’s no real way to tell people if it impacts you so you have to check every release and read the console logs, which is not what devs are used to
    3. The JS console won’t tell you (I don’t know if it can) what file caused the warning, so finding it is a crap shoot
    4. The documentation is high level, which is not helpful when you get micro level errors

    Okay, can we fix it?

    At this point, if you’ve made a WordPress Block for Gutenberg, make sure you test every single release with the JS console open. If you don’t do this, you will have a rude awakening until things are made a little better.

    How can things be made better? It will have to begin with a culture shift. Traditionally WordPress has used a “release and iterate” model. With Gutenberg, we’ve become “move fast and break things,” but that is only sustainable if everything broken can be documented for a fix.

    That means I see only one way to correct this, and it’s to slow down Gutenberg enough that deprecations AND THEIR CORRECTIONS are properly documented, and the error messages link to a page about deprecations.

    We need to not link to the general “here’s how attributes work” page, but instead to a specific page that lists those deprecations along side the WordPress versions impacted.

    Another matter is we should be posting in the Field Guide about these things. Currently the 6.3 field guide links to all the various pages where you can find information, but that means you have to click and open each one and hopefully find your exact usage. In my case, those links to the ten Gutenberg versions being ported to core never mention the issue I had.

    If we don’t start slowing down and paving the road for developers, we will begin haemorrhaging the very people who make WordPress a success.

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

  • WP-CLI Tables

    WP-CLI Tables

    I was working on an update to the Varnish plugin I’ve adopted, and one of the requested features was for more debugging tool. I’d added in a status page, but this plugin is also used by web hosts, and sometimes asking a customer “Can you go to this bad and send me the results?” is a time sink.

    So why not add in a command line tool?

    WP-CLI?

    I love WP-CLI. It’s a command line interface (CLI) for WordPress (WP) that lets you do most anything via the command line. You can install and activate plugins, update themes, even write posts and add users. But if you’re tech savvy, it’s also a great tool to automate and manage the minutia of WordPress maintenance drudgery.

    I’d already built out a basic varnish flush command (wp varnish purge) so adding on to it isn’t terribly difficult. But what was difficult was making the output what I wanted.

    Start With an Array

    No matter what you need an array of the correct format for this to work. I was already storing everything in an array I save in a variable called $results that looks like this:

    Array ( 
        [varnish] => Array ( 
            [icon] => awesome 
            [message] => Varnish is running properly and caching is happening. 
        ) 
        [remote_ip] => Array ( 
            [icon] => awesome 
            [message] => Your server IP setup looks good. 
        ) 
        [age] => Array ( 
            [icon] => good 
            [message] => Your site is returning proper "Age" headers. 
        )
    )
    

    I was initially doing this so I could loop and output all results with an icon and message on the status page, but translating this to wp-cli was a matter of taking the array and repurposing it.

    WP-CLI Tables

    In order to add in a table output to WP-CLI, you use the format_items function:

    WP_CLI\Utils\format_items( $format, $items, $headers );
    

    The $format value is taken from $assoc_args['format'] (I set mine to default to table if it’s not defined). The $items are your array, and the $headers are another array of what your headers are.

    This is the tricky part. You have to make sure your array of headers matches your array of items, and fulfills the output you desire. In order to do this, start with figuring out what you need to output.

    In my case, I wanted a name, a status (aka the icon), and the message. This means my array looks like this: $headers = array( 'name', 'status', 'message' )

    Rebuild The Array

    Once I sorted out what the format was like, based on the headers, I built the items array as follows:

    // Generate array
    foreach ( $results as $type => $content ) { 
    	$items[] = array(
    		'name'    => $type,
    		'status'  => ucwords( $content['icon'] ),
    		'message' => $content['message'],
    	);
    }
    

    Remember, my $results were already an array. I’m just making it look right here.

    Final Results

    How does it look? Like this:

    +-----------+---------+-------------------------------------------------------+
    | name      | status  | message                                               |
    +-----------+---------+-------------------------------------------------------+
    | varnish   | Awesome | Varnish is running properly and caching is happening. |
    | remote_ip | Awesome | Your server IP setup looks good.                      |
    | age       | Good    | Your site is returning proper "Age" headers.          |
    +-----------+---------+-------------------------------------------------------+
    

    And that is a nice tool for people to debug.

  • Safari and SameOrigin

    Safari and SameOrigin

    I was updating a site I’ve been neglecting. Due to reasons, it’s been about four months since I’ve looked at it, let alone done any work. But as I was cleaning up the data, moving ads around, and messing with widgets, I found myself stumped.

    Customizer was a blank screen.

    Debugging Javascript

    Now I happen to know that the WordPress customizer makes heavy use of javascript, so I popped open the console and found this error:

    [Error] Multiple 'X-Frame-Options' headers with conflicting values ('ALLOW-FROM https://example.com/wordpress/wp-admin/customize.php, SAMEORIGIN') encountered when loading 'https://example.com/2017/post-name/?customize_changeset_uuid=0774a706-2a5b-4700-a8fb-2c294708687c&customize_theme=utility-pro&customize_messenger_channel=preview-0'. Falling back to 'DENY'.
    
    [Error] Refused to display 'https://example.com/2017/post-name/?customize_changeset_uuid=0774a706-2a5b-4700-a8fb-2c294708687c&customize_theme=utility-pro&customize_messenger_channel=preview-0' in a frame because it set 'X-Frame-Options' to 'ALLOW-FROM https://example.com/wordpress/wp-admin/customize.php, SAMEORIGIN'.
    

    Right away, I knew that the problem wasn’t Javascript.

    Safari is Special

    A quick search netted me two possible tickets. First, there’s the problem with customizer failing to load if there was a home/siteurl domain mismatch and second, there’s the issue that customizer fails to load in Safari due to X-Origin Header mismatch.

    In reading those tickets, I determined that it was possible that since this site has WordPress in a folder (yes, named wordpress) but runs from the main domain, that could break it. There was also a possibility that NGINX or Apache were set to restrict SAMEORIGIN. Finally, there was the absolutely daft problem that Safari was special and didn’t like extra rules.

    Now, since this server runs multiple sites, and only this one was having any problems, I threw out all possible server related causes. I also discarded any Multisite related causes, as the site wasn’t the network. Next I determined it absolutely was Safari, by testing on Firefox and Chrome. That left me with the following probable causes:

    1. Some code on my site was breaking customizer

    No, really. That was it.

    The Solution

    Two choices here. Figure out what was broken or stop using Safari.

    I have reasons for using Safari, so I knuckled down. I searched all lines of my code for anything related to X-Frame-Options and came up empty. I tested by commenting out the relevant lines in WordPress core, which worked, but wasn’t tenable.

    Then I changed my search and looked for X-Frame-Options in all .htaccess files, and found this:

    <IfModule mod_headers.c>
         Header set X-Frame-Options "SAMEORIGIN"
    </IfModule>
    

    Removed that, and done.

    Why Was It There?

    I actually put that code in to prevent clickjacking. Clickjacking is what’s called a “UI redress attack.” It happens when a malicious attacker uses transparent or opaque laters to trick someone into clicking a button or a link on page, when they were intending to click another. Usually this is to steal private information.

    WordPress already does this for the login page, and for this site, that’s actually the only time ‘private’ data is sent on this site. Which means it’s mostly safe enough to leave be. There are ways I could do this in PHP code, but since WordPress isn’t the only tool on the site, it’s harder to maintain.

    The better fix would be to, with Apache or NGINX, check the domain and URL, and only apply clickjacking when I’m not in Customizer.

    Of course I have no idea how to do that. Yet.

  • Accidental Example

    Accidental Example

    My father was having email woes, so I undertook the monumental task of sorting out his hellish setup. Among other hurdles, he still uses (and in fact prefers) POP email.

    Don't judge him.

    However it was in reviewing the POP mail that I found a problem. He had over 145 emails, and of them only 33 or so were legitimate emails. Of the other 112, about 20 were 'mailing lists' (like Safeway and Egencia and crap we do actually use), 5 or so were porn, and then 87 were from a deployment service.

    Not His Monkey House

    I double checked that my father didn't use the service and then I looked at the email. They were all emails for an account payable system that he absolutely didn't use.

    Sample image of the emails, saying that someone was moved to "paid" in accounts payable.

    That's not at all Dad's job, so I agreed they were likely junk but how did they get there?

    A Real Company

    The first thing I did was check that this was a legit company. Interesting. I then did the logical step and requested a password reset for his email. It emailed me a link, which I clicked and yes, it let me reset the password… Except it didn't.

    I got an error saying that the 'username' was already in use.

    Which made no sense. I was on the password reset form. Not a create user form. So I tried a few different ways, and then tried to file a bug report or ask for help with is email and it all error'd out. It did not like his email.

    To Twitters!

    I then complained on Twitter, which netted me the very helpful Isabelle who DM'd me and knew right away what was happening.

    The hundreds of emails were actually just a mix-up because one of our product specialists had a demonstration company with a database with tons of 'demonstration users' with personalities and characters names and your dad's email got in by accident (due to its homonym toy story character).

    Isabelle

    Dad's domain is woody.com you see.

    Suddenly it all made sense.

    Why We Use Example.com

    They went ahead and removed his email from all their pipelines and deleted the fake account they'd made for the domain (which explains why I couldn't do a reset). And I haven't seen an email come in after that.

    It was a rude awaking for this poor company. We don't use real domains in our examples for a damn good reason: people copy/pasta.

    No one thought to check if the domain existed, and it's pure coincidence that they picked his email for the demos and examples. And yet it's a good reminder for you too. Those example domains you pick will probably be used by someone in production. Don't spam them.

    But a bigger concern is this. How much private data got sent to my father over the course of the weeks this was the case? How much information did he have access to that he shouldn't? You're all very lucky he's not malicious.

  • The Curious Case of a Comatose Cloud

    The Curious Case of a Comatose Cloud

    The summary here is that remotely hosting SVGs caused a massive slowdown.

    Isn’t the Cloud Magic?

    Nope. Well. No. It is totally magic, in that they’re great for large files and are an inexpensive storage alternative. But the cloud is, at the end of the day, just another server out there in the world, holding your data. Unless that cloud server is behind a CDN, you may not see a great deal of speed improvements on your site.

    And in my case, it didn’t.

    Diagnosing the Problem

    In building out a dev site with Tracy (LilJimmi), we noticed certain pages were really slow to load. 35 seconds slow. That’s unacceptable. I compared it to the live site, and it was faster, but some specific pages were still incredibly slow. What pages? The L Word for the most part. And as we inched closer to being done, I said “I’m going to fix this speed stuff before the weekend!” because you can’t go-live on a slow site. You just can’t.

    Once I was home, I fired up a local site, installed Query Monitor, and had a serious sit down with everything.

    It Wasn’t What I Thought

    My initial thought, the one I ruminated on during my bike ride home, was that it was the database queries. Most shows have one or two queer characters, but The L Word has 60 right now. While I may joke about wanting nine more, it’s a weird situation where I need to get the number of characters on the show without knowing the number of characters on the show. My assumption was that it was my calculation loop that caused the issue. That I was querying the queers too many times.

    Turned out it wasn’t (just) the number of characters, it was the number of tags.

    How It Got So Slow

    Most of the issue is my fault. Every single tag has a custom image associated with it. These images are stored remotely, in the cloud, and called as part of the design. The issue was that when calling the images I ran a check “Is the service available?” and if not, it would stop. When you make one or two calls, it’s no big deal. When you make a couple hundred, it adds up.

    The L Word had 2 icons per 60 characters, and then 15 tags, and then 30 more icons.

    Remote Get Is Slow

    I used wp_remote_get to process my images, and it was taking between .1 and .4 seconds per image. That adds up. At first I simplified my check-if-exists routine and more than halved the time to load from 35 to 15 seconds. But in order to drop the page back to 1 second-ish load times, I had to put the images local again. No matter what I did, if I loaded them remotely the best I could get was a 13 second page page load.

    Sometimes, local is better.

    What’s the moral?

    Obviously the moral is test before you rollout. Which we certainly did! By using Query Monitor, I was able to narrow down the speed for the database queries as well as the speed for all the HTTP requests. In doing so, I lost a CDN but gained speed. I’m trying to figure out how to speed up the CDN, maybe by finding a different front-end proxy, but right now I’ll keep using it for the large files like videos rather than the hundred small ones.

    I think it it’s worth it.