Удаление, активация и деактивация плагина: типичные функции и инструкции
Я разрабатываю плагин для WordPress. Какие типичные функции следует включить в процесс удаления?
Например, нужно ли удалять таблицы, которые я создал при установке?
Следует ли очищать записи опций?
Что-нибудь еще?

Существует три различных хука. Они срабатывают в следующих случаях:
- Удаление
- Деактивация
- Активация
Как безопасно запускать функции в этих сценариях
Ниже показаны правильные способы безопасного подключения функций обратного вызова, которые срабатывают во время упомянутых действий.
Поскольку вы можете использовать этот код в плагине, который использует:
- обычные функции,
- класс или
- внешний класс,
я покажу три разных демонстрационных плагина, которые вы можете изучить, а затем реализовать код в своем собственном плагине(ах).
Важное замечание заранее!
Поскольку эта тема чрезвычайно сложная и очень детализированная, с множеством крайних случаев, этот ответ никогда не будет идеальным. Я буду продолжать улучшать его со временем, так что заглядывайте сюда регулярно.
(1) Активация/Деактивация/Удаление плагинов.
Функции обратного вызова настройки плагина запускаются ядром WordPress, и вы не можете повлиять на то, как ядро это делает. Есть несколько моментов, которые нужно учитывать:
- Никогда не выводите ничего через
echo/print
во время функций обратного вызова настройки. Это приведет к сообщениюheaders already sent
, и ядро порекомендует деактивировать и удалить ваш плагин... не спрашивайте: я знаю... - Вы не увидите никакого визуального вывода. Но я добавил операторы
exit()
во все различные функции обратного вызова, чтобы вы могли понять, что действительно происходит. Просто раскомментируйте их, чтобы увидеть работу. - Очень важно проверять, что
__FILE__ != WP_PLUGIN_INSTALL
и (если нет: прерывать выполнение!), чтобы убедиться, что плагин действительно удаляется. Я рекомендую просто запускать функцииon_deactivation()
во время разработки, чтобы сэкономить время на восстановление всего. По крайней мере, я так делаю. - Я также добавляю некоторые меры безопасности. Некоторые из них уже выполняются ядром, но, как говорится, Лучше перестраховаться, чем потом жалеть!.
- Сначала я запрещаю прямой доступ к файлу, если ядро не загружено:
defined( 'ABSPATH' ) OR exit;
- Затем я проверяю, имеет ли текущий пользователь права на выполнение этой задачи.
- Последней задачей я проверяю реферер. Примечание: Могут быть неожиданные результаты с экраном
wp_die()
, запрашивающим соответствующие права (и хотите ли вы попробовать снова... ну конечно), если у вас произошла ошибка. Это происходит, потому что ядро перенаправляет вас, устанавливает текущее значение$GLOBALS['wp_list_table']->current_action();
вerror_scrape
, а затем проверяет реферер дляcheck_admin_referer('plugin-activation-error_' . $plugin);
, где$plugin
— это$_REQUEST['plugin']
. Так что перенаправление происходит на полпути загрузки страницы, и вы получаете эту странную полосу прокрутки и экран смерти внутри желтого уведомления/сообщения администратора. Если это произойдет: сохраняйте спокойствие и просто ищите ошибку с помощьюexit()
и пошаговой отладки.
- Сначала я запрещаю прямой доступ к файлу, если ядро не загружено:
(A) Плагин с обычными функциями
Помните, что это может не работать, если вы подключаете функции обратного вызова до определения функции.
<?php
defined( 'ABSPATH' ) OR exit;
/**
* Plugin Name: (WCM) Activate/Deactivate/Uninstall - Functions
* Description: Пример плагина, демонстрирующий функции обратного вызова для активации/деактивации/удаления с обычными функциями.
* 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}" );
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# 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}" );
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# exit( var_dump( $_GET ) );
}
function WCM_Setup_Demo_on_uninstall()
{
if ( ! current_user_can( 'activate_plugins' ) )
return;
check_admin_referer( 'bulk-plugins' );
// Важно: Проверьте, что файл тот же,
// который был зарегистрирован во время хука удаления.
if ( __FILE__ != WP_UNINSTALL_PLUGIN )
return;
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# 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) Архитектура на основе классов/OOP
Это наиболее распространенный пример в современных плагинах.
<?php
defined( 'ABSPATH' ) OR exit;
/**
* Plugin Name: (WCM) Activate/Deactivate/Uninstall - CLASS
* Description: Пример плагина, демонстрирующий функции обратного вызова для активации/деактивации/удаления в классах/объектах.
* 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}" );
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# 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}" );
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# exit( var_dump( $_GET ) );
}
public static function on_uninstall()
{
if ( ! current_user_can( 'activate_plugins' ) )
return;
check_admin_referer( 'bulk-plugins' );
// Важно: Проверьте, что файл тот же,
// который был зарегистрирован во время хука удаления.
if ( __FILE__ != WP_UNINSTALL_PLUGIN )
return;
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# exit( var_dump( $_GET ) );
}
public function __construct()
{
# Инициализация плагина: Подключите ваши функции обратного вызова
}
}
(C) Архитектура на основе классов/OOP с внешним объектом настройки
Этот сценарий предполагает, что у вас есть главный файл плагина и второй файл с именем setup.php
в подкаталоге плагина с именем inc
: ~/wp-content/plugins/your_plugin/inc/setup.php
. Это будет работать даже если папка плагина находится вне стандартной структуры WP, если папка контента переименована или если ваш файл настройки имеет другое имя. Только папка inc
должна иметь то же имя и расположение относительно корневой директории плагина.
Примечание: Вы можете просто взять три функции register_*_hook()
и классы и вставить их в свой плагин.
Главный файл плагина:
<?php
defined( 'ABSPATH' ) OR exit;
/**
* Plugin Name: (WCM) Activate/Deactivate/Uninstall - FILE/CLASS
* Description: Пример плагина
* 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;
}
}
Файл настройки:
<?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}" );
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# 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}" );
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# exit( var_dump( $_GET ) );
}
public static function on_uninstall()
{
if ( ! current_user_can( 'activate_plugins' ) )
return;
check_admin_referer( 'bulk-plugins' );
// Важно: Проверьте, что файл тот же,
// который был зарегистрирован во время хука удаления.
if ( __FILE__ != WP_UNINSTALL_PLUGIN )
return;
# Раскомментируйте следующую строку, чтобы увидеть функцию в действии
# exit( var_dump( $_GET ) );
}
}
(2) Обновления плагинов
Если вы пишете плагин, у которого есть собственная таблица в БД или опции, могут возникнуть ситуации, когда вам нужно изменить или обновить что-то.
К сожалению, пока нет возможности выполнить что-то при установке или обновлении плагина/темы. К счастью, есть обходной путь: подключите пользовательскую функцию к пользовательской опции (да, это некрасиво — но работает).
function prefix_upgrade_plugin()
{
$v = 'plugin_db_version';
$update_option = null;
// Обновление до версии 2
if ( 2 !== get_option( $v ) )
{
if ( 2 < get_option( $v ) )
{
// Функция обратного вызова должна вернуть true при успехе
$update_option = custom_upgrade_cb_fn_v3();
// Обновляем опцию только в случае успеха
if ( $update_option )
update_option( $v, 2 );
}
}
// Обновление до версии 3, выполняется сразу после обновления до версии 2
if ( 3 !== get_option( $v ) )
{
// Повторно запускаем с начала, если предыдущее обновление не удалось
if ( 2 < get_option( $v ) )
return prefix_upgrade_plugin();
if ( 3 < get_option( $v ) )
{
// Функция обратного вызова должна вернуть true при успехе
$update_option = custom_upgrade_cb_fn_v3();
// Обновляем опцию только в случае успеха
if ( $update_option )
update_option( $v, 3 );
}
}
// Возвращаем результат функции обновления, чтобы можно было проверить успех/неудачу/ошибку
if ( $update_option )
return $update_option;
return false;
}
add_action('admin_init', 'prefix_upgrade_plugin' );
Эта функция обновления не самый красивый/хорошо написанный пример, но, как уже говорилось: это пример, и техника работает хорошо. Я улучшу это в будущих обновлениях.

Это здорово, НО что мне действительно нужно знать, так это то, что я должен включить в свой метод деактивации... например, должен ли я удалять свои таблицы в базе данных или оставлять их на случай, если пользователь передумает и снова активирует плагин?

Насчёт "НО": Я упомянул, что есть 3 метода. Один для активации, один для временной деактивации и один для удаления. По моему мнению, "uninstall" означает "Удали меня и всё, что я сделал", в то время как "deactivate" — это временное состояние, которое может быть отменено. Но: Смотрите обновление. Я добавил комментарии по вашему вопросу + дополнил его некоторыми рекомендациями по разработке.

А, теперь я понимаю. Просто вопрос: когда вызывается uninstalled? Когда файлы удаляются??

Хм. Хороший вопрос. plugin.php
— это файл, который содержит register_uninstall_hook
и загружается вместе с ранними файлами в wp-settings.php (это основной файл, на который всегда стоит смотреть). Я обновил вопрос с docblocks из ядра.

Я также добавил проверку на константу WP_UNINSTALL_PLUGIN
, которая, по сути, является именем файла, так что вы можете хранить свои функции удаления в отдельном файле и избежать выполнения других функций, находящихся в файлах вашего плагина.

Для будущих читателей: Я удалил docBlocks, так как они были из WP 3.2.1. Вы всё ещё можете просмотреть правки этого вопроса, чтобы увидеть основные функции.

Но я всё равно не понимаю, когда происходит "удаление". При удалении плагина? При деактивации?

@OneTrickPony Будет вызвано, когда пользователь нажимает на ссылку удаления, которая запускает процесс самостоятельного удаления плагина. Я обновил ответ.

Я не видел такой ссылки. Предполагается, что она должна отображаться рядом с (де)активацией/редактированием/удалением?

@OneTrickPony Хорошо, я не могу больше обновлять, так как наткнулся на вики сообщества, но ответ на вопрос "что происходит при удалении" звучит так: Функция register_uninstall_hook()
добавляет файл удаления и callback-функцию в опцию 'uninstall_plugins'
в таблице Options базы данных. Затем это вызывается через функцию uninstall_plugin($name)
, которая вызывается функцией delete_plugin($array_of_plugins)
. Последняя функция запускается в зависимости от действия, установленного в /wp-admin/plugin.php
, когда (в switch
) case равен 'delete-selected'
. Это означает, что установлен $_REQUEST['verify-delete']
. Понятно? :)

Подведем итог: В админке /wp-admin/plugin.php » isset( $_REQUEST['verify-delete'] )
» delete_plugin($array_of_plugins)
» uninstall_plugin($name)
» update_option('uninstall_plugins',$array)
» define('WP_UNINSTALL_PLUGIN')
И include WP_PLUGIN_DIR.'/'.dirname($file).'/uninstall.php'

WP_UNINSTALL_PLUGIN определяется только если в папке плагина найден файл uninstall.php. Если вы используете метод с хуком удаления, а не метод с uninstall.php, константа не будет определена. Это видно при изучении исходного кода. На момент написания этого ответа текущей была версия: http://core.trac.wordpress.org/browser/tags/3.2.1/wp-admin/includes/plugin.php#L804

Ты прав. В любом случае через неделю я пересмотрю этот ответ и выложу репозиторий на GitHub, который будет проще поддерживать.

Примечание: Я изучил это и оставил комментарий к пул-реквесту, который я сделал. Обновления будут позже.

Я проверяю код, основанный на классовом подходе, и стандарты кода VIP вызывают у меня проблемы с этими глобальными переменными $_REQUEST
, утверждая, что они не санированы. Есть идеи, как это обойти?

@aendrew Они используются только в check_admin_referer()
. Их не нужно санировать, потому что ядро само этого не делает и в любом случае сравнивает с несанированными значениями $_REQUEST
. Но если они начнут ныть как маленькие девочки из-за этого, просто используйте filter_var()
или esc_attr()
.

@kaiser Так я и думал. На ваш ответ даже ссылаются в официальной документации WP, так что, вероятно, это не проблема. Спасибо, что ответили!

@kaiser Хотя, просто к сведению — phpcs
всё равно выдаёт ту же ошибку, даже если обернуть $_REQUEST в filter_var()
или esc_attr()
. Похоже, это скорее проблема самого phpcs
, чем что-то ещё...

Вам не следует проверять WP_UNINSTALL_PLUGIN в функции обратного вызова, если используете wp_register_uninstall_hook, это нужно делать только при использовании uninstall.php

Я не на 100% уверен, что вариант (c) работает в версии 4.2.4 (и возможно в более ранних). В моих тестах метод on_activation()
никогда не вызывался. Вместо этого в логах появлялось PHP-предупреждение: call_user_func_array() expects parameter 1 to be a valid callback, class 'WCM_Setup_Demo_File_Inc' not found in /path/to/wp-includes/plugin.php on line 496
Это выглядит логично, потому что включаемый файл не подключается до тех пор, пока плагин не станет активным (так как файл подключается в __construct()
, который выполняется только на хуке plugins_loaded

@kaiser В этом ответе вы предлагаете проверять __FILE__ != WP_UNINSTALL_PLUGIN
, однако на самом деле достаточно проверить, определена ли константа. Вам не нужно проверять её значение, и более того, оно не всегда будет корректным.

@J.D. Я не знал об этой ошибке при массовом удалении. Причина, по которой я проверяю, проста: убедиться, что правильный плагин вызывает это. После 5 лет я уже не совсем уверен в своих изначальных намерениях. Что касается вашей ссылки на Trac: возможно, вам стоит пересмотреть проблему, с которой вы столкнулись: массовое удаление не работает. Понижение константы может быть не лучшим решением. На мой взгляд, все равно должен быть способ определить, какой именно плагин удаляется.

Код удаления содержит 2 ошибки.
1. Как говорит @paul, WP_UNINSTALL_PLUGIN не определена.
2. И check_admin_referer( 'bulk-plugins' )
завершается неудачей, блокируя удаление.
"Лучше перестраховаться" звучит хорошо, но изменения в ядре WP могут вызывать ошибки.
Использование register_uninstall_hook()
вместо uninstall.php
означает, что нужно быть осторожным, в том числе в любом __construct()
. Если все статично, возможно, проблем не будет.
В комментариях упоминался https://tommcfarlin.com/wordpress-plugin-boilerplate/
Это более современное решение, и в нем нет current_user_can
, check_admin_referer
или register_uninstall_hook
.

Для проверки текущей системы на наличие необходимых функций, таких как версия PHP или установленные расширения, можно использовать следующий код:
<?php # -*- coding: utf-8 -*-
/**
* Plugin Name: T5 Проверка требований плагина
* Description: Проверка версии PHP и установленных расширений
* Plugin URI:
* Version: 2013.03.31
* Author: Fuxia Scholz
* Licence: MIT
* License URI: http://opensource.org/licenses/MIT
*/
/*
* Не запускать на каждой странице, достаточно страницы плагинов.
*/
if ( ! empty ( $GLOBALS['pagenow'] ) && 'plugins.php' === $GLOBALS['pagenow'] )
add_action( 'admin_notices', 't5_check_admin_notices', 0 );
/**
* Проверяет текущую систему на соответствие требованиям плагина.
*
* @return array Ошибки или пустой массив
*/
function t5_check_plugin_requirements()
{
$php_min_version = '5.4';
// см. 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[] = "Ваш сервер использует PHP версии $php_current_version, но
для работы этого плагина требуется как минимум PHP $php_min_version. Пожалуйста, обновите PHP.";
foreach ( $extensions as $extension )
if ( ! extension_loaded( $extension ) )
$errors[] = "Для работы этого плагина необходимо установить расширение $extension.";
return $errors;
}
/**
* Вызывает t5_check_plugin_requirements() и деактивирует плагин при наличии ошибок.
*
* @wp-hook admin_notices
* @return void
*/
function t5_check_admin_notices()
{
$errors = t5_check_plugin_requirements();
if ( empty ( $errors ) )
return;
// Подавить уведомление "Плагин активирован".
unset( $_GET['activate'] );
// Название этого плагина
$name = get_file_data( __FILE__, array ( 'Plugin Name' ), 'plugin' );
printf(
'<div class="error"><p>%1$s</p>
<p><i>%2$s</i> был деактивирован.</p></div>',
join( '</p><p>', $errors ),
$name[0]
);
deactivate_plugins( plugin_basename( __FILE__ ) );
}
Проверка для PHP 5.5:

