Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • Structured Data

    Structured Data

    Every month around the 15th and 30th, I check Google Webmaster to see if there are any 'errors' I need to address. Most of the time this is 404s on pages that were wrong for 30 minutes, and Google decided to crawl me right then. In January, I got a bunch of new alerts about 'structured data.'

    What is it?

    Google uses structured data that it finds on the web to understand the content of the page, as well as to gather information about the web and the world in general. Which is a fancy way of saying Google looks for specific classes and attributes in your page to determine what's what. 

    Most of the time, your theme handles this for you. It's why you see hentry and authorcard in your page source. This is part of telling Google what the content is, contextually.

    Bad Errors

    Sadly, Google's report just said I had "missing fn" errors and gave me this:

    itemtype: http://microformats.org/profile/hcard
    photo: https://secure.gravatar.com/avatar/CODE?s=118&d=retro&r=g

    That's not very helpful unless you already know what you're looking for. And even then… There was a link to test live data and I clicked on it, only to get a report back that I had no errors. Frustrating, right?

    Then I checked a different error:

    Missing: author
    Missing: entry-title
    Missing: updated

    That was a little better, I thought, and I found out that since we'd removed author and updated from those posts, for a valid reason mind you, that was showing an error. The 'easy' fix was to make this function:

    function microformats_fix( $post_id ) {
    	$valid_types = array( 'post_type_authors', 'post_type_characters', 'post_type_shows' );
    	if ( in_array( get_post_type( $post_id ), $valid_types ) ) {
    		echo '<div class="hatom-extra" style="display:none;visibility:hidden;">
    			<span class="entry-title">' . get_the_title( $post_id ) . '</span>
    			<span class="updated">' . get_the_modified_time( 'F jS, Y', $post_id ) . '</span>
    			<span class="author vcard"><span class="fn">' . get_option( 'blogname' ) . '</span></span>
    		</div>';
    	}
    }

    And then I just tossed it into my post content.

    Does This Matter?

    Kind of. It doesn't appear to actively hurt your SEO but it can help a little. In my case, I'm not actually using 'Author' so it's an inconvenience. And I don't want to make public when a page was last updated, since it's meant to be static content, but also it gets 'saved' a lot more than it gets updated, due to how I process some data. Basically it would lie to users.

    But. Apparently I get to lie to Google.

    Yaaay.

  • Customizing Jetpack Feedback

    Customizing Jetpack Feedback

    Fair bit of warning, this is a big code heavy post. I use Jetpack to handle a variety of things on my sites. Contact forms, stats, embeds, monitoring, backups, a bit of security (brute force prevention), and in some cases Photon and galleries. Oh and I especially use it for the tweeting and facebookieing of posts. Love it or hate it, it has it's uses and as a user, I find it easier to work with than its alternatives. However! Jetpack is not perfect. And my current drama llama was that I wanted to do two things:
    1. Show on my dashboard how many messages I had
    2. Mark a feedback message as 'answered' without deleting
    The second item was very important as on a shared-management type site, it's hard to know who did what and did everyone handle an email or what have you? Really a better tool would be something that you could reply to from within the admin dashboard, versus emailing all over, but that's another day. Instead, I decided to tackle a custom post status.

    This is NOT Fully Supported

    This is my caveat. My big warning. WordPress doesn't yet fully support Custom Post Status. That is, yes you can totally register them, but there's no easy way to put in the interface, as the trac ticket has been around since 2010 (you read that right) and it's still not done. All that said, if you're willing to wrangle a bit of Javascript and sanitize your outputs properly, you can do this.

    Big Block of Code

    <?php
    /*
    Description: Jetpack Customizations
    Version: 1.0
    */
    
    if ( ! defined('WPINC' ) ) die;
    
    /**
     * Customize_Jetpack_Feedback class.
     * Functions used by Jetpack to cutomize Feedback
     */
    class Customize_Jetpack_Feedback {
    
    	/**
    	 * Constructor
    	 * @since 1.0
    	 */
    	public function __construct() {
    		add_action( 'dashboard_glance_items', array( $this, 'dashboard_glance' ) );
    		add_action( 'admin_head', array( $this, 'dashboard_glance_css' ) );
    		
    		add_action( 'init', array( $this, 'custom_post_statuses' ), 0 );
    		add_filter( 'post_row_actions', array( $this, 'add_posts_rows' ), 10, 2);
    		add_action( 'plugins_loaded', array( $this, 'mark_as_answered' ) );
    		add_filter( 'display_post_states', array( $this, 'display_post_states' ) );
    		add_action( 'admin_footer-post.php', array( $this, 'add_archived_to_post_status_list' ) );
    		add_action( 'admin_footer-edit.php', array( $this, 'add_archived_to_bulk_edit' ) );
    	}
    
    	/**
    	 * Add custom post status for Answered
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	public function custom_post_statuses() {
    		register_post_status( 'answered', array(
    			'label'                     => 'Answered',
    			'public'                    => false,
    			'exclude_from_search'       => true,
    			'show_in_admin_all_list'    => true,
    			'show_in_admin_status_list' => true,
    			'label_count'               => _n_noop( 'Answered <span class="count">(%s)</span>', 'Answered <span class="count">(%s)</span>' ),
    		) );
    	}
    
    	/**
    	 * Add URL for replying to feedback.
    	 * 
    	 * @access public
    	 * @param mixed $actions
    	 * @param mixed $post
    	 * @return void
    	 * @since 1.0
    	 */
    	public function add_posts_rows( $actions, $post ) {
    		// Only for Feedback
    		if ( $post->post_type == 'feedback' ) {
    			$url = add_query_arg( 'answered_post_status-post_id', $post->ID );
    			$url = add_query_arg( 'answered_post_status-nonce', wp_create_nonce( 'answered_post_status-post_id' . $post->ID ), $url );
    	
    			// Edit URLs based on status
    			if ( $post->post_status !== 'answered' ) {
    				$url = add_query_arg( 'answered_post_status-status', 'answered', $url );
    				$actions['answered_link']  = '<a href="' . $url . '" title="Mark This Post as Answered">Answered</a>';
    			} elseif ( $post->post_status == 'answered' ){
    				$url = add_query_arg( 'answered_post_status-status', 'publish', $url );
    				$actions['answered']  = '<a class="untrash" href="' . $url . '" title="Mark This Post as Unanswered">Unanswered</a>';
    				unset( $actions['edit'] );
    				unset( $actions['trash'] );
    			}
    		}
    		return $actions;
    	}
    
    	/**
    	 * Add Answered to post statues
    	 * 
    	 * @access public
    	 * @param mixed $states
    	 * @return void
    	 * @since 1.0
    	 */
    	function display_post_states( $states ) {
    		global $post;
    
    		if ( $post->post_type == 'feedback' ) {
    			$arg = get_query_var( 'post_status' );
    			if( $arg != 'answered' ){
    				if( $post->post_status == 'answered' ){
    					return array( 'Answered' );
    				}
    			}
    		}
    
    		return $states;
    	}
    
    	/**
    	 * Process marking as answered
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	public function mark_as_answered() {
    
    		// If contact forms aren't active, we'll just pass
    		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    
    			// Check Nonce
    			if ( isset( $_GET['answered_post_status-nonce'] ) && wp_verify_nonce( $_GET['answered_post_status-nonce'], 'answered_post_status-post_id' . $_GET['answered_post_status-post_id'] ) ) { 
    				// Check Current user Can and then process
    				if( current_user_can('publish_posts') && isset( $_GET['answered_post_status-status'] ) ) {
    					$GLOBALS[ 'wp_rewrite' ] = new wp_rewrite;
    		
    					$status  = sanitized_text_field( $_GET['answered_post_status-status'] );
    					$post_id = (int) $_GET['answered_post_status-post_id'];
    		
    					// If it's not a valid status, we have a problem
    					if ( !in_array( $status, array( 'answered', 'publish' ) ) ) die( 'ERROR!!!' );
    		
    					$answered = array( 'ID' => $post_id, 'post_status' => $status );
    					wp_update_post( $answered );
    				}
    			}
    
    		}
    	}
    
    
    	/**
    	 * add_archived_to_post_status_list function.
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	function add_archived_to_post_status_list(){
    		global $post;
    		$complete = $label = '';
    
    		// Bail if not feedback
    		if ( $post->post_type !== 'feedback' ) return;
    
    		if( $post->post_status == 'answered' ) {
    			echo '
    				<script>
    					jQuery(document).ready(function($){
    						$("#post-status-display" ).text("Answered");
    						$("select#post_status").append("<option value=\"answered\" selected=\"selected\">Answered</option>");
    						$(".misc-pub-post-status label").append("<span id=\"post-status-display\">Answered</span>");
    					});
    				</script>
    			';
    		} elseif ( $post->post_status == 'publish' ){
    			echo '
    				<script>
    					jQuery(document).ready(function($){
    						$("select#post_status").append("<option value=\"answered\" >Answered</option>");
    					});
    				</script>
    			';
    		}
    	} 
    
    	public function add_archived_to_bulk_edit() {
    		global $post;
    		if ( $post->post_type !== 'feedback' ) return;	
    		?>
    			<script>
    			jQuery(document).ready(function($){
    				$(".inline-edit-status select ").append("<option value=\"answered\">Answered</option>");
    				$(".bulkactions select ").append("<option value=\"answered\">Mark As Answered</option>");
    			});
    			</script>
    		<?php
    	}
    
    	/*
    	 * Show Feedback in "Right Now"
    	 *
    	 * @since 1.0
    	 */
    	public function dashboard_glance() {
    		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    			foreach ( array( 'feedback' ) as $post_type ) {
    				$num_posts = wp_count_posts( $post_type );
    				$count_posts = ( isset( $num_posts->publish ) )? $num_posts->publish : '0';
    				if ( $count_posts !== '0' ) {
    					if ( 'feedback' == $post_type ) {
    						$text = _n( '%s Message', '%s Messages', $count_posts );
    					}
    					$text = sprintf( $text, number_format_i18n( $count_posts ) );
    					printf( '<li class="%1$s-count"><a href="edit.php?post_type=%1$s">%2$s</a></li>', $post_type, $text );
    				}
    			}
    		}
    	}
    
    	/*
    	 * Custom Icon for Feedback in "Right Now"
    	 *
    	 * @since 1.0
    	 */
    	public function dashboard_glance_css() {
    	if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    		?>
    		<style type='text/css'>
    			#adminmenu #menu-posts-feedback div.wp-menu-image:before, #dashboard_right_now li.feedback-count a:before {
    				content: '\f466';
    				margin-left: -1px;
    			}
    		</style>
    		<?php
    		}
    	}
    
    }
    
    new Customize_Jetpack_Feedback();
    
    What this does is create a new status for your feedback of “Answered”. Ta Dah!
  • Restrict Site Access Filters

    Restrict Site Access Filters

    I have a demo site I use to for development. One of the things I want is to be able to lock the site to logged in users only and that I can do via Restricted Site Access by 10up.

    One of the things the plugin also allows is to open up access to an IP, so someone who doesn't have an account can check the site before you go live. The problem with this feature is caching.

    Caching Restricted Pages

    It doesn't really matter what kind of caching system you use, the point is all the same. People who aren't logged in should get a cached version of the content. People who are logged in, or whom you've determined need a unique experience, don't get cached content. That's the barebones of caching.

    The problem I ran into with restricted site access is that if I whitelisted an IP range, and someone from that range visited the site, they generated a page which my cache system … cached. That meant the next person got to see the cached content.

    Worf from Star Trek face-palming

    Now this may not actually be a problem in all cache systems, but I happened to be using Varnish, which is fairly straightforward about how it works. And, sadly, the plugin I'm using doesn't have a way around this. Yet.

    Filters and Hooks

    Like any enterprising plugin hoyden, I popped open the code and determined I needed to address the issue here:

    // check if the masked versions match
    if ( ( inet_pton( $ip ) & $mask ) == ( $remote_ip & $mask ) ) {
    	return;
    }

    This section of code is checking "If the IP matches the IP we have on our list, stop processing the block. It's okay to show them the content." What I needed was to add something just above the return to tell it "And if it's Varnish, don't cache!"

    At first my idea was to just toss a session_start() in there, which does work. For me. Adam Silverstein was leery of that having unintended consequences for others, and wouldn't it be better to make it hookable? After all, then any caching plugin could hook in! He was right, so I changed my pull request to this:

    do_action( 'restrict_site_access_ip_match', $remote_ip, $ip, $mask ); // allow users to hook ip match

    The next version of the release will have that code.

    In The Field

    Now, assuming you've slipped that code into your plugin, how do you actually use it?

    Since I need to have this only on my 'dev' site, and I'm incredibly lazy efficient, I decided to put this code into the MU plugins I use for the site:

    if ( DB_HOST == 'mysql.mydevsite.dream.press' ) {
    	add_action( 'restrict_site_access_ip_match', 'mydevsite_restrict_site_access_ip_match' );
    }
    
    function mydevsite_restrict_site_access_ip_match() {
    	session_start();
    }

    This is not the only way to do it. I also happen to have a define of define( 'MYSITE_DEV', true ); in my wp-config.php file, so I could have checked if that was true:

    if ( defined( 'MYSITE_DEV' ) && MYSITE_DEV ) { ... }

    Now, you'll notice I'm using sessions, even after Adam and I determined this could be bad for some people. It can. And in my case, in this specific situation, it's not dangerous. It's a quick and dirty way to tell Varnish not to cache (because PHP sessions indicate a unique experience is needed).

    The downside is that not caching means there's more load on my server for the non-logged in user who is legit supposed to be visiting the site. Since this is a development site, I'm okay with that. I would never run this in production on a live site. 

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

  • Still Not Using Plugins for Security

    Still Not Using Plugins for Security

    Seven years, and my answer to 'do I need a security plugin' is the same.

    Nope.

    What is a Security Plugin?

    A security plugin is not a plugin like 'brute force protect' or 'limit login attempts.'

    A security plugin is like Better WP Security or WordFence or a hundred other plugins that promise to scan your site and let you know what's changed.

    This is not to say that first set of plugins aren't there to make you 'safer,' it's that those are single use, targeted plugins that address a single issue. Limiting login attempts prevents someone from trying the same attack over and over and over until they get in.

    By contrast, all-in-one security plugins try to do everything. They scan your code, your data, and your site. They look for all the possible attack vectors and they try to plugin them.

    What Makes That Secure?

    That's the question I ask people. If a plugin adds in 2-factor authentication, I ask them what it does for them? Password expirations, captchas, file compares etc. Those are all good things, individually, but are they applicable for all people? What, specifically, about those things makes you more secure or not?

    Now. Before you get all shirty with me, I am well aware of what all of those things are good for. With the exception of captchas (which are not accessibility friendly, please stop using them), all of those things make a lot of sense. You expire passwords and, one hopes, require strong passwords to make it harder to break in. But if you have a 2FA setup, do you need to require rotating passwords?

    It’s All About Thinking

    Security plugins stop people from thinking about what's going on.

    I've seen it time and again, people install a plugin that 'makes them safe,' follow the bare minimum of requirements, and then install whatever they want without thinking about it, leave registrations open, and oops, get hacked.

    This is not to say that security plugins don't prevent some of that from happening, but they're often an 'after the fact' solution. That is, usually a security plugin doesn't know to block X until X has been exploited. That's kind of the nature of the beast, though, and why WordPress and many other CMS developers don't release full details on security fixes until they've been out there for a while. They want to give people a chance to upgrade before saying "Hey, y'all who didn't are super vulnerable."

    It’s Also About Speed

    Security plugins also have a tendency to make your site slower. This usually comes up when people have turned on everything that comes with a security plugin. Which goes right back to my point about thinking. The user doesn't think, because they're not yet educated, about the impact of the code on their site.

    To put it simply, the more things you ask WordPress to do before it can load a page, the slower it will be to load a page.

    Pretty cut and dried, right?

    What’s My Answer?

    I don't call this the 'right' answer or even the best one. Not everyone has access to my resources after all, so it's not fair to say "Hire Mika to think for you!" But to me, the best answer is to use the resources you have intelligently.

    Firewalls, from a server side, are all but a requirement to me. If your web host doesn't have one, and most at least have ModSecurity, get a new web host. If you disabled it on your site because a random plugin doesn't work with it, delete the plugin and turn it back on. If you can't move to a new host, look into firewalls like Incapsula or Sucuri. Put something between users and data.

    Site Scanning is a great tool, but don't run it on WordPress. A great example of smart security scanning is VaultPress. It's a remote service that has a copy of all your files and it scans the copy, not your site, for issues. There are other services you can use that scan your site without affecting traffic. Again, web hosts often have tools for this.

    Be Aware of what's going on. Don't just let security be a black box. Make sure you know what kinds of attacks are common on your site. If you're hit by a DDoS, for example, where they're just hammering your site to take it down, a 2FA plugin will not help. If they're trying to log in all the time, a scanner is probably not what you need.

    Lock It Down. If you don't need it, don't use it. If you don't need it on, turn it off. Update regularly. Don't install everything under the sun. 

    Don’t Buy Into FUD

    This is a tricky balance. On the one hand, I want to say 'don't panic if your favourite security plugin of choice tells you everything doomed!' Remember, they're trying to sell you things. But on the other … don't think everything's fine and dandy.

    It's not a simple solution. You have to simultaneously be aware of problems and not overwhelmed by them. You have to learn how to care about which ones are important to you and which are not.

    In a word, you have to think.

    And there is no plugin on the planet that can think for you.

  • Thimking About Security

    Thimking About Security

    It's been a while since I last talked about security and WordPress plugins, so I thought it was a good time to do it again.

    still don't use any. But we'll get to that in a minute.

    Don’t Be ‘Stupid’

    My mother is one of the few people I know who has almost completely conquered the will to be stupid.

    Miles Vorkosigan on his mother, Cordelia Naismith Vorkosigan
    Brothers in Arms by Lois McMaster Bujold

    Understanding what makes something secure or insecure is not as obvious as I wish it was. I often say that the trick to being secure is not being stupid. Of course that's easier said than done, and I know it.

    Still, my record holds true that the one time I was hacked, it was from my own stupidity. I knew it was wrong and foolish and I did it anyway. And my guiding principal of security remains a constant reminder "Don't be stupid."

    But what is stupid? Every time you leave your house, you lock your doors, right? You do the idiot walk, as my grandmother Taffy called it. Keys, wallet, phone? Is the gas off? Is the heat on? Are the windows closed? You check the normal things and then you lock the door and off you go.

    Of course, we all have been an hour into an 8 hour drive and panicked "Did I close the garage!?" And for some people, even the simple act of locking the door is an arduous journey of 10 or 30 or 55 checks. In order to say 'don't be stupid' we have take ourselves honestly and seriously, and remember that 'stupid' just means 'don't not think.'

    THIMK

    That was not a typo. Nor was the title of this post.

    While we all make fun of IBM and MAD Magazine, I recall reading "Welcome to the Monkey House" by Kurt Vonnegut, and Ma Kennedy had the sign over her desk. At the time, I was unaware of the MAD magazine spoof on the matter. THINK was a sign folks at IBM had, and THIMK was the spoof.

    When I read it in Vonnegut, and bear in mind I was young and naive, I found it far more compelling than the idea of telling someone to THINK. With the letter changed, it forced me to reassess my assumptions of what the meaning was. After all, telling someone to THINK means, well, think. But telling someone to THIMK is a different matter.

    Eating the Elephant

    You know that old joke? How do you eat an elephant? One bite at a time. Well. That's security.

    I've been a loud opponent of the TSA, the way it's implemented in the US currently. It makes us feel better by making us think (N) that something is being done. And, yes, the TSA has found problems. But their job is to look through a thousand small things and find the odd-one-out. They're looking for the weird.

    When we perform a security audit over anything, be it a plugin or a server, or a door, we look for what we know is likely wrong. When I review a plugin, I look for the common issues. I skim for them, or grep for them, because I know what I'm looking for, and my eyes are trained to find it.

    But then, once I see the major and common issues aren't there, I read the whole thing. I look at the plugin as a whole entity, and I think. What does the code mean? What is it's intent?

    Metaphysical Security

    Without the ability to spy into the soul of the developer and glean an understanding of their raisons d'être, we're left with monitoring actions and making best guesses. And we're going to be wrong from time to time.

    It's no secret that last year, the WordPress security world found a new villain in the despicable people who buy plugins and slip backdoors into them. I saw some complaints that this sort of vulnerability wouldn't exist in [insert your CMS here], except … it will. It can and it will.

    We are all vulnerable because we choose to trust. We trust the developer to have good intentions. We trust the reviewers to be good people and care more about the security and sanity of code than themselves (which is a whole different ball of fish). We trust the ongoing development not to be handed over to evil people.

    That last one is unavoidable. People trust me to review code and react in the 'best' way for the community. But what if someone found my asking price and bribed me? What if I let bad code like backdoors into the WordPress Plugin directory? It would probably get caught, eventually, but still. Even if we locked down plugins to specific users accounts and didn't let anyone but admins (like me and Otto) add users, we would still at the end of the day remain vulnerable to humanity.

    Security Is Ongoing

    The truth is this.

    We are always, every day, insecure and vulnerable.

    Having a website that is your 'life' or career or business or even just a passion-project is dangerous.

    You should treat your website with as much thought and security as you do your own home. Check the gas. Check the lights. Make sure the door is locked. Get a security system. Hire someone to review the site and the server. But take it seriously. 

    Your website is 'you' on the Internet. And it deserves as much care as locking your car and not parking it in a shady part of time.

    Summary?

    Pay attention to what you put on your website.

    Trust no one. Not even me.