No one planned for this

I have a client whose website I’ve been managing for over a decade.

Long before I was working with them, the client registered their domain with a company called NameSecure. As you can see by clicking that link, their website still exists, but they were long ago purchased by Network Solutions (one of the worst companies in the world). NameSecure’s zombie, circa 2002 website lives on though. I can still log into the client’s account. I’ll come back to that in a minute.

When I started working with the client, my preferred hosting company was Media Temple. As you can see by clicking that link, their website doesn’t exist anymore, after they were acquired and dismantled several years ago by GoDaddy. The company whose name, I still believe, was supposed to be “God Addy,” as in “the god of addresses,” when some nascent techbro registered it in 1999, before marketing stepped in and retconned it to the slightly less awful name that the world has known ever since they kicked off their sexist, years-long Super Bowl ad campaign of the early 2000s. (And yes, I’ve read the contrary history on Wikipedia.)

GoDaddy mostly absorbed everything Media Temple-related, and the client set me up with “Delegate Access” so I can get into their GoDaddy account. That’s where we made our DNS changes a few years ago to point to the Digital Ocean server I was running their site on until Digital Ocean became a disaster. And it’s where I went again last year when I moved their site to WP Engine. (Don’t even get me started.)

Well they’ve had it with WP Engine’s ludicrous overage charges (and so have I), so now we’re moving on to the hosting-company-that-only-mostly-sucks-rather-than-completely-sucks du jour, Hostinger. So far the worst I can say about Hostinger is that sometimes their web control panel just stops responding. But I blame React (and therefore, indirectly, Facebook/Meta) for that.

I’ve got their site ready to go on Hostinger, so now it’s just down to the DNS update.

I started by logging into GoDaddy, and was surprised to see there are no domains listed for their account. Then I pieced together what must have happened. We had canceled their Media Temple hosting several years ago, but there was still a (no longer necessary) annual SSL certificate auto-renewal that kept happening. The client finally reached out to me in time to stop the auto-renewal this year, so we canceled it, effectively ending all of the client’s paid services with GoDaddy. But their domain continued to use the Media Temple name servers.

I’m just guessing here, but it seems that canceling the last paid GoDaddy service also shut off access to the domain management tools. So, their undead DNS zone file endures on Media Temple’s zombie name servers, but with no way to make any edits.

No worries, I thought. Let’s just jump back to NameSecure. It’s ancient, but it is where their domain is actually registered, and they do let you set your domain to use their own name servers. (I always prefer to keep name servers at the registrar, whenever possible, anyway.)

But… oh dear. NameSecure’s zone file editor only lets you create A, MX, and CNAME records. No TXT records. Seriously… no TXT records??? In 2025, those are essential for a variety of reasons. So, we can’t do that.

OK… then I guess our only alternative is to… ugh… move the name servers over to Hostinger. That is obviously what Hostinger wants us to do. That’s what hosting companies always want you to do, and the main reason I don’t want to do it. It’s a lot more of a pain to switch hosting companies when your current host is handling your DNS. It’s not truly lock-in, but it might as well be.

Anyway, that’s where we are. No one back in the late ’90s and early 2000s planned for the 2025 domain registrar zombie apocalypse.

And now, I’m just sitting here, waiting for the name server change to propagate, so I can actually edit the client’s zone file in Hostinger. This is the next-worst thing I can say about Hostinger: if you host domains, let them edit the DNS zone file even before they’re using your name servers! It would be so, so nice if I could plug in all of the client’s MX records while I’m waiting for propagation. As it is, there’s going to be some indeterminate amount of time when their email absolutely will stop working, between when the name server change propagates, making Hostinger allow me to actually edit the damn zone file, and when I actually get those records plugged in and they propagate.

Ideally, this should only take a few minutes. But name server changes can take anywhere from a few minutes to a day or more to propagate. Do I just need to sit here on the edge of my seat that entire time?

I guess so.

Well, that has me all caught up on this stupid situation. I don’t have much else to say about it… I just have to wait. But all of this just feels like another nail in the coffin of this stupid industry that never planned ahead and also never seems to learn from past mistakes.

“We’re sorry, your order for $3.7 trillion worth of bumper stickers has failed.”

One of my web clients emailed me about a rash of fake orders they received a few days ago.

Usually these fake orders are for very small amounts, because the people (or bots) placing them are testing stolen credit cards, so they try to fly under the radar.

I’m not sure what was going on here. Needless to say, my client was unable to fulfill this order. And I’m not masking their IP address (the only part of this that isn’t fake) because they don’t deserve anonymity.

