Deactivate Other Plug-ins On Deactivation

Note: The original proposed fix might cause undesirable effects if more than one plug-in is deactivated at once. The reason being that ‘aborting’ a deactivation, will abort all subsequent deactivations when deactivating more than one plug-in. I’ve included a better workaround which is far more reliable.

In this article I considered the situtation where you were developing an add-on plug-in, B, for a plug-in A, where B will obviously requrie A to be installed and active to function correctly. That article focused on how you can check that A was active, when activating B and if not displaying an admin notice.

Now we flip the problem on its head. Let’s suppose that the user is now deactivating A. We want to make sure that we then deactivate B (obviously only if it’s active – but WordPress handles these checks).

How we might like it to work but it doesn’t…

We might hope that we can just use deactivate_plugins inside the deactivation callback for A… unfortunately this doesn’t work and inspecting the code reveals why.

When the user deactivates A, WordPress calls deactivate_plugins(A).This function does the following:

  1. Gets an arary of all the current active plugins from the database.
  2. Performs some checks (e.g. is plugin A actually active?)
  3. Removes A from this array
  4. Fires the hook deactivate_A (which we hook onto using register_deactivation_hook)
  5. Updates the array to the database.

Now at step 4, we call deactivate_plugins(B) to deactivate B. 1-5 run through for B and is completed. The eagle eyed will notice that at this point the database is updated to consider B inactive, but A still active – but this isn’t the problem, in fact the reverse.

Once that’s completed we proceed to step 5 (in the original deactivate_plugins() call for A). Now this array is updated to the database – but this array was the very original one retrieved in step 1 and only has A removed. In particular we retrieved it at the beginning when B was still active, and so it contains B.

Note: your deactivation callbacks for B are fired, even through WordPress still thinks its active next time the page loads.

Solution – see better solution below

The fix is simple. In the callback of A, deactivate B as before. Then retrieve the updated active plug-ins array, and remove A from it and update the database. The database now has both A and B as inactive. Finally, to prevent WordPress undoing this, wp_redirect and exit to ‘abort’ the original deactivate_plugins process:

//This goes inside plugin A.
//When A is deactivated. Deactivate B.
register_deactivation_hook(__FILE__,'my_plugin_A_deactivate'); 
function my_plugin_A_deactivate(){
     $dependent = 'B/B.php';

    if( is_plugin_active($dependent) ){

        $dependent = plugin_basename( trim( $dependent) );
        $parent = plugin_basename( __FILE__  );

        //Deactivate the dependent 'add on'.
        deactivate_plugins($dependent);

        //Here comes the work around.... Manually remove THIS plugin from updated active_plugins
        $current = get_option( 'active_plugins', array() );
        $key = array_search( $parent, $current );
        if ( false !== $key ) {
            array_splice( $current, $key, 1 );
        }
        update_option('active_plugins', $current);

        //Now redirect to prevent WordPress from adding 'dependent' back in...
        wp_redirect(admin_url('plugins.php?deactivate=true&plugin_status=all&paged=1'));
        exit();
    }
}

A better solution

For reasons given at the top of this post, the above solution is less than idea. However, a better work-around is at hand. When WordPress updates the database with active arrays it does so with the generic update_option function. Consequently update_option_{$option} is triggered (after the option has been updated). You can use this to hook to deactivate your plug-in.

(There is the pre_update_option_{$option} filter in which you could filter the active plug-ins – but this is a bit too low level, and among other things occurs after sanitize_option is applied. While that doesn’t do anything for this option, it might do in the future. Also, we still want to use deactivate_plugins rather than handle the database option directly).

Here’s the complete solution:

//This goes inside Plugin A.
//When A is deactivated. Deactivate B.
  register_deactivation_hook(__FILE__,'my_plugin_A_deactivate'); 
function my_plugin_A_deactivate(){
    $dependent = 'B/B.php';
    if( is_plugin_active($dependent) ){
         add_action('update_option_active_plugins', 'my_deactivate_dependent_B');
    }
}

function my_deactivate_dependent_B(){
    $dependent = 'B/B.php';
    deactivate_plugins($dependent);
}

A similar workaround exists for network wide deactivation for multi sites.

I’ve posted this as a solution to this question on WordPress StackExchange. A trac ticket is been opened for this bug.

How to check another plug-in exists and get its details

If you’re writing an add-on for one of your (or someone else’s) plug-in you want to make sure that that plug-in is active (and quite often, if it’s a sufficiently new version).

Is the plug-in active?

You can get tell if if a plug-in is active by using is_plugin_active($plugin) where $plugin is the sub-directory/file of the plug-in.

Getting the plug-in details

To get the details of that plug-in you could use get_plugin_data() but rather than taking the sub-directory/file as an argument (as above), it requires the path to the plug-in file. I’m not sure of way of doing this without relying on some constants – so an alternative is to use get_plugins().

get_plugins() returns an array, with the plug-ins as keys (more exactly, the sub-directory/file) and the plug-in’s details (including version) as a value. As an example, take my Event Organiser plugin.

 $plugin = 'event-organiser/event-organiser.php';
 if( is_plugin_active($plugin) ){
      $plugins = get_plugins();
      $plugin_data = $plugins[$plugin];
      print_r($plugin_data);
 }

This prints out something like the following:

Array ( 
 [Name] => Event Organiser 
 [PluginURI] => http://wp-event-organiser.com
 [Version] => 1.4 
 [Description] => Creates a custom post type 'events' with features such as reoccurring events, venues, Google Maps, calendar views and events and venue pages 
 [Author] => Stephen Harris 
 [AuthorURI] => http://stephenharris.info
 [TextDomain] => ''
 [DomainPath] => ''
 [Network] => ''
 [Title] => Event Organiser 
 [AuthorName] => Stephen Harris 
)

Putting it all together

Let’s suppose we have a plugin A (this is the ‘parent’ plug-in) and we we want to create an add-on for it, B. We’ll allow the add on to be activated – but if A isn’t activated, we’ll display an error message:

register_activation_hook(__FILE__,'B_add_on_plugin_activate'); 
function B_add_on_plugin_activate(){
     $parent= 'A/A.php';

    if( is_plugin_active($parent) ){
        $plugins = get_plugins();
        $plugin_data = $plugins[$parent];

        //The parent plug-in is installed - but is it the correct version?
        if( $plugin_data['Version'] < '1.5' ){
            update_option('pluginA_install_nag','wrongversion');
        }
    }else{
        update_option('pluginA_install_nag','needtoinstall');
    }
}

add_action('load-plugins.php','B_add_on_activate_check');
function my_add_on_activate_check(){
    if( get_option('pluginA_install_nag',false) ){
        delete_option('pluginA_install_nag');
        add_action('admin_notices', 'B_add_on_install_A_notice');
    }
}

function B_add_on_install_A_notice(){
    echo '<div class="updated"><p>
        This plug-in requires <strong>A</strong> (v1.5 or higher) to function correctly. Download <a href="">A</a>.
    </p></div>';
}

Alternatively you could redirect from the activation, and – in effect – abort it. But by doing so, you obviously cant display a message (since your add-on plug-in won’t be activated …). This is probably bad, because it gives the user no feedback.

Deactivating…

What if someone installs the add-on plugin B, and then deactivates the A. Suppose, in this scenario we’d like to deactivate our add on plugin too. We might hope that we can just use deactivate_plugins inside the deactivation hook for A… unfortunately this doesn’t work and inspecting the code reveals why. I talk about it in this article.