Немного запутался, то есть здесь нет вызова register_activation_hook
— почему бы его не использовать? Также сработает ли это до или после register_activation_hook
и будет ли register_activation_hook
срабатывать, даже если проверка выше не пройдёт?

Понял — но если плагин активируется вне страницы плагина (например, как часть зависимостей темы), то ваши проверки будут пропущены, верно? Я попробовал переместить add_action( 'admin_notices', 't5_check_admin_notices', 0 );
в хук активации, и плагин активируется без выполнения проверок...

@kaiser уже объяснил, как работает хук активации, я хотел показать альтернативу. Если плагин не активирован через страницу плагинов, может возникнуть фатальная ошибка, да. Этот подход не может работать на хуке активации без серьезной переработки, потому что этот хук срабатывает после admin_notices
.

Собственно, только что наткнулся на простой способ: http://stackoverflow.com/a/13927297/362445

Думаю, это должно сработать: `register_activation_hook( FILE, function() { add_option('Activated_Plugin','Plugin-Slug'); });
add_action('admin_init', 'load_plugin'); function load_plugin() { if ( ! current_user_can( 'activate_plugins' ) ) return; if(is_admin()&&get_option('Activated_Plugin')=='Plugin-Slug') { delete_option('Activated_Plugin'); add_action( 'admin_notices', 't5_check_admin_notices', 0 ); } }`