This person (most likely not a bot in this case) placed 174 orders in a span of just under 2 hours. Many of them were for reasonable amounts. Some were… like this. A few included clumsy attempts at code injection. None of them went through, of course, and no real harm was done. I’m just a bit mystified at what this person was hoping to achieve.

It almost seems like the work of a bored teenager, trying to flex their ersatz hacking skills.

It’s just kind of unfortunate that this wasn’t real. The sales tax alone could have fully funded the Minnesota and Hennepin County governments for quite a while. (I looked it up. The state’s revenue is around $2 billion per month. At that rate, this single order could have kept the state fully funded for about 13 years. Oh, and this was only one of several orders for amounts like this.)

It gets even better! I checked the order notes, and it turns out PayPal rejected the order because the quantity exceeded the allowed value. No shit. Why doesn’t WooCommerce limit the quantity input field? Ridiculous.

OK, so how do we fix this?

It seems obvious that we should be able to set a maximum value for the quantity input. And there are WooCommerce add-ons that give you a lot of flexibility to set minimum and maximum quantities on an individual product basis. There are plenty of reasons a complex ecommerce store might want or need those kinds of rules, but I want something more basic. I just want to set a hard upper limit of 100 on the quantity field for all products in this store.

The WooCommerce quantity field is an <input type="number"> field. And that field has min and max attributes you can use to set the valid number range. A number input is a special text input field that has up and down arrow keys that let you increment/decrement the value by amounts controlled by the step attribute.

But here is what I find absolutely fucking maddening about number inputs: you can also type in them. You can type whatever you want in them. You can type letters. Special characters. And worst, numbers outside the range dictated by the min and max attributes. Browsers are “supposed” to validate the input and prevent submitting the form, but in practice I’ve seen that both be extremely hit-or-miss, and also vary wildly from browser to browser.

It makes no sense to me at all why browsers allow anything other than numerical characters in a number input, i.e. the digits 0-9, plus periods, commas, and hyphens. Why? WHY???

Sometimes I really hate this job.

Anyway… let’s pretend number inputs don’t suck. Hooks make it easy to set the max value for the WooCommerce quantity input. Here’s what I used:

function my_woocommerce_quantity_input_args($args, $product) {
    $args['max_value'] = 100;
    return $args;
}
add_filter(
    'woocommerce_quantity_input_args',
    'my_woocommerce_quantity_input_args',
    PHP_INT_MAX - 1, 2
);

You could add a conditional to only set the max_value to 100 when it’s not already set (the default value is -1 by the way), although I was having some trouble getting that to work and I didn’t want to bother troubleshooting it any further… especially since, as indicated by my previous rant, max doesn’t actually do anything to prevent users from typing in a ludicrously large number, or the entire text of the Declaration of Independence, if they so desire. (OK, to be fair, in my testing, Safari’s validation does prevent you from submitting the form with text in the field. But it totally ignores the max value. And I don’t understand why it doesn’t just wipe out invalid characters as you type.)

Side note on my code snippet above: I like to ensure that my code isn’t going to get changed by something else that runs later, so I have a habit of setting the priority on WordPress hooks to PHP_INT_MAX - 1 to get it to run as late as possible. (I use the - 1 to give myself an “out” if I need to add something even later.)

Anyway… we still need a solution, and that’s where we have to resort to JavaScript. (Specifically I’m using jQuery because it’s WordPress.) It’s 2025 and we still have to use JavaScript to work around mind-numbingly basic limitations of HTML. ¯\_(ツ)_/¯

Here’s the jQuery code I wrote to both a) force the quantity input to only accept non-negative integers, and b) actually force the min and max limits, as the user types. (Of course someone could still get around this by disabling JavaScript, but I’m pretty sure there are other steps in the ordering process that would fail without it anyway.)

jQuery('input[type="number"].qty').on('change keyup', function() {
    var min = jQuery(this).attr('min');
    var max = jQuery(this).attr('max');
    var q = jQuery(this).val();
    q = q.replace(/\D/g, '');
    if (typeof min != 'undefined' && min != '') {
        if (Number(q) < Number(min)) { q = ''; }
    }
    if (typeof max != 'undefined' && max != '') {
        if (Number(q) > Number(max)) { q = ''; }
    }
    jQuery(this).val(q);
});

