Half-Elf on Tech

Thoughts From a Professional Lesbian

Category: How To

  • Mobile Ad Detection

    Mobile Ad Detection

    I screwed up not that long ago.

    I got an email from Google Adsense telling me that one of my sites was in violation because it was showing two ads on the same mobile screen, which is not allowed. Until I started using some of Googles whole page on mobile ads (an experiment), this was never an issue. Now it was. Now I had to fix it.

    Thankfully I knew the simpliest answer would be to detect if a page was mobile and not display the ads. Among other things, I know that I hate too many ads on mobile. So all I wanted was to use the Google page level ads – one ad for the mobile page, easily dismissible. Therefore it would be best if I hide all but two other ads. One isn’t really an ad as much as an affiliate box, and one Google responsive ad.

    For my mobile detector, I went with MobileDetect, which is a lightweight PHP class. I picked it because I was already using PHP to determine what ads showed based on shortcodes so it was a logical choice.

    Now the way my simple code works is you can use a WordPress shortcode like [showads name="google-responsive"] and that calls a file, passing a parameter for name into the file to generate the ad via a mess of switches and sanitation. Really you can go to http://example.com/myads.php?name=leaderboard and it would show you the ad.

    The bare bones of the code looks like this:

    <?php
    
    require_once 'Mobile_Detect.php';
    $detect = new Mobile_Detect;
    
    $thisad = trim(strip_tags($_GET["name"]));
    $mobileads = array('google-responsive', 'affiliate-ad');
    
    // If it's mobile AND it's not in the array, bail.
    if ( $detect->isMobile() && !in_array($thisad, $mobileads) ) {
    	return;
    }
    
    echo '<div class="jf-adboxes '.$thisad.'">';
    
    switch ($thisad) {
    	case "half banner":
    		echo "the ad";
    		break;
    	case "line-buttons":
    		echo "the ad";
    		break;
    	default:
    		echo "Why are you here?";
    }
    
    echo '</div>';
    

    The secret sauce is that check for two things:

    1. Is the ad not one I’ve authorized for mobile?
    2. Is this mobile?

    Only if both are false will the script continue to run. It’s simple but I like to have things stop as soon as possible to make loading faster. There’s no css trickery to hide things via mobile size detection. It’s as simple, and as close to a binary check as I can make it.

  • Blocking Together

    Blocking Together

    Out of GamerGate, the amazing Randi Harper created Block Together which allows you to block everyone associated with the nasty parts of the whole mess.

    To use it, it’s two steps.

    1. Sign up on Block Together
    2. Click on the link to @randi_ebooks’s list and press subscribe

    That’s it. Once you do that, you’ll automatically block the masses. But this goes far further than Gamer Gate. By making a blocklist of your own, you can manage the people who regularly harass, offend, or otherwise make your life on Twitter miserable.

    I’m a firm, devoted, supporter of freedom of speech. I also defend my right not to listen to someone I don’t want to hear. I don’t have to listen. Those blocklists can be incredibly useful to share with people, like your friends and people who face similar harassment, so you can protect yourself.

    Sharing your block list

    If you choose to share your block list with friends, Block Together will create an unlisted, unguessable URL to access your block list. You can share the URL by email or Direct Message if you want to keep it private among friends, or you can tweet the URL if you are okay sharing your block list publicly. You can always disable sharing from the Settings page. If you do so, the URL to access your blocks will be deleted forever. If you choose to share again in the future, you will create a new, different URL. If you choose to disable sharing, you need to separately remove any subscribers you no longer want, on the Subscriptions page.

    Many people find that they don’t want to share their block list because they find there are accounts on it they don’t remember blocking, or that aren’t particularly abusive. This is partly because Twitter, for a long time, did not offer Mute. So if you wanted to stop seeing a merely unfunny account that gets frequently retweeted, blocking used to be the only fix. Now Twitter offers Mute, so you can Mute those accounts instead. Block Together makes it easy to remove them from your shared block list with the ‘Unblock and Mute’ button on the My Blocks page.

    I don’t public share my list. I have shared it to a few people, but since I block rather than mute people, it’s very easy for people to take offense at me putting them on the list. To me, blocking someone means “I don’t chose to have conversations with you in this manner in Twitter.” There are companies of friends I’ve blocked because they follow me to Twitter after a conversation on the Plugin Repository team.

    Actually for me, most of my blocked people are either people who have violently responded to social activism tweets, people who tweet me thinking that’s a faster way to get their plugin approved/reviewed, people who are implicitly aggressive towards me without taking the time to learn the whole story, concern trolls, and passionate people who have gone overboard.

    Yes, I block people who ping me about their plugins. My Twitter account is not the right way to address those things. Neither is Facebook. People who cannot respect the fact that I’m not working 24/7 don’t deserve my attention. I block them, and all the begging in the world won’t change that. I block probably faster than most people would consider ‘fair’ but it’s my Twitter account, not theirs, and I have a way I wish to control access and information. I need a reason to block people, but I’m not required to explain that to everyone.

    But if I make that list public, people would probably use it as leverage to harass me more or treat me worse. They have in the past. It’s the double edged sword where I want to help my friends but I need to protect myself. For now, my list will be private to people whom I know well only, and who won’t take it as offensive.

  • Hiding Custom Taxonomy Parents

    Hiding Custom Taxonomy Parents

    I have a few custom taxonomies that I want to be shown as textboxes and in order to do that, the simplest way is in the custom taxonomy, you set them up as hierachical true. This makes them behave like categories. The problem is I really don’t want these things to be hierarchical. That is, I don’t want people adding in a parent/child relationship.

    The simplest way around this is to cheat with CSS:

    select#newtropes_parent {
        display: none;
    }
    .form-field.term-parent-wrap {
        display: none;
    }
    

    That hides the parent value. In order to have it show on the proper pages, I put it in a file called shows.css, in the same folder as my shows.php file that controls all the settings for the shows CPT (this includes the custom taxonomies used by shows) and wrapped it in this:

    add_action( 'admin_enqueue_scripts', 'shows_my_scripts', 10 );
    function shows_my_scripts( $hook ) {
    	global $current_screen;
    	wp_register_style( 'shows-styles', plugins_url('shows.css', __FILE__ ) );
    	if( 'post_type_shows' == $current_screen->post_type || 'tropes' == $current_screen->taxonomy ) {
    		wp_enqueue_style( 'shows-styles' );
    	}
    }
    

    Now you can’t see that there are parents. Perfect. Done.

  • Customizing Taxonomies as Dropdowns in Quick Edit

    Customizing Taxonomies as Dropdowns in Quick Edit

    When you go to quick edit for a post, you will automatically see your custom taxonomies as editable fields:

    Custom Taxonomies showing in quick edit!

    But there’s a problem … I don’t want these to be text fields nor do I want them to be checkboxes. When it’s just you running a site, there’s less to worry about with regards to these things. You know what you’re adding, and if you typo, you blame yourself. At the same time, WordPress only has two options for these sorts of things: tags (freeform text) and categories (checkboxes).

    Technically they’re settings for hierarchical, where false is text and true is checkboxes, and I’ve hidden the parent field (a topic for another post). But doing that makes them all check boxes, and I want to restrict people to one and only one checkbox. Except I don’t. The UX of having checkboxes vanish is pretty shitty, and it isn’t logical that you would only check one box.

    Let me explain. In this example, which is practical, I have a taxonomy for human sexuality. I chose taxonomies and not post-meta because that would allow me to sort much more easily on data by groups. Simply, I can grab a list of all people who are flagged as ‘pansexual’ with one function and not have to reinvent the wheel.

    That means I can use radio buttons or a dropdown. I feel a dropdown is a better, cleaner, UX, so that’s what I’m going to do. You can use the same logic here, though, so don’t worry.

    After reading ShibaShake’s documentation on adding in quick edit values I winced. Adding it with quick_edit_custom_box is super easy. The problem is that you have to use an incredibly weird amount of javascript to get the post ID and pass data back and forth. As weird and annoying as that was, it actually works. The example, of course, is comparing a CPT, where as I am using a custom taxonomy, so my own code was a little crazier.

    Remove the taxonomy from Quick Edit

    To do this starts out sounding like the stupidest idea ever, but you want to set show_in_quick_edit to false in your taxonomy. That does what it sounds like, removing the item from the quick edit page. Obviously now I have to add it back.

    It’s important to note that if you don’t have any custom columns in your post list view, the rest of this won’t work. I do, and since part two of all this will be about being able to edit those custom columns, I don’t have to worry. If you do, here’s how you add a fake column… Ready? You add and remove the column. I know, I know, WP can be weird:

    add_filter('manage_posts_columns', 'lezchars_add_fake_column', 10, 2);
    function lezchars_add_fake_column( $posts_columns, $post_type ) {
        $posts_columns['lez_fakeit'] = 'Fake Column (Invisible)';
        return $posts_columns;
    }
    add_filter('manage_edit-post_columns', 'lezchars_remove_fake_column');
    function remove_dummy_column( $posts_columns ) {
        unset($posts_columns['lez_fakeit']);
        return $posts_columns;
    }
    

    Like I said, I know it’s weird.

    Add the Quick Edit Box

    Since I know I’m going to be building out a few more of these, and one is a cross-relational CPT to CPT drama, I’ve broken mine out into a switch/case basis.

    // Add quick Edit boxes
    add_action('quick_edit_custom_box',  'lezchars_quick_edit_add', 10, 2);
    function lezchars_quick_edit_add($column_name, $post_type) {
    	switch ( $column_name ) {
    		case 'shows':
    			// Multiselect - CPT where characters may have multiple
    			break;
    		case 'roletype':
    			// Single Select - Custom Taxonomy
    			break;
    		case 'taxonomy-lez_sexuality':
    			?>
    			<fieldset class="inline-edit-col-left">
    			<div class="inline-edit-col">
    			<span class="title">Sexual Orientation</span>
    				<input type="hidden" name="lez_sexuality_noncename" id="lez_sexuality_noncename" value="" />
    				<?php 
    				$terms = get_terms( array( 'taxonomy' => 'lez_sexuality','hide_empty' => false ) );
    				?>
    				<select name='terms_lez_sexuality' id='terms_lez_sexuality'>
    					<option class='lez_sexuality-option' value='0'>(Undefined)</option>
    					<?php
    					foreach ($terms as $term) {
    						echo "<option class='lez_sexuality-option' value='{$term->name}'>{$term->name}</option>\n";
    					}
    						?>
    				</select>
    			</div>
    			</fieldset>
    			<?php
    		break;
    	}
    }
    

    This is all pretty basic stuff. I’m simply making a form for a drop-down based on the contents of my taxonomy. If you want to make it radio buttons, do that.

    Saving The Changes

    Now we’re getting a little weirder. We need to save our changes, but only if it’s not an auto-save, and if it’s the correct post type (post_type_characters for this page), and if the user can edit the page:

    add_action('save_post', 'lezchars_quick_edit_save');
    function lezchars_quick_edit_save($post_id) {
        // Criteria for not saving: Auto-saves, not post_type_characters, can't edit
        if ( ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) || ( 'post_type_characters' != $_POST['post_type'] ) || !current_user_can( 'edit_page', $post_id ) ) {
    		return $post_id;
    	}
    
    	$post = get_post($post_id);
    
    	// Lez Sexuality
    	if ( isset($_POST['terms_lez_sexuality']) && ($post->post_type != 'revision') ) {
    		$lez_sexuality_term = esc_attr($_POST['terms_lez_sexuality']);
    		$term = term_exists( $lez_sexuality_term, 'lez_sexuality');
    		if ( $term !== 0 && $term !== null) {
    			wp_set_object_terms( $post_id, $lez_sexuality_term, 'lez_sexuality' );
    		}
    	}
    }
    

    The two checks going on here are first to be sure it’s the right POST action to save on, and then to only update if the term already exists. If you wanted to append terms instead of replace, you could add a ‘true’ param to wp_set_object_terms, however I want only set one sexuality per person in this case.

    At this point, the code actually works!

    The new dropdown

    Changing the current selection for the dropdown

    There’s one problem though. If you’re working along with me this far, you’ll have noticed that the default selection is always ‘(Undefined)’ and that’s not what we want. The extension of this problem is we have to use bloody javascript to edit it. Damn it.

    // Javascript to change 'defaults'
    add_action('admin_footer', 'lezchars_quick_edit_js');
    function lezchars_quick_edit_js() {
    	global $current_screen;
    	if ( ($current_screen->id !== 'edit-post_type_characters') || ($current_screen->post_type !== 'post_type_characters') ) return;
    	?>
    	<script type="text/javascript">
    	<!--
    	function set_inline_lez_sexuality( widgetSet, nonce ) {
    		// revert Quick Edit menu so that it refreshes properly
    		inlineEditPost.revert();
    		var widgetInput = document.getElementById('terms_lez_sexuality');
    		var nonceInput = document.getElementById('lez_sexuality_noncename');
    		nonceInput.value = nonce;
    
    		// check option manually
    		for (i = 0; i < widgetInput.options.length; i++) {
    			if (widgetInput.options[i].value == widgetSet) {
    				widgetInput.options[i].setAttribute("selected", "selected");
    			} else { widgetInput.options[i].removeAttribute("selected"); }
    		}
    	}
    	//-->
    	</script>
    	<?php
    }
    
    // Calls the JS in the previous function
    add_filter('post_row_actions', 'lezchars_quick_edit_link', 10, 2);
    
    function lezchars_quick_edit_link($actions, $post) {
    	global $current_screen;
    	if (($current_screen->id != 'edit-post_type_characters') || ($current_screen->post_type != 'post_type_characters')) return $actions;
    
    	$lez_nonce = wp_create_nonce( 'lez_sexuality_'.$post->ID);
    	$lez_sex   = wp_get_post_terms( $post->ID, 'lez_sexuality', array( 'fields' => 'all' ) );
    
    	$actions['inline hide-if-no-js'] = '<a href="#" class="editinline" title="';
    	$actions['inline hide-if-no-js'] .= esc_attr( __( 'Edit this item inline' ) ) . '" ';
    	$actions['inline hide-if-no-js'] .= " onclick=\"set_inline_lez_sexuality('{$lez_sex[0]->name}', '{$lez_nonce}')\">";
    	$actions['inline hide-if-no-js'] .= __( 'Quick&nbsp;Edit' );
    	$actions['inline hide-if-no-js'] .= '</a>';
    	return $actions;
    }
    

    Please don’t ask me to explain that. And yes, I know we should be using Unobtrusive Javascript and hooking into the DOM instead, but I don’t yet know how to do that.

    I do know that if I wanted to add in multiple checks, one way is to duplicate the function set_inline_lez_sexuality( widgetSet, nonce ) and rename it to set_inline_lez_gender and then extend the actions like this:

    $actions['inline hide-if-no-js'] .= " onclick=\"set_inline_lez_sexuality('{$sex_terms[0]->name}', '{$sex_nonce}');set_inline_lez_gender('{$gender_terms[0]->name}', '{$gender_nonce}')\">";
    

    There are more combinations and concatenations one can do here. Knock yourself out. It’s enough javascript for me today!

  • Multisite Emails and Redirects

    Multisite Emails and Redirects

    I wrote a plugin that allows people to Join A Multisite on a Per-Site Basis.

    There are some things it doesn’t do that I have no intention of adding into the plugin, but people often ask me how to do them. Personally, I think people should always know they’re on a network, and hiding this will only lead to complaints later one, but my way is not the only way.

    That said. I am aware of things people try to do that my plugin won’t. All of these snippets should go in a file in mu-plugins. I’d name it multisite-registration.php personally. That way I know what it is right away.

    Emails By Site

    When you register for a network site, you always get emailed from the network. This means even if I go to halfelf.org to reset my password, the email always comes from ipstenu.org. To change the password reset emails to be from the one where you’ve actually pressed the reset link is pretty easy:

    add_filter( 'retrieve_password_message', function ($message, $key) {
      	return str_replace(get_site_url(1), get_site_url(), $message);
    }, 10, 2);
    
    add_filter( 'retrieve_password_title', function($title) {
    	return "[" . wp_specialchars_decode(get_option('blogname'), ENT_QUOTES) . "] Password Reset";
    });
    

    But when we talk about the activation it’s a little messier. If you’re using my plugin, you can have users sign up on a specific site. You probably want to have new user activations come from that site and if you do, you need to do this:

    add_filter( 'wpmu_signup_blog_notification_subject', function($subject) {
    	return "[" . wp_specialchars_decode(get_option('blogname'), ENT_QUOTES) . "] Activate Your Account";
    });
    
    add_filter( 'wpmu_signup_blog_notification_subject', function($subject) {
    	return "[" . wp_specialchars_decode(get_option('blogname'), ENT_QUOTES) . "] Activate Your Account";
    });
    

    That subject can, obviously, be changed.

    Redirect Lost Password Pages

    So you may have noticed that the lost password page on a network always points to the network and never the site you’re on.

    Screenshot of my login screen showing halfelf in the URL bar, but the reset link points to ipstenu.org

    That can actually be fixed by doing this:

    add_filter( 'lostpassword_url', function ($url, $redirect) {	
    	
    	$args = array( 'action' => 'lostpassword' );
    	
    	if ( !empty($redirect) )
    		$args['redirect_to'] = $redirect;
    	return add_query_arg( $args, site_url('wp-login.php') );
    }, 10, 2);
    
    add_filter( 'network_site_url', function($url, $path, $scheme) {
      
      	if (stripos($url, "action=lostpassword") !== false)
    		return site_url('wp-login.php?action=lostpassword', $scheme);
      
       	if (stripos($url, "action=resetpass") !== false)
    		return site_url('wp-login.php?action=resetpass', $scheme);
      
    	return $url;
    }, 10, 3 );
    

    This simply filters the URL and if you’re on a site’s login page, use that site for the URL.

    Redirect Logins to Their Site

    This one is messier. If you always want a user to be redirected to ‘their’ site, you have to know what their primary blog is on the network. You can do this, and for the most part, this works:

    add_filter('login_redirect', function ( $redirect_to, $request_redirect_to, $user ) {
        if ($user->ID != 0) {
            $user_info = get_userdata($user->ID);
            if ($user_info->primary_blog) {
                $primary_url = get_blogaddress_by_id($user_info->primary_blog) . 'wp-admin/';
                if ($primary_url) {
                    wp_redirect($primary_url);
                    die();
                }
            }
        }
        return $redirect_to;
    }, 100, 3);
    

    If they don’t have a primary_blog, they’ll be punted to the main for the network, as it should be.