Uninstall, Activate, Deactivate a plugin: typical features & how-to

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?

Read More

Do I clean up my option entries?

Anything else?

Related posts

Leave a Reply

2 comments

  1. 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.

  2. 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