How to adapt my plugin to Multisite?
Asked Answered
G

1

34

I have many plugins that I wrote for WordPress, and now I want to adapt them to MU.
What are the considerations / best practices / workflow / functions / pitfalls that I have to follow / avoid / adapt in order to 'upgrade' my plugins to support also Multisite installations?

For example, but not limited to:

  • Enqueue scripts/register
  • Incuding files (php, images)
  • Paths for custom files uploads
  • $wpdb
  • Activation, uninstall, deactivation
  • Handling of admin specific pages

In the Codex, there are sometimes remarks about Multisite in single function description, but I did not find any one-stop page that address this subject.

Guaco answered 19/12, 2012 at 20:19 Comment(0)
N
72

As for enqueuing and including, things go as normal. Plugin path and URL are the same.

I never dealt with anything related to upload paths in Multisite and I guess normally WP takes care of this.


$wpdb

There is a commonly used snippet to iterate through all blogs:

global $wpdb;
$blogs = $wpdb->get_results("
    SELECT blog_id
    FROM {$wpdb->blogs}
    WHERE site_id = '{$wpdb->siteid}'
    AND spam = '0'
    AND deleted = '0'
    AND archived = '0'
");
$original_blog_id = get_current_blog_id();   
foreach ( $blogs as $blog_id ) 
{
    switch_to_blog( $blog_id->blog_id );
    // do something in the blog, like:
    // update_option()
}   
switch_to_blog( $original_blog_id );

You may find examples where restore_current_blog() is used instead of switch_to_blog( $original_blog_id ). But here's why switch is more reliable: restore_current_blog() vs switch_to_blog().


$blog_id

Execute some function or hook according to the blog ID:

global $blog_id;
if( $blog_id != 3 )
    add_image_size( 'category-thumb', 300, 9999 ); //300 pixels wide (and unlimited height)

Or maybe:

if( 
    'child.multisite.com' === $_SERVER['SERVER_NAME'] 
    || 
    'domain-mapped-child.com' === $_SERVER['SERVER_NAME']
    )
{
    // do_something();
}

Install - Network Activation only

Using the plugin header Network: true (see: Sample Plugin) will only display the plugin in the page /wp-admin/network/plugins.php. With this header in place, we can use the following to block certain actions meant to happen if the plugin is Network only.

function my_plugin_block_something()
{
    $plugin = plugin_basename( __FILE__ );
    if( !is_network_only_plugin( $plugin ) )
        wp_die(
            'Sorry, this action is meant for Network only', 
            'Network only',  
            array( 
                'response' => 500, 
                'back_link' => true 
            )
        );    
}

Uninstall

For (De)Activation, it depends on each plugin. But, for Uninstalling, this is the code I use in the file uninstall.php:

<?php
/**
 * Uninstall plugin - Single and Multisite
 * Source: https://wordpress.stackexchange.com/q/80350/12615
 */

// Make sure that we are uninstalling
if ( !defined( 'WP_UNINSTALL_PLUGIN' ) ) 
    exit();

// Leave no trail
$option_name = 'HardCodedOptionName';

if ( !is_multisite() ) 
{
    delete_option( $option_name );
} 
else 
{
    global $wpdb;
    $blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
    $original_blog_id = get_current_blog_id();

    foreach ( $blog_ids as $blog_id ) 
    {
        switch_to_blog( $blog_id );
        delete_option( $option_name );    
    }
    switch_to_blog( $original_blog_id );
}

Admin Pages

1) Adding an admin page

To add an administration menu we check if is_multisite() and modify the hook accordingly:

$hook = is_multisite() ? 'network_' : '';
add_action( "{$hook}admin_menu", 'unique_prefix_function_callback' );

2) Check for Multisite dashboard and modify the admin URL:

// Check for MS dashboard
if( is_network_admin() )
    $url = network_admin_url( 'plugins.php' );
else
    $url = admin_url( 'plugins.php' );

3) Workaround to show interface elements only in main site

Without creating a Network Admin Menu (action hook network_admin_menu), it is possible to show some part of the plugin only in the main site.

I started to include some Multisite functionality in my biggest plugin and did the following to restrict one part of the plugin options to the main site. Meaning, if the plugin is activated in a sub site, the option won't show up.

$this->multisite = is_multisite() 
        ? ( is_super_admin() && is_main_site() ) // must meet this 2 conditions to be "our multisite"
        : false;

Looking at this again, maybe it can be simply: is_multisite() && is_super_admin() && is_main_site(). Note that the last two return true in single sites.

And then:

if( $this->multisite )
    echo "Something only for the main site, i.e.: Super Admin!";

4) Collection of useful hooks and functions.

Hooks: network_admin_menu, wpmu_new_blog, signup_blogform, wpmu_blogs_columns, manage_sites_custom_column, manage_blogs_custom_column, wp_dashboard_setup, network_admin_notices, site_option_active_sitewide_plugins, {$hook}admin_menu

Functions: is_multisite, is_super_admin is_main_site, get_blogs_of_user, update_blog_option, is_network_admin, network_admin_url, is_network_only_plugin

PS: I rather link to WordPress Answers than to the Codex, as there'll be more examples of working code.


Sample plugin

