Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: wordpress

  • Zaptodon

    Zaptodon

    On a site, I use Zapier to automate a specific set of tasks. Every day, the website sets up a show/character of the day (I think you know what site this is…) and it posts that show/character to Twitter, Tumblr, Facebook and …. Mastodon.

    Or at least it does now.

    Zapier

    Caveat! I pay for this service. The webhooks used are in the starter package at $19.99/month annually.

    There’s a service, Zapier, that allows you to make incredibly complex if/then/else checks and performs an action. It’s not cheap, but at $250 a year it’s not expensive for my needs. Way back when, I picked it because I needed something that would let me actually script, and I recognized that running pushes like that all from my own server was a lot more work than it should be, what with all the libraries and so on.

    Seeing as that wasn’t driven by a post or any action except time, it had its own quirks. But then one day in May we all woke up and saw how dumb Twitter was. They priced WordPress.com and Automattic out of their own API!

    But not Zapier.

    Note: Automattic made the right choice here. With the API cost starting at $42,000 a month (yes, a month, and I can remember when I made that a year and thought I was hot shit), knowing how inexpensive Jetpack Pro/Premium/Whatever is, there was no way they could justify it.

    Zapier’s business model has a large chunk invested in pushing things to social (among all sorts of cool things, like calendar integration). So when I had to revisit how I posted new articles to Twitter anyway, I figured I’d wrangle Mastodon as well.

    The Zap Flow

    Overall, my flow looks like this:

    Screenshot of the Zapier flow, which shows: 1 - new item in RSS, 2 Post in Webhooks, 3 Create Tweet

    But what goes in each?

    The first one is a built in and easy trigger. It follows the RSS for the site and, when there’s a new article, off it goes.

    The third one is the tweet, which is about as straightforward as you might expect.

    The second one is Mastodon. That’s where we’re going to concentrate today.

    Add an App

    To do this, you need to create an ‘app’ for your Mastodon account. Log in to your instance (mine here is mstdn.social) and edit your profile. On that page, on the menu to the left, is an item called Development.

    On that page you’ll see a list of Applications, if you have any, and a button to create a New Application. That’s what we want to do today. Click on that button and you’ll get the basic data:

    New Application setup on Mastodon, asking for name and website.

    I put in “Zapier” and “https://zapier.com” as my

    Scroll further down and there are a bunch of options. You only need to have these two checked:

    • read:accounts
    • write:statuses 

    The read will let us know things worked, and write … I really hope that one is obvious for you, but you have to be able to write to post.

    Click create and you will be redirected to the Application page where it will now list your app to Zapier. Click on that and you’ll be show a page with the set up info, but now it has a Client Key and a Client secret.

    Example of the client keys and tokens.

    I clicked regenerate right after I took this screenshot.

    You’ll be able to get at this whenever you want, so don’t panic.

    Back to Zapier

    Over on Zapier, pop open your Zap. In my case, I had a pre-built one for Twitter, so I added this in by telling it I wanted to add an Action. Since I pay for Zapier, I have access to their premium webhook to post:

    Pretty clear I think. I need “Webhooks by Zapier” and the Event is POST. That’s telling Zapier what to do with the hook.

    The next part of the step is the Action and that has a lot of stuff. The first two are the URL and the Payload:

    The URL is going to be https://yourinstance.com/api/v1/statuses?access_token=[YourToken] — What’s your token? Remember that as the third item shown on the edit page for your Application over on your instance? Yep! Paste that in.

    I picked JSON for my payload since I’d been using it elsewhere. For the next part, I have to dig out the data. For Mastodon, you want your data ‘type’ to be status since that is literally telling it “I wanna make a status post!” and the content I made the description and the link.

    Example of STATUS and content.

    If you click on that box where I have description etc, it’ll pop up with more options!

    Example of other data you could insert.

    Pretty nifty! I left the rest as default:

    • Wrap Request In Array – no
    • File – empty
    • Unflatten – yes
    • Basic Auth – empty
    • Headers – empty

    Click on continue and you can test.

    Done!

    That’s it! Now your RSS feeds will auto post to Mastodon.

    I’m sure someone’s wondering “Why aren’t you using ActivityPub, Mika!?!” And the answer is… It doesn’t actually work on all hosts. ActivityPub requires you to be able to write to your .well_known/ folder and, currently, you cannot do that on DreamHost because it’s managed at the server level.

    This is not a wrong choice by either party! DreamHost (especially on DreamPress, the managed WP solution) wants to prevent you from breaking your SSL. Now, thanks to @diziara, there is a workaround if you can edit the .htaccess file in your .well_known folder:

    # Permit access to the challenge files but nothing else
    Order allow,deny
    Allow from all
    
    RewriteCond %{REQUEST_URI} ^/[.]well-known/webfinger+$
    RewriteRule .* /wp-json/activitypub/1.0/webfinger [L]
    
    RewriteCond %{REQUEST_URI} ^/[.]well-known/acme-challenge/[a-zA-Z0-9_-]+$
    RewriteRule .* - [L]
    
    RewriteRule .* - [F]
    

    Assuming your install is in root (mine is) you put that into the .htaccess and it works! I was surprised that it also let me edit on DreamPress, but I’m not sure if that will last. I’ll keep my support-thread updated though.

    And the other thing… I don’t want people to ‘follow’ my blog like that. I mean, you could, but also people follow me as me, and if I auto-post to ‘me’ then it works. Telling people to follow my blog and me is tricky since people are lazy (seriously we all are). But if that’s your thing, then yes, you absolutely can follow @ipstenu@halfelf.org and get all my articles.

    I’m still going to use a combination, since while I do want people to follow my blog, I suspect more will follow me instead. Also it’s easier to follow up on engagements (questions etc) if I’m watching ‘me’ and not two places. The other problem is it’s letting you follow ME at my blog. My other site has many more authors, and this isn’t quite right for that.

    The nice thing, though, is that there isn’t a single perfect answer for everyone’s use case. For most people, ActivityPub will work and they can be made discoverable. For the others, though, hold on until end of June. My friends at Automattic are planning to have post-to-mastodon support in their next iteration.

    I’ll still need my zaps since I post things that aren’t blog posts, but I’m looking forward to one less.

  • Stopping Jerks in Gravity Forms

    Stopping Jerks in Gravity Forms

    “Hang on,” I hear you say. “Didn’t you already do this?”

    Well, kind of. I did with Ninja Forms (and actually that resulted in me removing Ninja Forms and going back to Jetpack, and isn’t that a pill?). And I’ve mentioned Gravity Forms before with disallowed keys.

    This is really an extension of the disallowed keys and some very specific complex things I’ve changed since those posts.

    Yeah, tech changes.

    What’s a “Disallowed Key”?

    Disallowed keys are basically all those terms you put in your Disallowed Comment list (formerly the Blacklist):

    A screenshot of the disallowed comment keys field from Wordpress, with a string of examples like 'foo.bar@gmail' and so on

    Now if you look at my example above, I have a bunch of terms in there. Each line is a ‘key,’ meaning @example.net is a key.

    WordPress built this in for handling comments, specifically, but I’ve long advocated for things to be global, and since this is saved in my site, I can easily re-use it by telling Jetpack or anything else “If this email is on the list of disallowed keys, I don’t want to see it.”

    The reality of doing that is, of course, harder than it looks.

    For example, if you use Gmail, then foobar@gmail.com and foo.bar@gmail.com and foobar+ihatemika@gmail.com are all the same bloody thing! And if I block generaljerkyperson do I then want to block generaljerkyperson@nonjerky.com ?

    You have to make some decisions. Now solving the email one with periods and plus signs was easy. By comparison.

    Jerky People Aren’t Spammers

    This is the other thing to get into our heads. Jerky people ain’t spammers. Spammers are, weirdly, easier to spot and handle. Someone posts about viagra? Spam. Someone posts about SEO? Probably spam (though not always on a tech site). But those are all pretty self evident once you look at them!

    What is not self-evident is something like the world I have.

    Over on a site I run, we have a group instance (Slack, Discord kind of thing) and we let people sign up via Google Sheets for a while. But we ended up with one person in the group who was an antagonist. Let’s call her Bertie.

    Bertie isn’t a terrible human, but she has “Opinions” and they are the right ones. She likes a very specific thing, and does not like variations or changes. And if you don’t like what she does, you’re wrong, she’s right, and damn you. She picks fights, constantly, she disrespects anything anyone else likes… To put it how my friend Tracy might say “She yucks on other peoples’ yums.”

    We attempted to negotiate with her. Point out “Hey you’re in a GROUP and if you cannot respect people enough to tolerate their opinions when they differ, then this ain’t the group for you.”

    Bertie didn’t. And she didn’t change. So we banned her. Fine.

    She tried to sign up via Google Sheets again. We said no. And again. No. Then she tried new emails, new bios, new fake names. One actually got in. But as soon as we learned it was her, we kicked her.

    And at this point, we were fucking tired of playing whack-a-mole. So I decided to make something better.

    Catch and Release

    My overall thought process boils down to this: Every single jerky person has tells.

    They just do. And a human can spot them and go ‘wait a second…’ because we’re actually really good at recognizing patterns that are similar. A computer has to be taught that “If the submission is from this region or has this kind of email, it’s probably them.”

    So I started to build out some logic that would check all my flagged emails and IPs. Then I took advantage of Gravity Forms’ API to make a note in the entry so I could have a record of why someone was flagged.

    Here’s what it looks like for someone who’s email and IP was on the naughty list:

    Seeing that come in overnight, by the way, delighted me. It shunted the annoying Bertie to spam for two reasons, and none of us had to deal with her.

    How it Works

    This is the code part. The down and dirty here is I have two classes, one for finding spammers, and then the Gravity Forms that calls it. I did this because at the start, I had both Jetpack and Gravity Forms AND Google Forms. Obviously I can’t block someone hitting the Google Form directly, but I used to have this prevent that from loading the form. It was easy to get around. I know I know.

    Even though I now exclusively use Gravity Forms on the site, I left these separate to be future friendly. Also it means you can steal that to put it into whatever you’re doing. It’s GPLv2.

    By the way, everything goes to spam for a simple reason: It lets me clear up false positives.

    Find Spammers

    This class is what hooks into disallowed keys and checks if the email is banned, the domain is banned, or the IP used it banned. It also has an option to check for people who are moderated! That means if you wanted to flag people who might be jerks, you can do it.

    class Find_Spammers {
    
    	/**
    	 * List of disallowed Keys
    	 *
    	 * We check for emails, domains, and IPs.
    	 *
    	 * @return array the list
    	 */
    	public static function list( $keys = 'disallowed_keys' ) {
    
    		// Preflight check:
    		$valid_keys = array( 'disallowed_keys', 'moderation_keys' );
    		$keys       = ( in_array( $keys, $valid_keys, true ) ) ? $keys : 'disallowed_keys';
    
    		// Time for the show!
    		$disallowed_keys  = array();
    		$disallowed_array = explode( "\n", get_option( $keys ) );
    
    		// Make a list of spammer emails and domains.
    		foreach ( $disallowed_array as $spammer ) {
    			if ( is_email( $spammer ) ) {
    				// This is an email address, so it's valid.
    				$disallowed_keys[] = $spammer;
    			} elseif ( strpos( $spammer, '@' ) !== false ) {
    				// This contains an @ so it's probably a whole domain.
    				$disallowed_keys[] = $spammer;
    			} elseif ( rest_is_ip_address( $spammer ) ) {
    				// IP adresses are also spammery people.
    				$disallowed_keys[] = $spammer;
    			}
    		}
    
    		return $disallowed_keys;
    	}
    
    	/**
    	 * Is someone a spammer...
    	 * @param  string  $email_address The email address
    	 * @param  string  $plugin        The plugin we're checking (default FALSE)
    	 * @return boolean                True/False spammer
    	 */
    	public static function is_spammer( $to_check, $type = 'email', $keys = 'disallowed_keys' ) {
    
    		// Default assume good people.
    		$return = false;
    
    		// Get disallowed keys & convert to array
    		$disallowed = self::list( $keys );
    
    		if ( 'email' === $type ) {
    			$email_address = $to_check;
    
    			// Break apart email into parts
    			$emailparts = explode( '@', $email_address );
    			$username   = $emailparts[0];       // i.e. foobar
    			$domain     = '@' . $emailparts[1]; // i.e. @example.com
    
    			// Remove all periods (i.e. foo.bar > foobar )
    			$clean_username = str_replace( '.', '', $username );
    
    			// Remove everything AFTER a + sign (i.e. foobar+spamavoid > foobar )
    			$clean_username = strstr( $clean_username, '+', true ) ? strstr( $clean_username, '+', true ) : $clean_username;
    
    			// rebuild email now that it's clean.
    			$email = $clean_username . '@' . $emailparts[1];
    
    			// If the email OR the domain is an exact match in the array, then it's a spammer
    			if ( in_array( $email, $disallowed, true ) || in_array( $domain, $disallowed, true ) ) {
    				$return = true;
    			}
    		}
    
    		if ( 'ip' === $type ) {
    			$ip      = $to_check;
    			$bad_ips = false;
    			foreach ( $disallowed as $nope ) {
    				if ( rest_is_ip_address( $nope ) ) {
    					if ( ( strpos( $ip, $nope ) !== false ) || $ip === $nope ) {
    						$bad_ips = true;
    					}
    				}
    			}
    
    			// If they're a bad IP, then they're a bad IP and we flag.
    			if ( false !== $bad_ips ) {
    				$return = true;
    			}
    		}
    
    		return $return;
    	}
    
    }
    
    new Find_Spammers();
    

    Gravity Forms Check

    You may have noticed that the spammer checker is really just that, a checker. You have to call it. How I call it is via a Gravity Forms function. This does a couple kind of redundant things, and I know it can be optimized.

    The IP checker has some extra stuff to help me record where an IP is from when people submit, in order to try and catch other ‘common traits.’ It’s using ip-info, and amusingly I’ve found it’s mostly right. For some reason, it got the IP location of the same IP as being from 3 separate locations. I suspect it’s Bertie trying to be smarter and use a VPN. The fact that the Location is not the only measuring stick I use though means she can change her IP and email a bunch of times, but I have other checks.

    One improvement on my list is that if someone has a certain number of red-flags, it treats it like a jerk and sends to spam.

    class My_Gravity_Forms {
    
    	public function __construct() {
    		// Check all Gravity Forms ... forms for spammers.
    		add_action( 'gform_entry_is_spam', array( $this, 'gform_entry_is_spam' ), 10, 3 );
    	}
    
    	/**
    	 * Mark as spam
    	 *
    	 * If someone on our block-list emails, auto-mark as spam becuase we do
    	 * not want to hear from them, but we don't want them to know they were rejected
    	 * and thus encourage them to try other methods. Aren't assholes fun?
    	 *
    	 * @param  boolean  $is_spam  -- Is this already spam or not?
    	 * @param  array    $form     -- All the form info
    	 * @param  array    $entry    -- All info from the entry
    	 * @return boolean            true/false if it's "spam"
    	 */
    	public function gform_entry_is_spam( $is_spam, $form, $entry ) {
    
    		// If this is already spam, we're gonna return and be done.
    		if ( $is_spam ) {
    			return $is_spam;
    		}
    
    		$spam_message = 'Failed internal spam checks';
    		$warn_message = '';
    		$is_spammer   = false;
    		$is_moderated = false;
    		$is_bot       = false;
    		$is_vpn       = false;
    
    		// Loop and find the email:
    		foreach ( $entry as $value => $key ) {
    			if ( is_email( $key ) && ! $is_spammer ) {
    				$email        = $key;
    				$is_spammer   = Find_Spammers::is_spammer( $email, 'email', 'disallowed_keys' );
    				$is_moderated = Find_Spammers::is_spammer( $email, 'email', 'moderated_keys' );
    			}
    
    			if ( rest_is_ip_address( $key ) && ! $is_spammer ) {
    				$ip           = $key;
    				$is_spammer   = Find_Spammers::is_spammer( $ip, 'ip', 'disallowed_keys' );
    				$is_moderated = Find_Spammers::is_spammer( $ip, 'ip', 'moderated_keys' );
    				$is_bot       = self::check_ip_location( $ip, 'hosting' );
    				$is_vpn       = self::check_ip_location( $ip, 'proxy' );
    			}
    		}
    
    		// If this was a bot...
    		if ( true === $is_bot ) {
    			$warn_message .= 'Likely submitted by a bot or someone scripting. ';
    		}
    
    		// If a VPN...
    		if ( true === $is_vpn ) {
    			$warn_message .= 'Using a VPN. This may be harmless, but it\'s also how people evade bans. ';
    		}
    
    		// And if it's a spammer...
    		if ( $is_spammer ) {
    			$message = $spam_message;
    
    			if ( isset( $email ) ) {
    				$message .= ' - Email ( ' . $email . ' )';
    			}
    			if ( isset( $ip ) ) {
    				$message .= ' - IP Address ( ' . $ip . ' )';
    			}
    
    			$result = GFAPI::add_note( $entry['id'], 0, 'My Robot', $message, 'error', 'spam' );
    			return true;
    		} else {
    			if ( ! empty( $warn_message ) ) {
    				$add_note = GFAPI::add_note( $entry['id'], 0, 'My Robot', $warn_message, 'warning', 'spam' );
    			}
    		}
    
    		// If we got all the way down here, we're not spam!
    		return false;
    	}
    
    	/**
    	 * IP Checker
    	 */
    	public function check_ip_location( $ip, $format = 'full' ) {
    		$return    = $ip;
    		$localhost = array( '127.0.0.1', '::1', 'localhost' );
    
    		if ( in_array( $ip, $localhost, true ) ) {
    			$return = 'localhost';
    		} else {
    			$api     = 'http://ip-api.com/json/' . $ip;
    			$request = wp_remote_get( $api );
    
    			if ( is_wp_error( $request ) ) {
    				return $ip; // Bail early
    			}
    
    			$body = wp_remote_retrieve_body( $request );
    			$data = json_decode( $body );
    
    			switch ( $format ) {
    				case 'full':
    					// Return: US - Chicago
    					$return .= ( isset( $data->countryCode ) ) ? ' ' . $data->countryCode : ''; // phpcs:ignore
    					$return .= ( isset( $data->countryCode ) ) ? ' - ' . $data->city : ''; // phpcs:ignore
    					$return .= ( isset( $data->proxy ) && true === $data->proxy ) ? ' (VPN)' : '';
    					break;
    				case 'hosting':
    					$return = ( isset( $data->hosting ) && true === $data->hosting ) ? true : false;
    					break;
    				case 'proxy':
    					$return .= ( isset( $data->proxy ) && true === $data->proxy ) ? true : false;
    					break;
    			}
    		}
    
    		return $return;
    	}
    }
    
    new My_Gravity_Forms();
    

    Any Issues?

    Two.

    1. I use an ‘approval’ feature (forked from “Gravity Forms Approvals” to allow for multiple approvers optional but only one has to approve to be a go – the original requires all approvers to approve) – for some reason this is not properly moving anything in spam or trash to a ‘rejected’ status
    2. IP-Info got a ‘different’ IP location than I see from the IP in two cases. I believe that’s due to the individual trying to juke the system and being caught anyway, but it needs some debugging.

    Oh and clearly I have some optimization I could stand to work on, but that’s for another day.

    This code is live, in production, and has been merrily blocking Bertie for some time.

  • Saving Theme Data to a File

    Saving Theme Data to a File

    Your life isn’t all WordPress.

    I know, I know, I said a dirty thing, but let’s be honest, everything isn’t always all WordPress. And when that happens, you have to do some weird things to make your data shared.

    One of the things I needed one day was a way for non-WordPress files to get access to a theme setting. See, the theme let me set a top-bar and customize it. Sometimes I did that to share news, sometimes to link to latest posts. Regardless, I updated it via Customizer, and it worked great for WordPress.

    Not so much for my static HTML site, or my non-WordPress PHP site.

    I dwelled on it for a while and then thought “Wait, if the theme knows how to echo a specific setting, then there has to be a function for that. And if there’s a function, then can’t I hook into Customizer saving to trigger the creation of an HTML file with the data and call that from my other sites?”

    And guess what? You can!

    The WordPress Code

    Toss this in an MU Plugin and it’ll save a file to wp-content/top-bar.html when you save customizer settings.

    <?php
    class HELF_Top_Bar {
    
    
    	public function __construct() {
    		add_action( 'customize_save_after', array( $this, 'save_top_file' ) );
    	}
    
    
    	/**
    	 * Safe the top file
    	 * When customizer is saved, copy the get_theme_mod() data and parse it
    	 * into a file: wp-content/top-bar.html
    	 * @return n/a
    	 */
    	public function save_top_file() {
    		$default     = 'Something Default Here';
    		$banner_text = get_theme_mod( 'top-banner-text', $default );
    
    		$fp = fopen( WP_CONTENT_DIR . '/top-bar.html', 'w' );
    		fwrite( $fp, $banner_text );
    		fclose( $fp );
    
    	}
    
    }
    
    new HELF_Top_Bar();
    
    

    The Javascript

    The what?

    For my other PHP file, it’s a simple file_get_contents() call to the HTML. But for static HTML I had to resort to trickery with Javascript.

    <script>
    	var elem = document.getElementById("wpcontent");
    
    fetch('https://example.com/wp-content/top-bar.html', {
            credentials: 'include'
        })
            .then(function (response) {
                return response.text();
            })
            .then(function (text) {
                console.log('Request successful', text.length);
    			console.log('Request successful2', text);
    			$('.wpcontent').html(text);
            })
            .catch(function (error) {
                console.log('Request failed', error)
            });
    </script>
    
    <div class="wpcontent"></div>
    
    

    And magic happens.

  • On Behavior and Respect

    On Behavior and Respect

    I’ve had an interesting week with WordPress. It’s been bad enough that I have to preface this post with a note.

    I have no plans to quit WordPress at this time.

    Good Faith and History

    This morning, I woke up thinking about a statement I picked up from Wikipedia. Assume good faith. I like that. I try to do it. The concept is simple and direct. Don’t assume everyone’s evil, instead assume they do mean well, but sometimes they may have trouble expressing it properly.

    And while I do believe that most people don’t mean to be evil (there are exceptions…), I think that more people remain concerned about themselves over anything else. And this self-involved nature causes problems like happened recently, with choices certain companies made to self-promote in ways that other people found offensive and harmful.

    So when I think about ‘good faith’ I do it with a look back to the previous actions someone (an individual or a company) has taken. How have they behaved before? Have they constantly shown poor choices? Is this a first? What happened the last time I tried to talk to them about it? Did we have a discussion? Did I get 15 emails in a row, alternately being called names or being begged to give them another chance?

    That means I find it strange to watch people use the concept of ‘Good Faith’ to argue that they don’t look at people’s past actions to judge their current ones.

    I’d like to think that my consistency would be something people would use to judge my actions, but I’ve learned people whom I’d trusted don’t. And yes, that’s sad. It’s depressing to find out people would rather jump to outrage and pointing fingers and blaming me than taking into consideration 10 years of work.

    Respect and Doubt

    Respect is both given and earned. You give people respect for a position, under the assumption they deserve it, and people either live up to that respect and thus earn more, or they don’t. But when you have an unknown person, you start from assuming good faith based on the hope that they have legitimately earned the position.

    Obviously when you know someone, hung out and had dinner, your assumptions are based on more than that. And if someone has a public history you can turn to, you can use that to base your assumptions.

    That’s not what happened to me this week. Instead, I found out people actually assume bad faith, because perhaps my opinions are different than theirs, or because I saw something in a different way. It felt like “Assume good faith, but only if you’re on my side.” And that? That is sad.

    I imagine how different things would have been to say “Hey, y’all. Mika’s been really careful about using her power here for five years. Give her the benefit of the doubt.”

    Instead, people said I was seeing things that weren’t there. I was playing a victim. It was all in my head. There’s a word for that: gaslighting. God help you if you call them out on it.

    Damage and Care

    It’s in a week like this where I totally understand why so many people have been quitting WordPress. People have worked hard to do good for a community, without any expectations of compensation, but they find out their opinions are dismissed and their word discarded or minimized. They feel disrespected, and it’s worse when they feel made fun of by the community they’re trying to help.

    Some of them have chosen to walk away from WordPress, and I fully support that choice. To do anything less would be like telling someone that the beatings will stop once morale improves. It would be cruel and unkind to dismiss their feelings, and it would mean I’m not listening to them and have no empathy for them.

    Also I’d have to be blind not to see it, because it happens to me all the time. This week? People I thought I knew assumed the worst in me. They didn’t give me the benefit of the doubt and, when I asked what I’d done to deserve that, they said I’d done nothing. They said they were just being fair and hearing all sides.

    In other words all the work I’d done, being consistent and fair, acting carefully and listening to everyone was pointless. In the end, they were just going to dismiss all of that and jump on the bandwagon with everyone else.

    And it was more than one person I’ve know for about a decade who did it.

    For a while I wondered “Did I really mess up here?” And then I asked myself if I’m told I did nothing to be not-trusted, but I was going to be anyway, was there a possible positive outcome here?

    Empathy and Power

    It really boils down to empathy. If someone says “Hey, this hurt me.” and your reply is “Yeah, I don’t see that” then you’re dismissing what they said. And it’s not just because I get treated badly that I have empathy, it’s because my parents, my family taught me to have empathy and care about the strangers as much as I cared about myself. We don’t live in isolation, we live in a community.

    You can see why I call myself a Socialist, right? I care not just about the people I know, but the people I don’t know. I think about the impact my choices have beyond me, and given the amount of power I wield, that has a lot more weight than you might think.

    Oh yes, I have an insane amount of power, and it scares the hell out of me. I could destroy a company with a click. I could insta-ban people for wrongs. I could close plugins for every single security and guideline mistake. I could publicize exactly what specific people did to get permanently banned. Worse, I could spread fear and doubt in the entirety of WordPress, just by closing a plugin.

    I don’t. I handle the majority of that quietly, on the books but privately, because I assume good faith in everyone, even people who make massive mistakes. And because I consider the negative impact to the community in general before I take an action.

    How much trust do I erode in WordPress as a whole with what I do or say? How much damage do I cause? How many people do I hurt? How many people will this person hurt if I permit them to carry on as they have been? Will their uncensured actions damage the reputation of WordPress? Will the community forgive a mistake?

    That’s what I think about, every single day, before I approve, reject, close, or open any plugin.

    Alone and Together

    If you look at some of the people who’ve left the WordPress Community recently, you’ll see a trend. They feel alone. They feel like they’ve been tasked with ever increasing, insurmountable, chores, and they have no support or backup.

    I feel that way too. It took months to be taken seriously about a problem, to the point that serious action was taken. Months, in which I questioned myself. Was I seeing something that wasn’t there? Was my value so little that I’m not worth taking the time to address this problem?

    To put it in perspective for you, someone told me that my father’s death was my fault for banning them for abusive behavior.

    When you look at it, you’d wonder how I could ever doubt myself. Well, that’s what happens when people don’t step up and ask how they can help. And certainly I could have been more vocal about it, but at the same time, it illustrates the invisibility problem in our community. People are hurt all the time, and no one is looking out for them.

    Should I have to scream that someone is hurting me for it to be seen? There’s no oversight in all things, but there’s also no clear way to ask for help. How much worse would this have been if I didn’t have support from people in the community, people in places who could (and did) help me?

    What about everyone else?

    Unending and Critical

    Now look back at Slacks and Blogs and Twitter. You know which ones I mean. Read what people are saying and assuming, and ask yourself “Is this making a welcoming environment?”

    Far too many of us have used our critiques as excuses, without caring for the damage they cause. Dismissing people’s pain. Not offering honest and sincere apologies. We hide behind the veneer of “I’m just passionate” or “I’m being critical.” And instead of discussing the idea, we sling ‘understandings’ like accusations, and we cut at people for disagreeing. We assume the worst and treat people shamefully.

    And worst of all? Our comrades allow this to happen in their backyards. They won’t remove a homophobic ‘joke’ comment because clearly it’s not meant sincerely. They will allow someone to be called a powerless puppet. They give space for hateful comments that barely even have a veneer of merit.

    We’ve stopped encouraging meaningful discourse and regressed into screaming across the aisle that the other person is wrong. We believe our way is the only valid way, and we will tear people down, all the while claiming we’re doing it for the greater good.

    And yet people can look at all that and not see the pitchforks and tiki torches.

    Comments are Disabled

    There’s a reason I disabled comments and mute and block people on twitter with ruthless abandon. It’s not that I don’t want to hear different voices, it’s that it’s stressful to be attacked all the time. It makes a person physically ill. Certainly it’s made me that on more than one occasion.

    I don’t leave comments open, I don’t engage with certain community news sources, I left many Slack groups and I don’t offer comments when asked very often. You see, I can either do good work for the community, do my best and keep things safe, secure, and as fair as a human can, or I can wade through toxicity.

    I decided to do good work.

    I would like to think that a decade of it would allow any perceived missteps of mine to be taken with a grain of salt and a sip of trust. I will still believe in the inherent goodness of people, and their ability to make colossal mistakes. I will still accept an appology when sincerely given.

    But I will not forget and I may not forgive.

    Then again, forgiveness should never be the point of your apology.

  • Spam Your Blacklist

    Spam Your Blacklist

    As mentioned when I began my hiatus, there would be the occasional code post. Here’s one that is born from how annoying someone is.

    The Situation

    I have a serial harasser. He’s a troll and a semi-stalker who doesn’t understand the meaning of “No.” I’ve blocked him on social media, his emails are blackholes, and as I don’t have contact forms on my sites, nor do I have open comment forms at the moment, it’s a non-issue here.

    However, I do have another site which he found and decided to use my contact form to spam me and my co-admin with 10+ emails. When I found out, I blocked his IP address. He was on mobile, though, so I knew this would only last as long as he was on his phone. I needed a better solution.

    This is not a rare problem. Especially not for women online. One of the many ways in which men drive women offline is by upping the emotional labor needed to be online. That is, they attack us with message after message, generally in the guise of being ‘a nice guy,’ or ‘just trying to have an open conversation.’ But the reality is that they want to wear you down and get you to do what they want.

    It’s exhausting. If you’ve ever gone car shopping and had the dealer call you over and over with the hard sell, it’s like that.

    The Paradox

    Contact Forms are meant to be a way for people to contact you, outside of the comments on your site. That being so, they really do need to exist outside the confines of the comments, which means your comment moderation list is a bit inappropriate. You want people who are having comment problems to get a hold of you.

    At the same time, if you’ve blackholed someone, you don’t. You don’t want them to bother you at all, as reading their messages, even though you’re deleting them, is draining. So you want to be able to block them.

    Here’s the problem: most contact forms don’t let you do this out of the box.

    Yeah, think on that for a moment.

    Here are the top four contact form plugins:

    I use Jetpack, and while I may be annoyed I’m also a developer. So I did made an answer.

    The Caution

    This will not block everyone. If your harasser changes emails a lot, you’re out of luck. And this is the ‘excuse’ I see a lot of the time. Why bother if they’re going to change emails? The answer is obvious. If I can inconvenience them enough, and make it clear I don’t care, they’ll go away.

    Also if you do this right, they never know they’ve been blacklisted, so they think they’re getting to you and you’re sipping a damn mai tai.

    The Solution

    In March 2014, I opened a ticket asking for a way to blacklist people. They have made zero forward momentum on this in the 4.5 years since. So this little red hen is doing it herself.

    By using the built in filter for spam (which Akismet uses), this code checks if someone’s on the comment blacklist by IP or email, and if so, flags the message as spam. You don’t get an email. You do still get the message in your spam, which is not a great fix. I’d rather it just get dumped into trash, but there’s no filter I can find for that.

    Still. This works, and it shut the guy up.

    add_filter( 'jetpack_contact_form_is_spam', 'jetpack_spammers', 11, 2 );
    
    function jetpack_spammers( $is_spam, $form ) {
    	if ( $is_spam ) {
    		return $is_spam;
    	}
    
    	if ( wp_blacklist_check( $form['comment_author'], $form['comment_author_email'], $form['comment_author_url'], $form['comment_content'], $form['user_ip'], $form['user_agent'] ) ) {
    		return true;
    	}
    
    	return false;
    }
    

    But. That only helps what’s on the blacklist. And the blacklist has a couple drawbacks. First of all, while it absolutely does handle multiple words (so I can block ‘milady mika’ if I want), it’s a little more complex if you wanted to block someone using gmail and a plus sign in the email address. So if you want to block example+spammer@gmail.comthen you either have to add that in literally or you get creative. I went creative.

    add_filter( 'jetpack_contact_form_is_spam', 'jetpack_harassment', 11, 2 );
    
    function jetpack_harassment( $is_spam, $form ) {
    	// Bail early if already spam
    	if ( $is_spam ) {
    		return $is_spam;
    	}
    	$badlist   = array();
    	$blacklist = explode( "\n", get_option( 'blacklist_keys' ) );
    
    	// Check the list for valid emails. Add the email _USERNAME_ to the list
    	foreach ( $blacklist as $spammer ) {
    		if ( is_email( $spammer ) ) {
    			$emailparts = explode( '@', $spammer );
    			$username   = $emailparts[0];
    			$badlist[]  = $username;
    		}
    	}
    
    	// Check if the comment author name matches an email we've banned
    	// You'd think we didn't have to do this but ...
    	if ( in_array( $form['comment_author'], $badlist ) ) {
    		return true;
    	}
    
    	// Check if the email username is one of the bad ones
    	// This will allow spammer@example.com AND spammer+foobar@example.com to get caught
    	foreach ( $badlist as $bad_person ) {
    		if ( preg_match( '/' . $bad_person . '/', $form['comment_author_email'] ) ) {
    			return true;
    		}
    	}
    
    	return false;
    }
    

    My original take was hardcoded in, but this way is more elegant and covers the majority of the ways ‘nice’ people try to get around blocks. Now, if you’ve blocked spammer@example.com and someone submits a form with spammer+avoid@example.com this will catch them. It has a higher chance of catching ‘innocents’ (like innocent@spammer.com) however considering I’m looking for something like rosbeitam@example.com I’m reasonably confident in this for my personal application.

    The Take Away

    If you make a contact form, you damn well better make a way for users to block people from the back end, without having to code it.

    Merry Christmas, ya filthy animals.

  • ES5, ESNext, and a Headache

    ES5, ESNext, and a Headache

    You may have noticed, reading the Gutenberg documentation, that there are two ways to add new blocks. There’s ES5 and ESNext. The two code bases are similar, but they certainly can confuse new developers. And while it’s possible to migrate from one to the other, that too can be a bit of a headache.

    Consistently naming versions? HAH!

    Providing a consistent naming pattern is important for people to be able to understand what version of a software they’re using, and what’s next. Whatever you pick, when you decide how to go. you’re pretty much stuck with it forever. There are exceptions, but even Apple and Microsoft had semi-logical explanations for their names. I can’t really justify Windows ME to anyone, though.

    In Open Source land, people love to complain that WordPress itself doesn’t use semantic versioning (aka SemVer). That is, a jump from 4.2 to 4.3 is a major release, where as if it were SemVer, that would be a minor change.

    SemVer uses the concept of MAJOR.MINOR.PATCH with regards to numbers, which means you increment:

    1. MAJOR – when you make incompatible API changes,
    2. MINOR – when you add functionality in a backwards-compatible manner
    3. PATCH – when you make backwards-compatible bug fixes

    WordPress does MAJOR.MAJOR.MINOR-OR-PATCH which really confuses a lot of people, and I understand that.

    Of course, then you look at the history of JavaScript and you cry a little.

    JavaScript isn’t Java, and neither are coffee.

    We have to go back a while here. In 1995, Netscape Navigator was releasing a new coding language called LiveScript. In 1996 they renamed it to JavaScript, presumably to capitalize on the whole ‘Java’ craze. I actually took some Java classes back in those days. Anyway, Netscape tossed the deal over to ECMA International for some standardization, and got us ECMAScript.

    ECMAScript is the language, Javascript is the most popular implementation of the language. Its like HTML and XHTML, and when you get down to brass tacks, most people don’t care. They use the terms interchangeably. And that’s okay.

    From 1996 to around 2010, nothing changed. Javascript trucked along doing what it did, and ECMAScript didn’t change much at all. There was ECMAScript2 and ECMAScript3, but after that, we had a decade of nothing. The astute reader now has gone “ECMAScript… ECMA Script. ES?” And they would be correct.

    What is ESNext?

    ECMAScript 5 (aka ES5) came out in 2009, but really it didn’t get picked up until 2012. This is because of our old nemesis, Internet Explorer. In the last 6 years, developers have pretty much stuck to ES5, since it works in all modern browsers. We have ES6, also known as ES2015, but not every browser supports it yet. Which is why we have ESNext.

    To put it simply, ESNext (or ES.Next) is the future version of ECMAScript which is yet to be released. If you hear ES7 or ES2016, that’s actually the same thing. The naming system is a little janky and confusing, if you hadn’t already noticed.

    And this is why you’ll hear it called ESNext in WordPress. It encompasses ES6/ES2015, ES7/ES2016, and whatever comes next. Aaaaaah you see? 

    ES4 was abandoned by the way.

    It’s Not Dangerous.

    While not all browsers support ESNext, there’s good news for WordPress. It doesn’t matter because we transpile (I’ll get there in a second). For WordPress and Gutenberg, the primary difference is going to be in the ‘style’ of code and the build process. The break down is as follows:

    • ES5 is more obscure to write, but runs immediately
    • ESNext is more clean to write, but requires post compilation (transpilation) to run Gutenberg

    That probably didn’t help. Okay, how about this. There a way to write CSS (called SASS) which lets you add programatic features to your CSS. ES5 and ESNext are the same way. You totally can write ESNext without compiling, but not yet for Gutenberg. This is kind of the same thing, except when we transpile ESNext, we’re converting it to ES5.

    Now, we use ES5 in the end for a couple reasons, but primarily it’s because Gutenberg uses React, and React JSX (which translates Javascript to XHTML). That requires us to transpile back to ES5 in order to be used by all browsers. For now.

    Browsers update a lot faster than they did when we were trying to get rid of IE5. Still, Internet Explorer is around and will be for a while at financial institutions, so don’t get super excited yet.

    Where does this leave us?

    When you go to write your first Gutenblocks, I recommend ES5 for the simple ones and ESNext for the complex ones. ESNext is more semantic, in that it’s laid out in a more human readable way. ES5 is faster to edit and test.