Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I'm making a wordpress plugin. What are typical things I should include in the uninstall feature?

For example, should I delete any tables I created in the install function?

Do I clean up my option entries?

Anything else?

share|improve this question
    
I wasted so much time trying to get it working. The problem is that init hook not working inside of registering hooks. I suppose that not one hook (action or filter) will not work so early. Read the notes by link below. codex.wordpress.org/Function_Reference/register_activation_hook It says: "Registering the hook inside your plugins_loaded hook is too late and it won't run! (Even when it seems to work for register_deactivation_hook until the wp_loaded hook.)" –  Anton Nov 9 '11 at 12:43
    
I'm the one who updated the codex to what you mentioned, so this is considered in the above ↑ answer. :) –  kaiser Feb 28 '12 at 0:00
add comment

2 Answers

up vote 68 down vote accepted

There are three different hooks. They trigger in the following cases:

  • Uninstall
  • Deactivation
  • Activation

How-to trigger functions safely during the scenarios

The following shows the right ways to safely hook callback functions that get triggered during the mentioned actions.

As you could use this code in a plugin that uses

  • plain functions,
  • a class or
  • an external class,

I'll show three different demo plugins that you can inspect and then later on implement the code in your own plugin(s).

Important note upfront!

As this topic is extremely difficult and very detailed and has a dozen+ edge cases, this answer will never be perfect. I'll keep improving it over time, so check back on a regular base.

(1) Activate/Deactivate/Uninstall plugins.

The plugin setup callbacks are triggered by core and you have no influence on how core does this. There're some things to keep in mind:

  • Never, ever echo/print anything(!) during setup callbacks. This will lead to headers already sent message and core will recommend to deactivate and delete your plugin... don't ask: I know...
  • You won't see any visual output. But I added exit() statements to all different callbacks so you can get some insights on what is really happening. Just uncomment them to see stuff working.
  • It's extremely important that you check if __FILE__ != WP_PLUGIN_INSTALL and (if not: abort!) to see if one is really uninstalling the plugin. I'd recommend to simply trigger on_deactivation() callbacks during development, so you save yourself the time that you'd need to get everything back in. At least this is what I do.
  • I as well do some security stuff. Some is done by core as well, but hey! Better safe than sorry!.
    • First I disallow direct file access when core isn't loaded: defined( 'ABSPATH' ) OR exit;
    • Then I check if the current user is allowed to do this task.
    • As a last task, I check the referrer. Note: There can be unexpected results with a wp_die() screen asking for proper permissions (and if you want to try again ... yeah, sure), when you got an error. This happens as core redirects you, sets the current $GLOBALS['wp_list_table']->current_action(); to error_scrape and then checks the referrer for check_admin_referer('plugin-activation-error_' . $plugin);, where $plugin is $_REQUEST['plugin']. So the redirect happens at half the page load and you get this wired scroll bar and the die screen insight the yellow admin notice/message box. If this happens: Stay calm and just search for the error with some exit() and step-by-step debugging.

(A) Plain functions plugin

Remember that this might not work if you hook the callbacks before the function definition.

<?php
defined( 'ABSPATH' ) OR exit;
/**
 * Plugin Name: (WCM) Activate/Deactivate/Uninstall - Functions
 * Description: Example Plugin to show activation/deactivation/uninstall callbacks for plain functions.
 * Author:      Franz Josef Kaiser/wecodemore
 * Author URL:  http://unserkaiser.com
 * Plugin URL:  http://wordpress.stackexchange.com/questions/25910/uninstall-activate-deactivate-a-plugin-typical-features-how-to/25979#25979
 */

function WCM_Setup_Demo_on_activation()
{
    if ( ! current_user_can( 'activate_plugins' ) )
        return;
    $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
    check_admin_referer( "activate-plugin_{$plugin}" );

    # Uncomment the following line to see the function in action
    # exit( var_dump( $_GET ) );
}

function WCM_Setup_Demo_on_deactivation()
{
    if ( ! current_user_can( 'activate_plugins' ) )
        return;
    $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
    check_admin_referer( "deactivate-plugin_{$plugin}" );

    # Uncomment the following line to see the function in action
    # exit( var_dump( $_GET ) );
}

