Self Sustaining Spam Stopper

I’ve been poking at a plugin on this site for a while that stops spam without sending the contents of comments to an external service for processing.

It’s not that using an external service is the wrong approach—it’s obviously a more powerful approach and likely to be more effective in many cases—I’ve just always found it annoying that spam has had that much control over us for so long.

Things have been working well enough for quite a while. It’s blocked somewhere around 4000 spam comments in the 6 months, blocked 0 false positives, and allowed a small handful (~5) through. I’m confident enough in it now for this site that I may now add a filter to auto-delete spam comments.

What’s really funny to me is that this is a simple honeypot with a couple lines of JavaScript that wait a second before clearing one of the prefilled form values.

If a bot is using wp-comment-post.php to submit the comment without checking form field names first, which most are, it fails.

If a bot grabs the HTML, looks for a form, and submits it in under 1.5 seconds, which most others seem to do, it fails.

If a bot actually loads a full browser session and waits for JavaScript to load, it passes. Luckily, not many bots do that!

I’ve also hacked in some support for Contact Form 7. A similar honeypot can be added to any form with the custom [ssss] field. That’s only been up on the Happy Prime contact form for a couple days, but our stream of spam email has gone to zero. This is very pleasant.

All of this is to say that you too can easily try Self Sustaining Spam Stopper! It’s been so long since I’ve submitted a plugin to the WP plugin repo and I’ve kind of missed the fun. It’s also such a great way to deploy open source plugins to many sites across multiple hosts at once, especially now that WordPress supports auto updates for plugins.

Check it out if you’re looking for an alternative. Let me know what I’ve missed. Open up an issue if you have suggestions!

A checklist for how I’d like comments to work in WordPress

I don’t get a lot of comments on this site, and I don’t leave a lot of comments elsewhere, but I like the idea of comments in general and I think I would engage more if I enjoyed the tools more. I think having a good commenting system would be a fun addition to WordPress.

Here’s a list of things that address how I think comments could be handled in WordPress so that they’re more flexible and enjoyable.

It should be possible for comments to be private.

In current WordPress, the only way for a comment to remain private is for it to stay in an “unapproved” status. It would be nice if comments could flow more easily between unapproved, private, and public.

Ideally:

  • When someone leaves a comment, an option is available to submit the comment as private so that only the post author will see it.
  • When a post author moderates a comment, an option is available to make it private.
  • A post author has the ability to privately reply to a public or private comment within the WordPress admin.

Commenters should be notified of their comment interactions.

There is no way in current WordPress for commenters to subscribe to comment threads or status changes via email.

Ideally:

  • Opt-in email notifications should be sent to the commenter when their comment has been approved and published.
  • Opt-in email notifications should be sent to the commenter when a reply has been posted to their comment.

Comments should have full webmention support.

Current WordPress does not have built in support for webmentions, though does have support for pingbacks and trackbacks. It would be nice if WordPress had the capability to be a webmention dashboard of sorts.

Ideally:

  • A commenter should be able to submit a comment via webmention from their own site.
  • An author should be able to send a webmention reply to comments received via webmention.
  • An interface should be available in the admin to send webmention comments to other sites. These webmentions have permalinks that can be used for replies.

Repeat commenters should be recognized

In current WordPress, information about commenters who are not also site authors is stored with each individual comment rather than being tied to the user table or another comment author table. It would be nice if simple passwordless authentication could be used to verify repeat commenters without requiring a full user account on the site.

Ideally:

  • An opt-in email notification is sent to a commenter with a one-time authentication link that, when clicked, “verifies” them on the site.
  • When a comment is left under that same email address in the future, and a cookie is not available in the browser for verification, a new one-time authentication link is sent for verification.

A lot of these items rely quite a bit on: how to ensure a self-hosted WordPress site properly manages email. But I think they’re all very doable and would enhance the overall commenting experience.

Now that I have a list, I’m going to start spending some time poking through it for this site. If you happen to know of helpful prior art, please leave a comment or send a webmention! 🍻

Dealing with big WordPress plugins

This post has been sitting as a draft for over 2 years now and I don’t remember why I didn’t just hit publish at the time. I’m no longer at WSU, but I do still agree with what I wrote, so here you go!

May 15, 2017:

I spend quite a bit of time on plugins at WSU.

One of the challenges is finding the right way to provide a variety of features to 4500 users while not sacrificing security, performance, and stability.

Every time we need a feature, we’re faced with two options:

  1. Use an existing plugin.
  2. Create a new plugin.

The best, best, best answer is to use an existing plugin. When this happens, it means that something out there not only meets the requirements, but happens to align with our other guidelines:

  • Has an open source license that is compatible with the GPL v2 (or later) license.
  • Has an open development process and a well defined path to contribution.
  • Solves a very specific need and does not contain a large amount of code.
  • Passes a full code review by WSU’s web team or other trusted members of the web community.

When I started tackling all of this a few years ago, I made a few exceptions for some big WordPress plugins. A couple require licenses, a couple have a massive code base, and a couple have fairly closed development processes without a defined path to contribution.

