<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>multisite Archives - Alexandros Georgiou</title>
	<atom:link href="https://www.alexgeorgiou.gr/tag/multisite/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.alexgeorgiou.gr/tag/multisite/</link>
	<description>Balancing brackets for a living</description>
	<lastBuildDate>Mon, 22 Apr 2024 09:03:11 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.alexgeorgiou.gr/wp-content/uploads/2021/07/cropped-alexgeorgiou-icon-32x32.png</url>
	<title>multisite Archives - Alexandros Georgiou</title>
	<link>https://www.alexgeorgiou.gr/tag/multisite/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>🖧 The wacky world of network activated WordPress plugins in multisite</title>
		<link>https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/</link>
					<comments>https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Mon, 19 Jun 2017 17:46:25 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[documentation]]></category>
		<category><![CDATA[multisite]]></category>
		<category><![CDATA[multiuser]]></category>
		<category><![CDATA[network activation]]></category>
		<category><![CDATA[site-wide]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[wordpress]]></category>
		<category><![CDATA[wp-cli]]></category>
		<category><![CDATA[wpmu]]></category>
		<guid isPermaLink="false">http://www.alexgeorgiou.gr/?p=224</guid>

					<description><![CDATA[<p>Insider secrets to making your plugin work well on multisite installs, both when it is network activated and when it is not.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/">🖧 The wacky world of network activated WordPress plugins in multisite</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Multisite</em> is arguably one of the weirdest features in <em>WordPress</em>. Making your plugin work well on multisite installs, both when it is network activated and when it is not, is one of the most fun things that you will have to do as a WordPress plugin developer. And by <em>fun</em>, I mean <em>not fun</em>. Not fun at all.</p>
<p>Introduced in 3.0, the multisite feature feels a little awkward when developing code, partly because things you take for granted in single-site installs are simply not there, partly because other things are unnecessarily different, partly because of the shifting terminology that has evolved over time, and partly because of the significantly less documentation, compared to that available on single-site features. Moreover there are significantly less articles about it out there, since most people care about single-site installs.</p>
<p>But, it works! After all, last time I checked, <a href="http://wordpress.com" target="_blank" rel="noopener noreferrer">wordpress.com</a> was up and running! The fact that people were able to extend WordPress so drastically in a way that actually works, is impressive to say the least. I can only imagine how hard it must have been. The idea is that you can make your WordPress behave just like <code>wordpress.com</code>, where users can create their own site.</p>
<p>So yes, some things about <strong>network-activated plugins</strong> on multisite suck, but they don&#8217;t suck to the point of being unusable. You just need to learn a few tricks. Fortunately, you came across this article, so all will become clear. Or will they?</p>
<h2>Some basic terminology</h2>
<p>A <em>multisite</em> (aka <em>MS</em>) install is sometimes also called a <em>network</em>. A network can have a multitude of <em>sites</em>, and sites can have many <em>blogs</em>. Confusingly, <em>site</em> can also mean <em>blog</em> besides <em>network</em>. And <em>network</em> does not mean what we normally mean in IT, it means a collection of <em>sites</em> or <em>multi-sites</em>, depending on who you ask. And a <em>blog</em> of course is not necessarily an actual weblog, but can be any type of <em>site</em>.</p>
<p>To add to the fun, in the past the <em>multisite</em> feature was named <em>multiuser</em> (or WPMU, or MU for short). Presumably this was changed because <em>multiuser</em> is a stupid name for this feature: all WordPress installations can have multiple users. But you do need to be aware of all the terms out there, because you will come across them at some point.</p>
<p>To paraphrase an old saying,</p>
<blockquote><p>There are only two hard problems in computer science, cache invalidation and naming WordPress things.</p></blockquote>
<p>Users who create blogs, or sites, or whatever (sigh!) on your <em>multisite</em> are <em>administrators</em> of their respective sites, and you, the owner of the <em>network</em> are a <em>network admin</em>, also known as a <em>super admin</em>. The <em>network admin</em> can administer global settings for the entire <em>network</em> from the <em>network admin menu</em>.</p>
<h2>What does network activated actually mean?</h2>
<p>Themes and plugins can either be:</p>
<ul>
<li><strong>activated</strong> on individual blogs by <em>administrators</em>, if the administrators have the capability to do so, or</li>
<li><strong>network-activated</strong> once, site-wide, by the <em>network admin</em>, making them available to all blogs.</li>
</ul>
<h2>Custom DB tables</h2>
<p>Most tables in the DB are duplicated for each blog, where the table name prefix contains the blog ID, so that, for example, options for blogs 3 and 4 are safely stored in tables <code>wp_3_options</code> and <code>wp_4_options</code> respectively. This allows for blog administrators to activate and configure their plugins separately for their blogs. As long as they activate the plugins themselves, everything should work just fine for most plugins, out of the box.</p>
<p>If you are maintaining custom DB tables, these will need some extra coding work. There are two ways to do this: Either</p>
<ul>
<li>add a <code>blog_id</code> column to your rows, and then filter your SQL queries by <code>blog_id=<a href="https://codex.wordpress.org/Function_Reference/get_current_blog_id" target="_blank" rel="noopener noreferrer">get_current_blog_id()</a></code>, or</li>
<li>create separate tables for each blog when your plugin activates, by binding with the <a href="https://codex.wordpress.org/Function_Reference/register_activation_hook" target="_blank" rel="noopener noreferrer"><code>register_activation_hook()</code></a> function, and also create tables whenever a new blog is created after your plugin has been activated, by binding to <a href="https://codex.wordpress.org/Plugin_API/Action_Reference/wpmu_new_blog"><code>wpmu_new_blog</code></a>.</li>
</ul>
<p>You can read more about this here:</p>
<blockquote data-secret="cBkskEYduB" class="wp-embedded-content"><p><a href="http://sudarmuthu.com/blog/how-to-properly-create-tables-in-wordpress-multisite-plugins/">How To Properly Create Tables In WordPress Multisite Plugins</a></p></blockquote>
<p><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  src="http://sudarmuthu.com/blog/how-to-properly-create-tables-in-wordpress-multisite-plugins/embed/#?secret=cBkskEYduB" data-secret="cBkskEYduB" width="600" height="338" title="&#8220;How To Properly Create Tables In WordPress Multisite Plugins&#8221; &#8212; Night Dreaming (by Sudar)" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe></p>
<h2>To network activate, or not to network activate?</h2>
<p>Depending on what your plugin actually does, you might want to have parts of it operate site-wide. This often makes sense, but know that in doing so you might be opening a big can of worms. The network admin area introduces its own set of actions and filters, its own menu structure, its own URL structure, a somewhat lacking settings API where you have to do some things manually, and you will have to think about how your <code>wp_cron</code> hooks work, new user capabilities, and different plugin activation code.</p>
<p>The function <a href="https://codex.wordpress.org/Function_Reference/is_multisite" target="_blank" rel="noopener noreferrer"><code>is_multisite()</code></a> tells you whether the WordPress install is multisite, but it tells you nothing about whether your plugin is activated for the network or whether it was activated at the blog level.</p>
<p>To find out whether your plugin is network activated or not, first import <a href="https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network" target="_blank" rel="noopener noreferrer">the right function</a>:</p>
<pre>if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
    require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
}</pre>
<p>Then, you can do this:</p>
<pre><code>if ( is_plugin_active_for_network( 'myplugin/myplugin.php' ) ) {</code> // do stuff }</pre>
<p>assuming that your plugin&#8217;s slug is <code>myplugin</code>. The function will always return <code>false</code> on single-site installs.</p>
<h2>So many options&#8230;</h2>
<p>If some options must apply to the entire network, you will need to expose a menu of panels with those options to the network administrator.</p>
<h3>Know your DB options</h3>
<p>Those options can be thought of as &#8220;global&#8221; for a site (aka network), and they ought to be stored in the <code>wp_sitemeta</code> table. The table name is programmatically available as <code>$wpdb-&gt;sitemeta</code>. It is just like the <code>wp_options</code> table, but totally different: Columns <code>option_name</code> and <code>option_value</code> correspond to <code>meta_key</code> and <code>meta_value</code>. If you&#8217;re wondering about the <code>site_id</code> column, no, it&#8217;s not an identifier to individual blogs, it&#8217;s a unique identifier to a site, because, yes, you can have several sites on one network, each with several blogs.</p>
<h3>:s/_option/_site_option/</h3>
<p>These site meta options are accessed not by the usual functions <a href="https://developer.wordpress.org/reference/functions/get_option/" target="_blank" rel="noopener noreferrer"><code>get_option()</code></a> and friends, but by <a href="https://codex.wordpress.org/Function_Reference/get_site_option" target="_blank" rel="noopener noreferrer"><code>get_site_option()</code></a>, <a href="https://codex.wordpress.org/Function_Reference/update_site_option" target="_blank" rel="noopener noreferrer"><code>update_site_option()</code></a>, <a href="https://codex.wordpress.org/Function_Reference/add_site_option" target="_blank" rel="noopener noreferrer"><code>add_site_option()</code></a> and <a href="https://codex.wordpress.org/Function_Reference/delete_site_option" target="_blank" rel="noopener noreferrer"><code>delete_site_option()</code></a>.</p>
<h3>Option-related capabilities</h3>
<p>You might be interested in the <a href="https://codex.wordpress.org/Roles_and_Capabilities#manage_network_options" target="_blank" rel="noopener noreferrer"><code>manage_network_options</code></a> capability that is normally granted to Super Admins. In fact, have a look at <a href="https://codex.wordpress.org/Roles_and_Capabilities#manage_network" target="_blank" rel="noopener noreferrer">Roles and Capabilities</a> in the Codex to make sure you know about all of the <code>manage_network_*</code> capabilities, as well as the <code>*_sites</code> capabilities. Essential reading if you&#8217;re a network admin.</p>
<h3>Updating site options</h3>
<p>Know that you will have to do things differently when creating admin forms for network-wide options. In a nutshell:</p>
<ul>
<li>you need to hook to the <code>network_admin_menu</code> action rather than <code>admin_menu</code>.</li>
<li>your HTML forms need to have a different <code>action</code> attribute,</li>
<li>you need to bind a submit handler to:
<ul>
<li>check for the admin nonce,</li>
<li>actually save the settings to the DB, and</li>
<li>redirect back to the right admin panel.</li>
</ul>
</li>
<li>Use <a href="https://codex.wordpress.org/Function_Reference/network_admin_url"><code>network_admin_url()</code></a> wherever you&#8217;d normally use <a href="https://codex.wordpress.org/Function_Reference/admin_url"><code>admin_url()</code></a>.</li>
</ul>
<p>I will not go through all of this detail here, as it is explained very well in this rare article:</p>
<blockquote data-secret="Gc6F9iV1q6" class="wp-embedded-content"><p><a href="https://vedovini.net/2015/10/using-the-wordpress-settings-api-with-network-admin-pages/">Using the WordPress Settings API with Network Admin pages</a></p></blockquote>
<p><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  src="https://vedovini.net/2015/10/using-the-wordpress-settings-api-with-network-admin-pages/embed/#?secret=Gc6F9iV1q6" data-secret="Gc6F9iV1q6" width="600" height="338" title="&#8220;Using the WordPress Settings API with Network Admin pages&#8221; &#8212; vedovini.net" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe></p>
<p>I find this article tells you all that you need to know about how to actually save site meta, but if you want to read more, you can <a href="https://codex.wordpress.org/Creating_Options_Pages#Pitfalls" target="_blank" rel="noopener noreferrer">have a look at the official documentation</a>, which currently simply links to <a href="https://wordpress.stackexchange.com/questions/64968/settings-api-in-multisite-missing-update-message/72503#72503" target="_blank" rel="noopener noreferrer">here</a> and <a href="https://web.archive.org/web/20111102180213/http://code.hyperspatial.com/1250/save-plugin-options-multisite-3-1" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h3>Getting hooked to site-wide options yet? Not to worry, there&#8217;s more!</h3>
<p>Keep in mind that there exists a multitude of hooks that are specific to network administration. For example, to do various custom stuff to a site-wide option before saving it to the DB, do not bind to <a href="https://codex.wordpress.org/Plugin_API/Filter_Reference/pre_update_option_(option_name)" target="_blank" rel="noopener noreferrer"><code>pre_update_option_{$option_name}</code></a>, but to <a href="https://developer.wordpress.org/reference/hooks/pre_update_site_option_option/" target="_blank" rel="noopener noreferrer"><code>pre_update_site_{$option_name}</code></a>.</p>
<h3>Set option values via the terminal like a 1334 h@x0r</h3>
<p>If you&#8217;re like me, you probably have set up your build process to auto-inject options to your dev environment, using <code>wp-cli</code>. Instead of</p>
<pre> wp option update option_name option_value</pre>
<p>do something like this instead:</p>
<pre>wp network meta set 1 option_name option_value</pre>
<p>where <code>1</code> is the number of your multisite environment.</p>
<h3>How to nag the right admin</h3>
<p>To show notices to the <em>network admin</em>, do not bind to <a href="https://codex.wordpress.org/Plugin_API/Action_Reference/admin_notices" target="_blank" rel="noopener noreferrer"><code>admin_notices</code></a>, instead bind to <a href="https://codex.wordpress.org/Plugin_API/Action_Reference/network_admin_notices" target="_blank" rel="noopener noreferrer"><code>network_admin_notices</code></a>.</p>
<h3>The surprising secret argument of the activation hook that nobody knew about!!!</h3>
<p>Apologies, when I&#8217;m blogging I sometimes go into full SEO mode. Anyhow&#8230;</p>
<p>For your activation handler to work, you will first need to bind a function with <a href="https://codex.wordpress.org/Function_Reference/register_activation_hook" target="_blank" rel="noopener noreferrer"><code>register_activation_hook()</code></a>, as usual. It turns out that your activation handler can take a parameter that tells you whether the plugin is being network-activated or not.</p>
<p>Here&#8217;s a handy way I like to use to create options that can either be global on the multisite level, or bound to the current blog, depending on whether your plugin was network-activated or activated on a single blog:</p>
<pre>function activation_hook( $network_active ) {
    call_user_func(
        $network_active ? 'add_site_option' : 'add_option',
        'option_name',
        'option_value'
    );
}
register_activation_hook( __FILE__, 'activation_hook' );</pre>
<h3>Clean up after yourself!</h3>
<p>Remember to do the same in your <code>uninstall.php</code>. You do clean up after yourself, don&#8217;t you? Of course you do, you&#8217;re a developer, not a filthy pig.</p>
<p>Remember, you will not be able to use <a href="https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network" target="_blank" rel="noopener noreferrer"><code>is_plugin_active_for_network()</code></a>, because by the time the uninstall code runs, the plugin should have already been deactivated. Since the plugin is being uninstalled completely, you can do something like the following, just to be on the safe side:</p>
<pre>// delete from site meta
delete_site_option( 'option_name' );

// also delete from options table for all blogs
global $wpdb;
foreach ( $wpdb-&gt;get_col( "SELECT blog_id FROM $wpdb-&gt;blogs" ) as $blog_id ) {
    switch_to_blog( $blog_id );
    delete_option( 'option_name' );
    restore_current_blog();
}</pre>
<p>You will also likely want to delete any custom tables you may have created. The trick here is to iterate over all the blogs and let WordPress choose the right DB prefix.</p>
<pre>foreach ( $wpdb-&gt;get_col( "SELECT blog_id FROM $wpdb-&gt;blogs" ) as $blog_id ) {
    switch_to_blog( $blog_id );
    $wpdb-&gt;query( "DROP TABLE IF EXISTS {$wpdb-&gt;prefix}mytable" );
    restore_current_blog();
}</pre>
<p>If you have chosen the separate tables way, you should also hook a <code>DROP TABLE</code> query to the <a href="https://developer.wordpress.org/reference/hooks/delete_blog/" target="_blank" rel="noopener noreferrer"><code>delete_blog</code></a> hook:</p>
<pre>function delete_blog_tables( $blog_id, $drop ) {
    if ( $drop ) {
        switch_to_blog( $blog_id );
        $wpdb-&gt;query( "DROP TABLE IF EXISTS {$wpdb-&gt;prefix}mytable" );
        restore_current_blog();
    }
}
add_action( 'delete_blog', 'delete_blog_tables' );</pre>
<p>If on the other hand you have one table for all blogs, and your rows have a <code>blog_id</code> column, do something more in the tune of:</p>
<pre>function delete_blog_tables( $blog_id, $drop ) {
    if ( $drop ) {
        $wpdb-&gt;query( "DELETE FROM {$wpdb-&gt;prefix}mytable WHERE blog_id = $blog_id" );
    }
}
add_action( 'delete_blog', 'delete_blog_tables' );</pre>
<p>There! All clean now!</p>
<h3>Cron</h3>
<p>You will want to use that handy little <em>foreach</em> loop in more than just the uninstall script. For instance, suppose you&#8217;re using <a href="https://developer.wordpress.org/plugins/cron/" target="_blank" rel="noopener noreferrer">cron</a>. If your plugin is network activated, cron will run once for the network, not once per blog. Assuming your cron handler needs to do stuff to each blog, you&#8217;d do something like:</p>
<pre>function cron_handler( ) {
    if ( is_plugin_active_for_network( 'myplugin/myplugin.php' ) ) {
        global $wpdb;
        foreach ( $wpdb-&gt;get_col( "SELECT blog_id FROM $wpdb-&gt;blogs" ) as $blog_id ) {
            switch_to_blog( $blog_id );
            do_stuff();
            restore_current_blog();
        }
    } else {
        do_stuff();
    }
}
add_action( 'cron_hook', 'cron_handler' );

if ( false === wp_next_scheduled( 'cron_hook' ) ) {
    wp_schedule_event( time(), 'every_now_and_then', 'cron_hook' );
}

function do_stuff() {
    // do your stuff here
}</pre>
<p>This way if your plugin runs network-wide, then your stuff is done on each one of the blogs on your multisite. But if the plugin is activated on the blog level, it does stuff only on the current blog.</p>
<p>Good stuff!</p>
<h2><del datetime="2017-08-13T18:22:13+00:00">Super secret plugin header: Network</del></h2>
<p><strong>Edit: This plugin header seems to not be respected any more; perhaps this is why it is not documented. It is part of WordPress history.</strong></p>
<p><del datetime="2017-08-13T18:22:13+00:00">Although you wouldn&#8217;t have guessed it from <a href="https://developer.wordpress.org/plugins/the-basics/header-requirements/" target="_blank" rel="noopener noreferrer">reading the relevant documentation</a>, the plugin header, i.e. that special block of comments that you put at the top of your main plugin file, can have something to say about network activation. (A tiny hint is given <a href="https://codex.wordpress.org/File_Header" target="_blank" rel="noopener noreferrer">in the Codex</a> that such a header exists, although the documentation does not currently bother explaining what it does. After all, if everything was documented, where would the fun be?)</del></p>
<p><del datetime="2017-08-13T18:22:13+00:00">If your plugin is intended to be <strong>only network activated</strong>, and not activated on the blog-level in multi-sites, then add this line to your plugin&#8217;s header:</del></p>
<pre>Network: True</pre>
<p><del datetime="2017-08-13T18:22:13+00:00">Now WordPress will not give the activation option to mere blog admins.</del></p>
<h2>Are you still with me?</h2>
<p>Awesome! There you have it! Now you know all that I currently know about WordPress multisite and network activated plugins. Which is arguably not a lot. If you know more and feel like sharing, you&#8217;re free to comment below.</p>
<p>Hopefully you now have some grasp of the types of things you should be looking out for when you take your plugin for a ride into the wacky world of network activation.</p>
<p>Take care.</p>
<p>Alex</p>
<p>The post <a href="https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/">🖧 The wacky world of network activated WordPress plugins in multisite</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
	</channel>
</rss>
