I’ve written before about disabling plugin deactivation in WordPress, but I’ve finally used that knowledge in practice–on this site.

The Problem

Let’s say you’re going along your day, developing things, and fixing things, and making the world a better place when all of a sudden you get a call from a client that their website is broken!!  After a bit of panicking and digging around, it turns out that they’ve been “optimizing” things by disabling random, but critical, plugins on the site.

The Solution

One way that you might fix this is to not install the plugins via the WordPress UI and require() them either in the theme directory, or as an mu-plugin.  The downside with this is that you lose the ability to easily and auto-update the plugins (if you’re okay with that).  You also the ability to easily see what plugins are active and installed in the admin UI.

The way I’ve got around this is to create this helper function inside an mu-plugin that allows the plugins to be installed and managed in the UI, but not disabled:

<?php
/**
 * Secures a plugin from accidental disabling in the UI.
 *
 * If a plugin is necessary for a site to function, it should not be disabled.
 * This functionc can also optionally "force" activate a plugin without having to
 * activate it in the plugin UI.  Forcing activation will cause it to skip all
 * core plugin activation hooks.
 *
 * @param string  $plugin              Plugin file to secure.
 * @param boolean $force_activation    Optional. Whether to force load the plugin. Default false.
 */
function emrikol_secure_plugin( $plugin, $force_activation = false ) {
	$proper_plugin_name = false;

	// Match if properly named: wp-plugin (wp-plugin/wp-plugin.php).
	if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin . '/' . $plugin . '.php' ) && is_file( WP_PLUGIN_DIR . '/' . $plugin . '/' . $plugin . '.php' ) ) {
		$proper_plugin_name = $plugin . '/' . $plugin . '.php';
	} else {
		// Match if improperly named: wp-plugin/cool-plugin.php.
		if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin ) && is_file( WP_PLUGIN_DIR . '/' . $plugin ) ) {
			$proper_plugin_name = $plugin;
		}
	}

	if ( false !== $proper_plugin_name ) {
		if ( true === $force_activation ) {
			// Always list the plugin as active.
			add_filter( 'option_active_plugins', function( $active_plugins ) use ( $proper_plugin_name ) {
				// Crappy hack to prevent infinite loops.  Surely there's a better way.
				global $emrikol_is_updating_active_plugins;

				if ( true === $emrikol_is_updating_active_plugins ) {
					unset( $emrikol_is_updating_active_plugins );
					return array_unique( $active_plugins );
				}

				if ( ! in_array( $proper_plugin_name, $active_plugins, true ) ) {
					$active_plugins[]                   = $proper_plugin_name;
					$emrikol_is_updating_active_plugins = true;

					update_option( 'active_plugins', array_unique( $active_plugins ) );
				}
				return array_unique( $active_plugins );
			}, 1000, 1 );
		}

		// Ensure the plugin doesn't get disabled somehow.
		// TODO: Diff arrays.  Only run if the plugin is being removed.
		add_filter( 'pre_update_option_active_plugins', function ( $active_plugins ) use ( $proper_plugin_name ) {
			if ( ! in_array( $proper_plugin_name, $active_plugins, true ) ) {
				$active_plugins[] = $proper_plugin_name;
			}
			return array_unique( $active_plugins );
		}, 1000, 1 );

		// Remove the disable button.
		$plugin_basename = plugin_basename( $proper_plugin_name );
		add_filter( "plugin_action_links_$plugin_basename", function( $links ) use ( $proper_plugin_name, $force_activation ) {
			if ( isset( $links['deactivate'] ) ) {
				$links['deactivate'] = sprintf(
					'<span class="emrikol-secure-plugin wp-ui-text-primary">%s</span>',
					$force_activation ? 'Plugin Activated via Theme Code' : 'Plugin Secured via Theme Code'
				);
			}
			return $links;
		}, 1000, 1 );
	}
}

It’s not perfect, but it’s working for me right now.  Like, right now on this site as you’re reading this. I’ve added it as an mu-plugin like so:

<?php
require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/secure-plugins.php' );

emrikol_secure_plugin( 'akismet' );
emrikol_secure_plugin( 'amp' );
emrikol_secure_plugin( 'jetpack' );
emrikol_secure_plugin( 'wp-super-cache/wp-cache.php' );

As you can see, this completely removes the “Deactivate” link in the UI:

The emrikol_secure_plugin() function takes two arguments:

  1. The plugin to secure.  This can either be the plugin slug (ex. jetpack) or the full plugin path if the plugin doesn’t follow standard naming conventions (wp-super-cache/wp-cache.php)
  2. A boolean, defaults to false.  If it is true the plugin will be forced to activate without user intervention.  This can be used to activate a plugin on a new install without having to manually enable it in the UI or via WP-CLI

Leave a comment

Leave a Reply

%d bloggers like this: