Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: wordpress

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

  • WordPress Multisite: Block Site

    WordPress Multisite: Block Site

    This came up when I was looking at WordPress.com, where one has the freedom to post anything within their ToS, and I saw someone’s moronic blog about how specific people were evil. Pick whatever you want, it doesn’t matter except assume it was something offensive to a minority.

    The Terms of Use says this:

    In particular, make sure that none of the prohibited items (like spam, viruses, or serious threats of violence) appear on your website.

    This was not a serious threat of violence, it was just ignorant, offensive, and stupid. I looked at the site and thought “What I want most in this moment is a big ass button to block this person from posting on my .com site, and to prevent them from ever being able to comment on any blog I own.”

    It doesn’t exist. (I will note I found BuddyBlock but I have no idea how well that would work, and it’s for BuddyPress only.)

    Part of the cool thing about WordPress Multisite is that you can run your own social network. With that power comes responsibility though. Users should be able to protect themselves while remaining on your network, allowing them to block other users they just don’t want to talk to.

    So why don’t we? Well effective blocking is hard. As I mentioned in my post about how (most) contact forms fail at this, the biggest issue is people can just fake who they are are try again. This is a little harder on a Multisite, where a legitimate email and account can be required to comment, but by default all members of a network can comment on any blog on the network. This means we’re opening ourselves up to the potential to more abuse.

    How would that big block work? There are a few approaches and I think the best route would be two fold.

    Blocking Users

    Everyone should have the ability to mute or block a user. As an end user, if I never want to see comments from John Smith again, I should be able to press ‘block.’ Then I would just see a note like [comment hidden] whenever I run into a comment from him on any blog on the network. On a non Multisite, I’d actually like to see that for any site that requires registration. Allow users to mute each other.

    As an admin, if I block John Smith, then his comments are immediately discarded. If you wanted to get fancy, then you’d hide his comment from everyone who isn’t him, so he thinks he’s still talking to people and just being ignored. A silence mode. Use some JS so an admin has to click to expand and see what’s going on, so if John Smith is escalating, he can be banned.

    That would be the other thing. Banning users from your sites on a Multisite should be totally possible. And on .com a way to report “User X keeps working around my blocks.” would help a lot.

    Also for admins, perhaps they should be able to see “X people have blocked this user” on the Dashboard. That said, I can see a massive possibility for abuse with that. If John Smith was an admin of his own blog and saw ’10 people blocked you…’ it could cause problems. It would be trivial to hide it from the user, so you could never know how many people blocked you, but I can think of a few fast workarounds. Easiest is to add a second admin account to my own blog on the network and check.

    Blocking Blogs

    This is mostly an issue on WordPress.com, since it’s one of the few places I know of that has a ‘reader’ that shows you blogs that you might be interested in. That’s how I found the offending blog, by the way. A friend runs a religious blog on .com and the one we both found appalling was a recommended blog to her. I’ve already talked to some people behind the scenes of .com about that and how the algorithm may need some turning. But even if she had stumbled on to it via a search, should she not be able to say “Ew! Block!”

    I would write it so that if someone clicked ‘block blog’ the following things happen:

    1. The owner of the blog is blocked from commenting on any blog I own
    2. The URL of the blog is placed on my blacklist
    3. Optionally, all admins of the blog are added to my blacklist

    Now I don’t have to see anything anymore.

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

  • Multisite And Theme Activation Checks

    Multisite And Theme Activation Checks

    Earlier this week I talked about how you can’t actually network activate all plugins. As it happens, I have a plugin for a specific theme, and it doesn’t like being Network Activated.

    The plugin is Genesis Simple Hooks and, logically, it only works if you have a Genesis theme installed and active.

    How It Works

    When the plugin is activated it properly runs the following activation hook:

    register_activation_hook( __FILE__, 'simplehooks_activation' );
    /**
     * This function runs on plugin activation. It checks to make sure Genesis
     * or a Genesis child theme is active. If not, it deactivates itself.
     *
     * @since 0.1.0
     */
    function simplehooks_activation() {
    
    	if ( ! defined( 'PARENT_THEME_VERSION' ) || ! version_compare( PARENT_THEME_VERSION, '2.1.0', '>=' ) )
    		simplehooks_deactivate( '2.1.0', '3.9.2' );
    
    }
    

    First this looks to see if there’s no parent theme or if the parent theme version is less than 2.1.0, and if either of those are the case (that is if there’s no parent theme, and the version is less than…) it calls simplehooks_deactivate.

    /**
     * Deactivate Simple Hooks.
     *
     * This function deactivates Simple Hooks.
     *
     * @since 1.8.0.2
     */
    function simplehooks_deactivate( $genesis_version = '2.1.0', $wp_version = '3.9.2' ) {
    
    	deactivate_plugins( plugin_basename( __FILE__ ) );
    	wp_die( sprintf( __( 'Sorry, you cannot run Simple Hooks without WordPress %s and <a href="%s">Genesis %s</a>, or greater.', 'genesis-simple-hooks' ), $wp_version, 'http://my.studiopress.com/?download_id=91046d629e74d525b3f2978e404e7ffa', $genesis_version ) );
    
    }
    

    And you know what? That sucks. It never checks for Genesis as the parent theme name, based on the assumption that only Genesis themes use the define of PARENT_THEME_VERSION which probably made sense a few years ago, but doesn’t anymore. And more importantly for this explanation, you can’t network activate it because unless you’re lucky enough to be running a Genesis child theme on the main site of your network, it won’t activate.

    Worse, it will throw errors on sites that aren’t.

    How I’d Fix It

    First I made a list of the action/function calls that require a Genesis Child Theme to be active in order to run properly: simplehooks_load_textdomain, simplehooks_init, simplehooks_execute_hooks

    Then I moved them all to their own section and wrapped them in a check:

    if ( simplehooks_can_run() ) {
    	add_action( 'plugins_loaded', 'simplehooks_load_textdomain' );
    	add_action( 'genesis_init', 'simplehooks_init', 20 );
    	add_action( 'genesis_init', 'simplehooks_execute_hooks', 20 );
    }
    

    What’s simplehooks_can_run? That’s a new function I’ve created to check for the requirements:

    /**
     * Can Simple Hooks Run
     *
     * This function checks if the requirements for Simple Hooks are met.
     *
     * @since 2.3.0
     */
    function simplehooks_can_run() {
    	
    	global $wp_version;
    	$my_theme = wp_get_theme();
    	$genesis_theme = wp_get_theme( 'genesis' );
    
    	if ( ( version_compare( $genesis_theme->get( 'Version' ), '2.1.0', '>=' ) && !is_null( $genesis_theme->get( 'Version' ) ) && !empty( $genesis_theme->get( 'Version' ) ) ) && ( $my_theme->get( 'Name' ) == 'Genesis' ||  $my_theme->get( 'Template' ) == 'genesis' ) ) {
    		return true;
    	}
    		
    	return false;
    	
    }
    

    What this checks is if the parent theme is 2.1.0 or higher and, if it is, if the theme or it’s parent is named Genesis. By having it use return I don’t have to check if it’s true or false, the if-check is smart enough for me not to do it explicitly.

    Finally I changed the if check in simplehooks_activation to look like this:

    	if ( !is_multisite() && !simplehooks_can_run() )
    

    What this isn’t doing is checking for WordPress 3.9.x anymore, as it’s 2016 and that was 2014 and I’m not worried about it. If I was, I’d toss && version_compare( $wp_version, '3.9.2', '>=' ) to the if-check in simplehooks_can_use to CYA.

    This also isn’t giving you an error on Multisite. That means if you network activate the plugin and, on Multisite, go to a site that does not have Genesis running, you don’t get an error. This is by design. There’s no point in telling a site-admin “Hey there’s this Genesis thing you can’t have because you’re not using it! NEENER!”

    I’m actually using this here on this site. If you’re interested at testing it out, grab it from my Github repo.

    Pull requests welcome.