function WCM_Setup_Demo_on_uninstall()
{
    if ( ! current_user_can( 'activate_plugins' ) )
        return;
    check_admin_referer( 'bulk-plugins' );

    // Important: Check if the file is the one
    // that was registered during the uninstall hook.
    if ( __FILE__ != WP_UNINSTALL_PLUGIN )
        return;

    # Uncomment the following line to see the function in action
    # exit( var_dump( $_GET ) );
}

register_activation_hook(   __FILE__, 'WCM_Setup_Demo_on_activation' );
register_deactivation_hook( __FILE__, 'WCM_Setup_Demo_on_deactivation' );
register_uninstall_hook(    __FILE__, 'WCM_Setup_Demo_on_uninstall' );

(B) A class based/OOP architecture

This is the most common example in nowadays plugins.

<?php
defined( 'ABSPATH' ) OR exit;
/**
 * Plugin Name: (WCM) Activate/Deactivate/Uninstall - CLASS
 * Description: Example Plugin to show activation/deactivation/uninstall callbacks for classes/objects.
 * Author:      Franz Josef Kaiser/wecodemore
 * Author URL:  http://unserkaiser.com
 * Plugin URL:  http://wordpress.stackexchange.com/questions/25910/uninstall-activate-deactivate-a-plugin-typical-features-how-to/25979#25979
 */


register_activation_hook(   __FILE__, array( 'WCM_Setup_Demo_Class', 'on_activation' ) );
register_deactivation_hook( __FILE__, array( 'WCM_Setup_Demo_Class', 'on_deactivation' ) );
register_uninstall_hook(    __FILE__, array( 'WCM_Setup_Demo_Class', 'on_uninstall' ) );

add_action( 'plugins_loaded', array( 'WCM_Setup_Demo_Class', 'init' ) );
class WCM_Setup_Demo_Class
{
    protected static $instance;

    public static function init()
    {
        is_null( self::$instance ) AND self::$instance = new self;
        return self::$instance;
    }

    public static function on_activation()
    {
        if ( ! current_user_can( 'activate_plugins' ) )
            return;
        $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
        check_admin_referer( "activate-plugin_{$plugin}" );

        # Uncomment the following line to see the function in action
        # exit( var_dump( $_GET ) );
    }

    public static function on_deactivation()
    {
        if ( ! current_user_can( 'activate_plugins' ) )
            return;
        $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
        check_admin_referer( "deactivate-plugin_{$plugin}" );

        # Uncomment the following line to see the function in action
        # exit( var_dump( $_GET ) );
    }

    public static function on_uninstall()
    {
        if ( ! current_user_can( 'activate_plugins' ) )
            return;
        check_admin_referer( 'bulk-plugins' );

        // Important: Check if the file is the one
        // that was registered during the uninstall hook.
        if ( __FILE__ != WP_UNINSTALL_PLUGIN )
            return;

        # Uncomment the following line to see the function in action
        # exit( var_dump( $_GET ) );
    }

    public function __construct()
    {
        # INIT the plugin: Hook your callbacks
    }
}

(C) A class based/OOP architecture with an external setup object

This scenario assumes that you got a main plugin file and a second file named setup.php in a subdirectory of the plugin named inc: ~/wp-content/plugins/your_plugin/inc/setup.php. This will work as well when the plugin folder is outside the default WP folder structure, as well when the content dir is renamed or in cases where your setup file is named different. Only the inc folder has to have the same name & location relative from the plugins root directory.

Note: You can simply take the three register_*_hook()* functions and the classes and drop them into your plugin.

The main plugin file:

<?php
defined( 'ABSPATH' ) OR exit;
/**
 * Plugin Name: (WCM) Activate/Deactivate/Uninstall - FILE/CLASS
 * Description: Example Plugin
 * Author:      Franz Josef Kaiser/wecodemore
 * Author URL:  http://unserkaiser.com
 * Plugin URL:  http://wordpress.stackexchange.com/questions/25910/uninstall-activate-deactivate-a-plugin-typical-features-how-to/25979#25979
 */