I've just rolled a Multisite plugin, Network Deactivated but Active Elsewhere, and made a non-working resumed annotated version bellow (see GitHub for the finished full working version). The finished plugin is purely functional, there's no settings interface.

Note that the plugin header has Network: true. It prevents the plugin from showing in child sites.

<?php
/**
 * Plugin Name: Network Deactivated but Active Elsewhere
 * Network: true
 */ 

/**
 * Start the plugin only if in Admin side and if site is Multisite
 */
if( is_admin() && is_multisite() )
{
    add_action(
        'plugins_loaded',
        array ( B5F_Blog_Active_Plugins_Multisite::get_instance(), 'plugin_setup' )
    );
}    

/**
 * Based on Plugin Class Demo - https://gist.github.com/toscho/3804204 
 */
class B5F_Blog_Active_Plugins_Multisite
{
    protected static $instance = NULL;
    public $blogs = array();
    public $plugin_url = '';
    public $plugin_path = '';

    public static function get_instance()
    {
        NULL === self::$instance and self::$instance = new self;
        return self::$instance;
    }

    /**
     * Plugin URL and Path work as normal
     */
    public function plugin_setup()
    {
        $this->plugin_url    = plugins_url( '/', __FILE__ );
        $this->plugin_path   = plugin_dir_path( __FILE__ );
        add_action( 
            'load-plugins.php', 
            array( $this, 'load_blogs' ) 
        );
    }

    public function __construct() {}

    public function load_blogs()
    { 
        /**
         * Using "is_network" property from $current_screen global variable.
         * Run only in /wp-admin/network/plugins.php
         */
        global $current_screen;
        if( !$current_screen->is_network )
            return;

        /**
         * A couple of Multisite-only filter hooks and a regular one.
         */
        add_action( 
                'network_admin_plugin_action_links', 
                array( $this, 'list_plugins' ), 
                10, 4 
        );
        add_filter( 
                'views_plugins-network', // 'views_{$current_screen->id}'
                array( $this, 'inactive_views' ), 
                10, 1 
        );
        add_action(
                'admin_print_scripts',
                array( $this, 'enqueue')
        );

        /**
         * This query is quite frequent to retrieve all blog IDs.
         */
        global $wpdb;
        $this->blogs = $wpdb->get_results(
                " SELECT blog_id, domain 
                FROM {$wpdb->blogs}
                WHERE site_id = '{$wpdb->siteid}'
                AND spam = '0'
                AND deleted = '0'
                AND archived = '0' "
        );  
    }

    /**
     * Enqueue script and style normally.
     */
    public function enqueue()
    {
        wp_enqueue_script( 
                'ndbae-js', 
                $this->plugin_url . '/ndbae.js', 
                array(), 
                false, 
                true 
        );
        wp_enqueue_style( 
                'ndbae-css', 
                $this->plugin_url . '/ndbae.css'
        );
    }

    /**
     * Check if plugin is active in any blog
     * Using Multisite function get_blog_option
     */
    private function get_network_plugins_active( $plug )
    {
        $active_in_blogs = array();
        foreach( $this->blogs as $blog )
        {
            $the_plugs = get_blog_option( $blog['blog_id'], 'active_plugins' );
            foreach( $the_plugs as $value )
            {
                if( $value == $plug )
                    $active_in_blogs[] = $blog['domain'];
            }
        }
        return $active_in_blogs;
    }
}

Other resources - e-books

Not directly related to plugin development, but kind of essential to Multisite management.
The e-books are written by no less than two giants of Multisite: Mika Epstein (aka Ipstenu) and Andrea Rennick.

N answered 11/4, 2013 at 3:42 Comment(7)
Some Great Great Info here ..I would give you not +1 but +100 if i could.. Thanks . all useful. as wp-mu is a bit of a "bastard" child, no one really knows how to deal with it well (just see the netword options admin UI) . Anyhow, one question , about the Network: true in the docblock , is it possible to somehow trigger it also for third-party plugins ? (where it is not in the original docblock ? )Guaco
god.. too many info to process :-) . and also I somehow never noticed that featrue . anyhow, thanks again. and if you come up with more stuff, it would be great if you will add it to this answer ..Guaco
Your first example of iterating through all blogs uses switch_to_blog($original_blog_id) after the loop instead of restore_current_blog() at the end of each iteration. You cite restore_current_blog() vs switch_to_blog(), but the example code does not include unset ( $GLOBALS['_wp_switched_stack'] ); $GLOBALS['switched'] = false; which are necessary in order to avoid unwanted behavior, according to the cited article.Contract
Amazing synopsis. Can we get this added to the WordPress codex somewhere?Beberg
@MikeNGarrett, done ;) codex.wordpress.org/Create_A_Network#Related_ArticlesN
@N I don't see anything like this at your link. This is probably the best synopsis I've been able to find for this anywhere.Ptarmigan
In the snippet to iterate through all blogs, the use of restore_current_blog() is the recommended way to return to the original blog, by WordPress developers, according to: codex.wordpress.org/Function_Reference/switch_to_blog. If you are tempted to use switch_to_blog( $original_blog_id ), because it is faster (and not more reliable, as stated), you MUST clear afterward the global variables which monitors the switching, as warned by WordPress developers here: codex.wordpress.org/Function_Reference/….Xylotomy

© 2022 - 2024 — McMap. All rights reserved.