A few notes on this:

  1. I’m using input[type="number"].qty as the jQuery selector because it efficiently identifies the quantity input on both the individual product page and the cart page in WooCommerce. I also probably don’t need to watch both the change and keyup events but I wanted to cover all the bases.
  2. After some trial and error with confusing behavior, I determined that the best UX for this comes from casting the values as Number() right at the point of comparison, and changing the value not to the min or max value, but to just empty it out.
  3. The .replace() method here uses a regular expression that rejects all non-numeric characters. If you need to accept negative numbers or fractions, you’ll need to change /\D/g to /[^\d.-]/g or /[^\d,-]/g if your locale uses commas as the decimal delimiter… or just /[^\d.,-]/g if you want to cover all the bases (and/or allow thousands separators).
  4. Still I am finding some odd behavior here with .replace() that I have never experienced before, and I’m having trouble narrowing down why. It seems to me that this code should be simply stripping non-numeric characters out of the string. So for example if I typed “56g” the value for q would become 56. But what I’m observing is that if the initial value for q is composed entirely of digits, it passes it along, but if there are any non-numeric characters in the string, the entire value gets wiped out. (I tested this with various regular expressions, e.g. \D, [^\d], [^0-9] etc. And I used alert() to output the value of q immediately after the .replace() line, to make sure it wasn’t an issue with the subsequent logic. I am absolutely mystified by this, but I’ve already spent more time on it than I can bill to the client, so I need to stop here.)
  5. I’ve been working with JavaScript for over 20 years, but it’s never been my primary area of emphasis… it still kind of feels like a foreign language to me, whereas PHP is my native tongue. So I don’t always do my JavaScript in the most “JavaScript-y” way.

WordPress capabilities crisis

This post originally appeared on the blog for my ICS Calendar plugin.


I’m not just a WordPress plugin developer… I’m also a WordPress user.

This very website, which sells a WordPress plugin, runs WordPress. And a bunch of other WordPress plugins, most of which I didn’t develop.

This morning, I found I suddenly was unable to log into the WordPress admin. Well… I could log in. But I couldn’t access any admin pages. At first I worried the site had been hacked. Although I’m aware of most of the typical hack vectors and have taken reasonable standard precautions to prevent that.

What happened today was unlike anything I had ever encountered before. It started with a fatal error in Gravity SMTP — the plugin I use for sending emails. I’m not throwing Gravity under the bus. What happened was totally not their fault; their plugin just happened to be the canary in the coalmine.

Specifically, I got this error:

Fatal error: Uncaught Error: Call to a member function add_cap() on null in /wp-content/plugins/gravitysmtp/includes/users/class-roles.php:105

That led me on a wild goose chase of deactivating plugins, reinstalling WordPress core, rebooting my server, manually editing records in the database… the stuff they tell you you “shouldn’t” do, but I know how to do because I’ve been doing this since before the rules existed. (In fact, some of my youthful mistakes in the late 1990s and early 2000s probably helped define the rules.)

None of that got me anywhere. One thing I did find that I thought would solve the problem was my discovery that the record in the wp_options data table that stores all of the roles and capabilities was missing. How does that happen?

A hack was unlikely. Data records do get corrupted or accidentally deleted from time to time. It’s possible that it was an obscure bug in a plugin, or more specifically a plugin conflict. I may never know.

Anyway, I tried to fix that by copying the corresponding record from the database of another WordPress site. But even though the {$table_prefix}_user_roles option was back, WordPress wasn’t reading it.

At least I knew the realm of the problem at this point, so I searched for another answer, and that’s when I found this helpful tutorial from Hostinger.

This requires using WP-CLI to run some commands over SSH. I didn’t have WP-CLI installed, but fortunately that’s easy to do.

Once I had it installed I ran the wp role list command, and was alarmed to get back an empty list. So maybe there was a second option I would have needed to restore in the database, but that didn’t matter because by now I was at the command line and could just do the rest of what I needed right there.

First, I needed to restore all of the core WordPress roles. That required a few simple commands:

wp role reset administrator
wp role reset editor
wp role reset author
wp role reset contributor
wp role reset subscriber

With the roles reset, I was actually able to access the WP admin area again, and I could proceed to the next step: the e-commerce portion of this site, unsurprisingly, is built on WooCommerce, so I needed to get Woo’s custom roles reset. The WP-CLI stuff only works for WordPress default roles, not custom roles created by plugins.

I checked my Users list, and sure enough all of the customers just had “None” listed as their role. I was reasonably confident the data was intact, and it was just that the “Customer” role was undefined.

Most plugins that make these kinds of data modifications have special functions that run on activation, so I just deactivated and reactivated all of my plugins, and that seems to have reset everything.

(Yes, I could have just run the wp role reset --all command to take care of all of that at once, but I didn’t read that far in the tutorial!)

In conclusion, this post is intended to serve two purposes:

  1. To help other WordPress site admins who might encounter this weird and alarming problem.
  2. To explain to ICS Calendar Pro customers what was going on if they had any trouble accessing the site earlier today.