register_activation_hook(   __FILE__, array( 'WCM_Setup_Demo_File_Inc', 'on_activation' ) );
register_deactivation_hook( __FILE__, array( 'WCM_Setup_Demo_File_Inc', 'on_deactivation' ) );
register_uninstall_hook(    __FILE__, array( 'WCM_Setup_Demo_File_Inc', 'on_uninstall' ) );

add_action( 'plugins_loaded', array( 'WCM_Setup_Demo_File', 'init' ) );
class WCM_Setup_Demo_File
{
    protected static $instance;

    public static function init()
    {
        is_null( self::$instance ) AND self::$instance = new self;
        return self::$instance;
    }

    public function __construct()
    {
        add_action( current_filter(), array( $this, 'load_files' ), 30 );
    }

    public function load_files()
    {
        foreach ( glob( plugin_dir_path( __FILE__ ).'inc/*.php' ) as $file )
            include_once $file;
    }
}

The setup file:

<?php
defined( 'ABSPATH' ) OR exit;

class WCM_Setup_Demo_File_Inc
{
    public static function on_activation()
    {
        if ( ! current_user_can( 'activate_plugins' ) )
            return;
        $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
        check_admin_referer( "activate-plugin_{$plugin}" );

        # Uncomment the following line to see the function in action
        # exit( var_dump( $_GET ) );
    }

    public static function on_deactivation()
    {
        if ( ! current_user_can( 'activate_plugins' ) )
            return;
        $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';
        check_admin_referer( "deactivate-plugin_{$plugin}" );

        # Uncomment the following line to see the function in action
        # exit( var_dump( $_GET ) );
    }

    public static function on_uninstall()
    {
        if ( ! current_user_can( 'activate_plugins' ) )
            return;
        check_admin_referer( 'bulk-plugins' );

        // Important: Check if the file is the one
        // that was registered during the uninstall hook.
        if ( __FILE__ != WP_UNINSTALL_PLUGIN )
            return;

        # Uncomment the following line to see the function in action
        # exit( var_dump( $_GET ) );
    }
}

(2) Plugin updates

If you write a plugin that has its own DB table or options, there might be scenarios where you need to change or upgrade things.