At the time I figured the trade off was a great set of features with a relatively low level of effort. A few years later I now know that it’s these plugins that add the most overhead. Not financial overhead—the world of WordPress plugins is still priced so very low, but time and effort overhead.

There are a handful of smaller plugins we’ve had installed for years now without any kind of issue. They usually solve a limited number of problems and, because of the backward compatibility provided by WordPress, just work.

When a plugin gets big, it becomes more of a product than a solution. It often has a product team focused on improving the feature set, attracting more customers, and increasing revenue. Changes are made that adjust markup on the front end and UI/UX on the back end, all in an effort to improve the experience for the product’s customers.

As the maintainer of a diverse platform powering 1500 sites at a university, I’m more often looking for solutions rather than products.

To me, these are plugins that have made a handful of clear decisions and modified WordPress in a small way. Ideally, there’s a way to unhook each of the decisions or a way to build on the solution in a custom way. I can create a simple plugin that extends with decisions of my own.

So, building on an earlier writeup, if you find yourself in a position where you need to support thousands of sites and users and you have the opportunity to start from scratch, here’s how I’d approach plugins:

  • Keep things simple.
  • If a big plugin is useful, find the right way for it to be activated on only one site.
  • Build extensions that provide access to the big plugin from other sites.
  • Make your own plugins small and focused.
  • Don’t get behind on updates.

I’ll stress a couple of these in more words.

If you give a site owner the ability to activate a plugin, there is a very good chance they will activate the plugin. 1500 sites later, you’ll be digging for hours to see who is really using the plugin when you want to reduce its impact. So make sure big plugins are only activated exactly where you want.

Don’t get behind on updates. The WordPress plugin model doesn’t really account for LTS releases, so security updates and bug fixes are shipped alongside UI/UX changes. If you’ve fallen for a big plugin and the product changes are causing an unexpected burden, you’re still better off updating and dealing with it now.

And get good at saying no in a productive way.

Restricted XML-RPC methods for WordPress mobile app support

For a long time I’ve had the path to xmlrpc.php blocked completely on a handful of sites so that I didn’t have to worry about Things. One thing that this messes with, if you don’t have Jetpack installed, is the WordPress mobile app. Without a WordPress.com connection, the mobile application relies on the XML-RPC API provided by your WordPress.

So I made a couple plugins.

First, the one you shouldn’t use in production, Log XML-RPC Requests, does exactly what it implies: logs incoming XML-RPC requests to a WordPress site as a custom post type.

I activated this on a site and then went screen by screen through the WordPress Android application to determine what XML-RPC methods were absolutely required in order for things to work.

That data was then used in the Restricted XML-RPC Methods plugin. This rejects any XML-RPC request that is not one of those required by the WordPress Android application. And pingbacks, because they’re underused and will always have a special place in my heart. 🙂

Any extra methods required by the WordPress IOS app are not enabled, but only because I don’t use an iPhone as my primary device. I’m happy to add those in if somebody passes me the list. It’s also possible that it just works!

Encoding serialized attributes for Gutenberg blocks in PHP

I stumbled backwards into this and didn’t see it discussed anywhere else yet, so here’s a path for anyone else finding themselves temporarily confused and internet searching.

Say you’ve created a custom block for the WordPress block editor, Gutenberg. In addition to regular content, there are one or more attributes associated with the block:

<!-- wp:jf/custom { "data":"Some data assigned as an attribute." } /-->

Now that you have the block in place, you want to migrate the meta from thousands of posts into this structure and inject it into the post content. This data can contain all sorts of characters, including quotes and HTML markup.

If you enter quotes and HTML markup directly into Gutenberg, the content is saved like so:

<!-- wp:jf/custom { "data":"Hey, a \u003Ca href=\u0022http:s\/\/jeremyfelt.com\/\u0022\u003Elink to my website\u003C\/a\u003E." } /-->

This works because the Block API replaces a handful of special characters via regex after JSON.stringify()has done the initial translation.

WordPress does not have a PHP mirror of the Block API’s serializeAttributes(), but the existing functions for encoding JSON are really flexible. Per the documentation for json_encode(), which itself is used by wp_json_encode(), a handful of constants are available to apply as a bitmask to ensure specific characters are encoded in a specific way.

So we end up with this:

<?php // String from non-Gutenberg data source. $string = 'Hey, a <a href="https://jeremyfelt.com/">link to my website</a>.'; $attribute = wp_json_encode( $string, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE ); // And $attribute is now: // "Hey, a \u003Ca href=\u0022https:\/\/jeremyfelt.com\/\u0022\u003Elink to my website\u003C\/a\u003E." // If storing with wp_insert_post(), etc... $attribute = addslashes( $attribute ); $block = '<!-- wp:jf/custom { "data":' . $attribute . ' } /-->';

And the block string can now be stored as part of the post content.

Note: addslashes() is needed if storing the data with wp_insert_post(), etc… because those strip slashes and \u003Ca looks like a slash that needs to be stripped unless it itself is slashed.