While this problem is certainly weird and alarming, it is also exceedingly rare. I have built, literally, hundreds of WordPress websites in the past 18 years. This is the first time I have ever encountered this issue. Let’s hope it’s the last. But if not, at least now I know what to do, and can jump straight to a 2-minute solution using WP-CLI, instead of fumbling around in the dark for 2 hours like I did this morning.

Jaco Pastorius: The Chicken/Soul Intro Chord Changes

Note: I’ve copied this over from my Patreon, mainly as an SEO experiment. These days I’m generally keeping the music-related stuff on YouTube and, to a lesser extent, Patreon, but I want to see whether this post is more “google-able” here or there.


This probably doesn’t warrant a full YouTube video. (Then again, maybe it does, but I have a gig this afternoon and I don’t have time to make one right now!)

The big band I play in (the one that has a gig this afternoon) has “The Chicken” in our books as an encore/jam. I don’t really use the sheet music for it at all because… well, every bass player should know how to play “The Chicken.” It’s just a 12-bar blues in B♭ with a funky groove.

“The Chicken” was written by Pee Wee Ellis when he was in the James Brown band, and was famously covered by the Jaco Pastorius Big Band in the early 1980s. The main part of the song, as I said, is a straightforward blues, but the Jaco recording begins with the “Soul Intro,” a gospel-infused, slow 3/4 intro.

My sheet music has a transcription of what Jaco plays in this intro, and I’ve memorized it, but it always helps to understand the underlying form when I’m going to be playing something like this.

Here’s the part as written (re-engraved by me in MuseScore so I can overlay the chords later; note that I’ve omitted a bunch of articulation marks because they’re not relevant here):

Once you’ve worked out where to play these lines on the fingerboard, this isn’t really that difficult to play. But I find that it’s a lot easier to work out where to play the lines if I understand the underlying chord structure.

I only have two things to go by here: this written out bass part, and the recording. I checked the rest of the books in our band library. I guess there’s no piano part, because the piano player is using the guitar part, and the guitar is just tacet in the intro. There’s also no score, and of course the changes for this wouldn’t be written in the horn parts. So, I’m on my own.

First I tried working out the chords just by looking at the bass part on its own. That got me… pretty close. But then I listened to the recording and tried playing the chords on the keyboard along with the music, and I discovered I was a bit off on a couple of them. (Specifically, it was minor details like treating the opening chord as a B♭Maj7 instead of just B♭, missing a passing ♭VII/II, not correctly guessing that the part where I’m just playing F octaves is, in fact, an F7sus4… stuff like that.)

Anyway, once I had both examined the written bass part and played my keyboard along with the recording, I came up with the chords shown below. How do you think I did?

To summarize in text, here are the changes as I have them (using percent signs for measure repeats):

B♭ | % | D7 | % |
Gm7 | B♭7/D | C6 | % |
D♭ | % | F7 | % |
G♭6 | % | F7sus4 | % |
% | % | E♭ Gm/D Cm7 | B♭ |
E♭7 | Bb6 ||

I’m still unsure about that 6 on the C6 in bar 7. It sounds weird played on the keyboard, so it’s mainly for the benefit of the bassist.

(Of course, I only noticed after I published this that MuseScore had notated the G♭s as F#s in bars 13 and 14. They’re G♭s in the original.)

Maybe I don’t really need a formal ADHD diagnosis

My undiagnosed ADHD brain this morning (abbreviated):

While making coffee, decided I was going to have a bagel. Got out the cream cheese and realized it was moldy. Washed out the container and went to throw it in the recycling. Discovered we didn’t have a paper bag for recycling under the sink. Went to the basement landing where we keep the paper bags. Discovered the motion sensor light wasn’t working. Replaced the bulb, but found both the bulb and the sensor were dead. Threw them both away and just installed a new bulb directly in the socket. Went back to the kitchen (surprisingly, remembering the paper bag for recycling!) and made a different breakfast since there was no cream cheese for the bagel. While eating, watched some YouTube videos. Glanced at my own YouTube channel stats, and went off on a stream of consciousness writing a script for a new YouTube video I’ll probably never make. Decided to have a second cup of coffee. While the water was heating up, noticed the stove needed to be cleaned. Took off the knobs to soak them in the sink, but found the sink drain was clogged. Took apart the sink drain to clear the clog. Realized the whole thing kind of needs to be replaced, but somehow managed to restrain myself from going to the hardware store right that instant. Cleared the clog and got things reassembled. Made my second cup of coffee. Decided I needed to document the morning here.

Still haven’t even started on my actual work for the day and it’s almost noon.