Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: multisite

  • Per-Site MU Plugins

    Per-Site MU Plugins

    A great many moons ago, I handled my per-site MU plugins in a very straight forward way. I made a halfelf-functions.php file and checked for the blog ID with if ( $blog_id == 2 ) {...} and off I went.

    Now? I do this:

    global $blog_id;
    $helf_site_url = parse_url( get_site_url( $blog_id ) );
    $helf_file = plugin_dir_path( __FILE__ ) .'functions/'. $helf_site_url['host'] .'.php';
    if ( file_exists( $helf_file ) ) {
        include_once( $helf_file );
    }
    

    I have a folder called ‘functions’ and in there I have a file for every site that needs it’s own functions. This also let me clean up some rather old code I wasn’t using anymore, but it also let me add in code to include local CSS. Since I version control my mu-plugins folder, this allowed me to move my custom CSS from Jetpack to a normal CSS file.

    Why did I need to do that? Jetpack CSS doesn’t allow the fill param for CSS. Their current justification is that it’s advanced enough that people should be editing the theme. And mine is “But … why?” SVGs are becoming more and more popular, after all, and a number of plugins are allowing them. That means to style your SVGs, you’ll want to use CSS. And you can’t with Jetpack, which means you’re back to the old hell of editing your theme’s CSS. And that was something I wanted to avoid.

    You’d think I could do this:

    $helf_file_css = plugin_dir_path( __FILE__ ) .'functions/css/'. $helf_site_url['host'] .'.css';
    function helf_scripts() {
    	if ( file_exists( $helf_file_css ) ) {
    		wp_enqueue_style( $helf_site_url['host'], $helf_file_css );
    	}
    }
    add_action( 'wp_enqueue_scripts', 'helf_scripts' );
    

    But that actually didn’t work. No matter what I did, the result of $helf_site_url['host'] was empty. I know, right? What’s up with that.

    What’s up is me not thinking about how functions ‘know’ what they know.

    function helf_scripts() {
    	global $blog_id;
    	$url = parse_url( get_site_url( $blog_id ) );
    	$css_path = plugin_dir_path( __FILE__ ) .'functions/css/'. $url['host'] .'.css';
    	$css_url = plugins_url( 'functions/css/'. $url['host'] .'.css', __FILE__ );
    
    	if ( file_exists( $css_path ) ) {
    		wp_enqueue_style( $url['host'] , $css_url );
    	}
    }
    add_action( 'wp_enqueue_scripts', 'helf_scripts' );
    

    Inside the function, you see, it can’t see non-globals. So it wouldn’t work. If you’re not using Multisite, you don’t need to use $blog_id, but I don’t know why you’d want to use this if you weren’t using Multisite. The other silly moment was remembering that plugin_dir_path() would give me /home/user/public_html/wp-content/mu-plugins which would make the URL a relative URL, and not at all what I wanted. But using plugins_url() would give me an absolute path.

    Perfect.

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

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

  • WordPress Multisite Control

    WordPress Multisite Control

    When you write a plugin for WordPress Multisite, you have three options for how to let users control the plugin options. It comes down to the manipulation of the ways we have to activate a plugin on Multisite, which are per-site or network only.

    I’m a firm adherent of having the network control as much as it logically should, but allowing each site to pick unique features. Never should someone be shocked to find out they’re on a network. A network is, after all, a collection of WordPress sites. Now your collection may or may not be related, but at the end of the day, someone should never be surprised to find out the site they signed up for is on a network.

    With this in mind, I separated the ‘control’ of the plugins into three groups.

    Network Only

    A Network Only plugin is one that should be controlled via the Network Admin. While the Settings API is a terrible bag of wet hair for Multisite, if you have a network plugin, then it should be for the network. The plugins that have no interface at all should be network activated. This is really simple, but in general if you’re adding this feature to your network, you probably want it on for everyone. There are some rare exceptions, but in general, network only is the key.

    Most network only plugins are clever enough to use Network: true in their plugin headers, which makes this much easier. If you think your code should only be activated by the network, use that.

    Per-Site

    A per-site plugin is activated on each site, controlled from each site, and the network admins have no authority save uninstalling the plugin. These plugins are things that each site should decide how to use. When I look at my own sites, I have a few that are like this. Like @Reply Two – when you look at it, you’d think it should be network only, but since it requires some per-site configuration with regards to comments, it’s best left as optional for each site.

    There is no Network: false setting, I’m afraid.

    Network Only Activation with Per-Site Control

    Here’s where it gets sticky, and plugins like Jetpack actually handle this better than most others. Take, for example, something like a plugin that adds features to a specific theme. If that theme isn’t active, the plugin shouldn’t error out. But a lot of us code our plugins to say “If this other plugin or theme isn’t active, don’t activate.” That sounds like a great idea except when you want to have it network activated. In those cases, the checks get weird and don’t run as expected.

    And then you have to consider what should control what. I mentioned Jetpack because it has a network admin screen.

    Jetpack's Network Override

    There you can enforce connections from your network admin, or not, as you see fit.

    Which One Is Right?

    While I’ve postulated this is very simple, it’s not. For example, when you have Jetpack, do I want everyone to edit every setting or just some? I’d want them to have the ability to use the CSS editor per-site, but maybe not VaultPress or Stats. The checks for that code is not as logical as it should be. The whens for running those checks, the priorities and weight given to who is more important, is not obvious.

    I would say that the Network Admins should have final say. But many people don’t agree with me on that. Many people think each site on a network would be best to exist on it’s own and stand alone, a part of a secret.

    And that too deserves room for thought.

  • Mailbag: An Appropriate Solution

    Mailbag: An Appropriate Solution

    Y’all know I don’t really like to answer these questions. I mean. Presumably you’ve noticed I don’t answer this a lot anymore?

    I don’t care if you use Multisite. And I hate this question because you’re (innocently) asking me one of the most incredibly complicated questions possible.

    I’m working with a college who wishes to create a portfolio system for their students. Basically, a student can create their own website – or multiple – to share with employers and others. I’m thinking WP Multisite may be a good option. I could have super admin access, the college’s admin can have super admin access, and each student will have admin access to their individual sites. Would you agree that WP Multisite would be an appropriate solution?

    I agree it can be an appropriate solution.

    I will never agree it’s the solution.

    Read the post I linked to above, will you? The one that explains exactly why this is such a damned hard thing to answer.

    Now. Ask yourself this:

    1. Are those students the people who will be happy where they can’t install a plugin or a theme, or will they badger you endlessly to install them?
    2. Will those students ever want to easily export/move their sites?
    3. Will they ever need ‘more’ than WordPress and, thus, need shell or DB access?
    4. What do you want them to be able to do when they’re done?
    5. How much time do you have to fix their sites?
    6. Are you able to review and ensure security for all plugins and themes they may want?
    7. Do they already have webhosting? (Most universities give you some.)

    Figure that out and you’ll know if it’s the most appropriate solution for them and you.