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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>