Sadly so far there's no possibility to run something on plugin/theme install or update/upgrade. Gladly there's a work-around: Hook a custom function to a custom option (yea, it's lame - but it works).

function prefix_upgrade_plugin() 
{
    $v = 'plugin_db_version';
    $update_option = null;
    // Upgrade to version 2
    if ( 2 !== get_option( $v ) ) 
    {
        if ( 2 < get_option( $v ) )
        {
            // Callback function must return true on success
            $update_option = custom_upgrade_cb_fn_v3();

            // Only update option if it was an success
            if ( $update_option )
                update_option( $v, 2 );
        }
    }

    // Upgrade to version 3, runs just after upgrade to version 2
    if ( 3 !== get_option( $v ) ) 
    {
        // re-run from beginning if previous update failed
        if ( 2 < get_option( $v ) )
            return prefix_upgrade_plugin();

        if ( 3 < get_option( $v ) )
        {
            // Callback function must return true on success
            $update_option = custom_upgrade_cb_fn_v3();

            // Only update option if it was an success
            if ( $update_option )
                update_option( $v, 3 );
        }
    }

    // Return the result from the update cb fn, so we can test for success/fail/error
    if ( $update_option )
        return $update_option;

return false;
}
add_action('admin_init', 'prefix_upgrade_plugin' );

Source

This update function is a not-so-nice/well-written example, but as said: It's an example and the technique works well. Will improve that with a later update.

share|improve this answer
    
This is great BUT what I really want to know are things that I should include in my deactivation method...for example, should I delete my tables in the database or leave them in case the user changes their mind and re-activates the plugin? –  redconservatory Aug 15 '11 at 21:25
    
Ad "BUT": I mentioned that there are 3 methods. One for activation, one for temporary deactivation and one for unstall. Imho "uninstall" says "Remove me and everything I did", while "deactivate" is a temporary state and might be redone. But: See update. I added comments about your Q + extended it with some development recommendations. –  kaiser Aug 15 '11 at 21:55
3  
Ah I understand now. Just a question, when does uninstalled get called? When the files are deleted?? –  redconservatory Aug 16 '11 at 22:40
    
Hm. Good Q. plugin.php is the file that holds register_uninstall_hook & gets loaded with the early files in wp-settings.php (which is the main file to always look at). I update the Q with the docblocks from core. –  kaiser Aug 17 '11 at 8:14
    
I also added the check for the WP_UNINSTALL_PLUGIN constant, which is basically the file name, so you can hold your uninstall functions in a separate file and avoid the execution of other functions that sit in your plugins files. –  kaiser Aug 17 '11 at 8:20
show 9 more comments

To test the current system for required featurs like PHP version or installed extensions you can use something like that:

<?php  # -*- coding: utf-8 -*-
/**
 * Plugin Name: T5 Check Plugin Requirements
 * Description: Test for PHP version and installed extensions
 * Plugin URI:
 * Version:     2013.03.31
 * Author:      Thomas Scholz
 * Author URI:  http://toscho.de
 * Licence:     MIT
 * License URI: http://opensource.org/licenses/MIT
 */

/*
 * Don't start on every page, the plugin page is enough.
 */
if ( ! empty ( $GLOBALS['pagenow'] ) && 'plugins.php' === $GLOBALS['pagenow'] )
    add_action( 'admin_notices', 't5_check_admin_notices', 0 );

/**
 * Test current system for the features the plugin needs.
 *
 * @return array Errors or empty array
 */
function t5_check_plugin_requirements()
{
    $php_min_version = '5.4';
    // see http://www.php.net/manual/en/extensions.alphabetical.php
    $extensions = array (
        'iconv',
        'mbstring',
        'id3'
    );
    $errors = array ();

    $php_current_version = phpversion();

    if ( version_compare( $php_min_version, $php_current_version, '>' ) )
        $errors[] = "Your server is running PHP version $php_current_version but
            this plugin requires at least PHP $php_min_version. Please run an upgrade.";

    foreach ( $extensions as $extension )
        if ( ! extension_loaded( $extension ) )
            $errors[] = "Please install the extension $extension to run this plugin.";

    return $errors;

}

/**
 * Call t5_check_plugin_requirements() and deactivate this plugin if there are error.
 *
 * @wp-hook admin_notices
 * @return  void
 */
function t5_check_admin_notices()
{
    $errors = t5_check_plugin_requirements();

    if ( empty ( $errors ) )
        return;

    // Suppress "Plugin activated" notice.
    unset( $_GET['activate'] );

    // this plugin's name
    $name = get_file_data( __FILE__, array ( 'Plugin Name' ), 'plugin' );

    printf(
        '<div class="error"><p>%1$s</p>
        <p><i>%2$s</i> has been deactivated.</p></div>',
        join( '</p><p>', $errors ),
        $name[0]
    );
    deactivate_plugins( plugin_basename( __FILE__ ) );
}

Test with a check for PHP 5.5:

enter image description here

share|improve this answer
    
Touch confused, so basically there isn't a call to register_activation_hook here -- why not use it? Also will this fire before or after register_activation_hook and will register_activation_hook fire even if the above doesn't pass? –  orionrush Jul 31 '13 at 12:52
    
It runs after the activation hook on the plugin page only. –  toscho Jul 31 '13 at 12:54
    
I see - but if the plugin is activated outside the plugin page (say as part of a theme dependency) then your checks will get skipped no? So I tried moving add_action( 'admin_notices', 't5_check_admin_notices', 0 ); into an activation hook and the plugin activates without performing the checks . . . –  orionrush Jul 31 '13 at 13:30
    
@kaiser has explained how the activation hook works, I wanted to show an alternative. If the plugin is not activated per plugin page a fatal error might happen, yes. This approach cannot work on an activation hook without serious rewrite, because that hook fires after admin_notices. –  toscho Jul 31 '13 at 13:46
    
Actually just stumbled on the easy way: stackoverflow.com/a/13927297/362445 –  orionrush Jul 31 '13 at 13:50
show 1 more comment

protected by kaiser Feb 11 '13 at 19:23

Thank you for your interest in this question. Because it has attracted low-quality answers, posting an answer now requires 10 reputation on this site.

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.