Добавить несколько каталогов плагинов

39

Задание

Вы можете зарегистрироваться и добавить дополнительные каталоги тем, используя register_theme_directory()для вашей установки WP. К сожалению, ядро ​​не предоставляет такую ​​же функциональность для плагинов. У нас уже есть MU-плагин, Drop-Ins, плагины и темы. Но нам нужно больше для лучшей организации файлов.

Вот список задач для достижения:

  • Добавить дополнительный каталог плагинов
  • Для каждого каталога плагинов необходима новая «вкладка», как показано здесь [1]
  • Дополнительный каталог будет иметь ту же функциональность, что и каталог плагинов по умолчанию.

Что там для тебя?

Лучший и самый полный ответ будет награжден за вознаграждение.


[1] Дополнительная вкладка для новой папки / каталога плагинов

кайзер
источник
3
Поскольку структура каталогов довольно сильно связана с константами каталогов, я сомневаюсь, что делать это на уровне файловой системы целесообразно (без использования ядра). Виртуальный уровень организации в администрировании может быть проще достичь на уровне расширения.
Rarst
@Rarst Который не должен мешать вам добавлять свои мысли :)
kaiser
Это было бы отличной особенностью.
ltfishie
Функция звучит хорошо. Просто нужно перепроектировать ядро, выяснить, как это должно быть сделано (путь WP), а затем отправить патч разработчикам ... вы бы хотели посмотреть на register_theme_directory () - search_theme_directories () - get_raw_theme_root () - get_theme_roots () - get_theme () - get_themes ()
Стерлинг Гамильтон
2
Ребята: Отправить что ? Это вопрос, а не ответ с полным кодом :) FYI: Новый билет на trac для переписыванияget_themes() в класс.
Кайзер

Ответы:

28

Ладно, я попробую. Некоторые ограничения, с которыми я столкнулся на этом пути:

  1. В подклассах WP_List_Table не так много фильтров, по крайней мере, там, где они нам нужны.

  2. Из-за отсутствия фильтров мы не можем поддерживать точный список типов плагинов в верхней части.

  3. Мы также должны использовать некоторые замечательные (читай: грязные) хаки JavaScript для отображения плагинов как активных.

Я обернул свой код области администратора внутри класса, поэтому имена моих функций не имеют префикса. Вы можете увидеть весь этот код здесь . Пожалуйста, внесите свой вклад!

Центральный API

Просто простая функция, которая устанавливает глобальную переменную, которая будет содержать наши каталоги плагинов в ассоциативном массиве. Будет $keyиспользоваться для извлечения плагинов и т. Д. $dir- это либо полный путь, либо что-то относительно wp-contentкаталога. $labelбудет для нашего отображения в области администратора (например, переводимая строка).

<?php
function register_plugin_directory( $key, $dir, $label )
{
    global $wp_plugin_directories;
    if( empty( $wp_plugin_directories ) ) $wp_plugin_directories = array();

    if( ! file_exists( $dir ) && file_exists( trailingslashit( WP_CONTENT_DIR ) . $dir ) )
    {
        $dir = trailingslashit( WP_CONTENT_DIR ) . $dir;
    }

    $wp_plugin_directories[$key] = array(
        'label' => $label,
        'dir'   => $dir
    );
}

Тогда, конечно, нам нужно загрузить плагины. Задержитесь plugins_loadedпоздно и пройдитесь по активным плагинам, загружая каждый.

Административная зона

Давайте настроим нашу функциональность внутри класса.

<?php
class CD_APD_Admin
{

    /**
     * The container for all of our custom plugins
     */
    protected $plugins = array();

    /**
     * What custom actions are we allowed to handle here?
     */
    protected $actions = array();

    /**
     * The original count of the plugins
     */
    protected $all_count = 0;

    /**
     * constructor
     * 
     * @since 0.1
     */
    function __construct()
    {
        add_action( 'load-plugins.php', array( &$this, 'init' ) );
        add_action( 'plugins_loaded', array( &$this, 'setup_actions' ), 1 );

    }

} // end class

Мы собираемся подключиться plugins_loadedочень рано и настроить разрешенные «действия», которые мы будем использовать. Они будут обрабатывать активацию и деактивацию плагинов, поскольку встроенные функции не могут делать это с пользовательскими каталогами.

function setup_actions()
{
    $tmp = array(
        'custom_activate',
        'custom_deactivate'
    );
    $this->actions = apply_filters( 'custom_plugin_actions', $tmp );
}

Тогда есть функция, подключенная в load-plugins.php. Это делает все виды забавных вещей.

function init()
{
    global $wp_plugin_directories;

    $screen = get_current_screen();

    $this->get_plugins();

    $this->handle_actions();

    add_filter( 'views_' . $screen->id, array( &$this, 'views' ) );

    // check to see if we're using one of our custom directories
    if( $this->get_plugin_status() )
    {
        add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
        add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
        // TODO: support bulk actions
        add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
        add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
        add_action( 'admin_enqueue_scripts', array( &$this, 'scripts' ) );
    }
}

Давайте пройдемся по одной вещи за раз. get_pluginsметод является оберткой другой функции. Он заполняет атрибут pluginsданными.

function get_plugins()
{
    global $wp_plugin_directories;
    foreach( array_keys( $wp_plugin_directories ) as $key )
    {
       $this->plugins[$key] = cd_apd_get_plugins( $key );
    }
}

cd_apd_get_pluginsэто рип из встроенной get_pluginsфункции без хардкода WP_CONTENT_DIRи pluginsбизнеса. В основном: получить каталог из $wp_plugin_directoriesглобального, открыть его, найти все файлы плагинов. Храните их в кеше на потом.

<?php
function cd_apd_get_plugins( $dir_key ) 
{
    global $wp_plugin_directories;

    // invalid dir key? bail
    if( ! isset( $wp_plugin_directories[$dir_key] ) )
    {
        return array();
    }
    else
    {
        $plugin_root = $wp_plugin_directories[$dir_key]['dir'];
    }

    if ( ! $cache_plugins = wp_cache_get( 'plugins', 'plugins') )
        $cache_plugins = array();

    if ( isset( $cache_plugins[$dir_key] ) )
        return $cache_plugins[$dir_key];

    $wp_plugins = array();

    $plugins_dir = @ opendir( $plugin_root );
    $plugin_files = array();
    if ( $plugins_dir ) {
        while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
            if ( substr($file, 0, 1) == '.' )
                continue;
            if ( is_dir( $plugin_root.'/'.$file ) ) {
                $plugins_subdir = @ opendir( $plugin_root.'/'.$file );
                if ( $plugins_subdir ) {
                    while (($subfile = readdir( $plugins_subdir ) ) !== false ) {
                        if ( substr($subfile, 0, 1) == '.' )
                            continue;
                        if ( substr($subfile, -4) == '.php' )
                            $plugin_files[] = "$file/$subfile";
                    }
                    closedir( $plugins_subdir );
                }
            } else {
                if ( substr($file, -4) == '.php' )
                    $plugin_files[] = $file;
            }
        }
        closedir( $plugins_dir );
    }

    if ( empty($plugin_files) )
        return $wp_plugins;

    foreach ( $plugin_files as $plugin_file ) {
        if ( !is_readable( "$plugin_root/$plugin_file" ) )
            continue;

        $plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false ); //Do not apply markup/translate as it'll be cached.

        if ( empty ( $plugin_data['Name'] ) )
            continue;

        $wp_plugins[trim( $plugin_file )] = $plugin_data;
    }

    uasort( $wp_plugins, '_sort_uname_callback' );

    $cache_plugins[$dir_key] = $wp_plugins;
    wp_cache_set('plugins', $cache_plugins, 'plugins');

    return $wp_plugins;
}

Следующим шагом является надоедливый бизнес по фактической активации и деактивации плагинов. Для этого мы используем handle_actionsметод. Это, опять-таки, явно сорвано с вершины wp-admin/plugins.phpфайла ядра .

function handle_actions()
{
    $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';

    // not allowed to handle this action? bail.
    if( ! in_array( $action, $this->actions ) ) return;

    // Get the plugin we're going to activate
    $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : false;
    if( ! $plugin ) return;

    $context = $this->get_plugin_status();

    switch( $action )
    {
        case 'custom_activate':
            if( ! current_user_can('activate_plugins') )
                    wp_die( __('You do not have sufficient permissions to manage plugins for this site.') );

            check_admin_referer( 'custom_activate-' . $plugin );

            $result = cd_apd_activate_plugin( $plugin, $context );
            if ( is_wp_error( $result ) ) 
            {
                if ( 'unexpected_output' == $result->get_error_code() ) 
                {
                    $redirect = add_query_arg( 'plugin_status', $context, self_admin_url( 'plugins.php' ) );
                    wp_redirect( add_query_arg( '_error_nonce', wp_create_nonce( 'plugin-activation-error_' . $plugin ), $redirect ) ) ;
                    exit();
                } 
                else 
                {
                    wp_die( $result );
                }
            }

            wp_redirect( add_query_arg( array( 'plugin_status' => $context, 'activate' => 'true' ), self_admin_url( 'plugins.php' ) ) );
            exit();
            break;
        case 'custom_deactivate':
            if ( ! current_user_can( 'activate_plugins' ) )
                wp_die( __('You do not have sufficient permissions to deactivate plugins for this site.') );

            check_admin_referer('custom_deactivate-' . $plugin);
            cd_apd_deactivate_plugins( $plugin, $context );
            if ( headers_sent() )
                echo "<meta http-equiv='refresh' content='" . esc_attr( "0;url=plugins.php?deactivate=true&plugin_status=$status&paged=$page&s=$s" ) . "' />";
            else
                wp_redirect( self_admin_url("plugins.php?deactivate=true&plugin_status=$context") );
            exit();
            break;
        default:
            do_action( 'custom_plugin_dir_' . $action );
            break;
    }

}

Пара пользовательских функций здесь снова. cd_apd_activate_plugin(сорвал с activate_plugin) и cd_apd_deactivate_plugins(сорвал с deactivate_plugins). Оба они такие же, как их соответствующие «родительские» функции без жестко закодированных каталогов.

function cd_apd_activate_plugin( $plugin, $context, $silent = false ) 
{
    $plugin = trim( $plugin );

    $redirect = add_query_arg( 'plugin_status', $context, admin_url( 'plugins.php' ) );
    $redirect = apply_filters( 'custom_plugin_redirect', $redirect );

    $current = get_option( 'active_plugins_' . $context, array() );

    $valid = cd_apd_validate_plugin( $plugin, $context );
    if ( is_wp_error( $valid ) )
        return $valid;

    if ( !in_array($plugin, $current) ) {
        if ( !empty($redirect) )
            wp_redirect(add_query_arg('_error_nonce', wp_create_nonce('plugin-activation-error_' . $plugin), $redirect)); // we'll override this later if the plugin can be included without fatal error
        ob_start();
        include_once( $valid );

        if ( ! $silent ) {
            do_action( 'custom_activate_plugin', $plugin, $context );
            do_action( 'custom_activate_' . $plugin, $context );
        }

        $current[] = $plugin;
        sort( $current );
        update_option( 'active_plugins_' . $context, $current );

        if ( ! $silent ) {
            do_action( 'custom_activated_plugin', $plugin, $context );
        }

        if ( ob_get_length() > 0 ) {
            $output = ob_get_clean();
            return new WP_Error('unexpected_output', __('The plugin generated unexpected output.'), $output);
        }
        ob_end_clean();
    }

    return true;
}

И функция деактивации

function cd_apd_deactivate_plugins( $plugins, $context, $silent = false ) {
    $current = get_option( 'active_plugins_' . $context, array() );

    foreach ( (array) $plugins as $plugin ) 
    {
        $plugin = trim( $plugin );
        if ( ! in_array( $plugin, $current ) ) continue;

        if ( ! $silent )
            do_action( 'custom_deactivate_plugin', $plugin, $context );

        $key = array_search( $plugin, $current );
        if ( false !== $key ) {
            array_splice( $current, $key, 1 );
        }

        if ( ! $silent ) {
            do_action( 'custom_deactivate_' . $plugin, $context );
            do_action( 'custom_deactivated_plugin', $plugin, $context );
        }
    }

    update_option( 'active_plugins_' . $context, $current );
}

Есть также cd_apd_validate_pluginфункция, которая, конечно же, является отрывом validate_pluginбез жестко закодированного барахла.

<?php
function cd_apd_validate_plugin( $plugin, $context ) 
{
    $rv = true;
    if ( validate_file( $plugin ) )
    {
        $rv = new WP_Error('plugin_invalid', __('Invalid plugin path.'));
    }

    global $wp_plugin_directories;
    if( ! isset( $wp_plugin_directories[$context] ) )
    {
        $rv = new WP_Error( 'invalid_context', __( 'The context for this plugin does not exist' ) );
    }

    $dir = $wp_plugin_directories[$context]['dir'];
    if( ! file_exists( $dir . '/' . $plugin) )
    {
        $rv = new WP_Error( 'plugin_not_found', __( 'Plugin file does not exist.' ) );
    }

    $installed_plugins = cd_apd_get_plugins( $context );
    if ( ! isset($installed_plugins[$plugin]) )
    {
        $rv = new WP_Error( 'no_plugin_header', __('The plugin does not have a valid header.') );
    }

    $rv = $dir . '/' . $plugin;
    return $rv;
}

Хорошо, с этим из пути. Мы действительно можем начать говорить об отображении таблицы списка

Шаг 1: добавьте наши представления в список в верхней части таблицы. Это делается путем фильтрации views_{$screen->id}внутри нашей initфункции.

add_filter( 'views_' . $screen->id, array( &$this, 'views' ) );

Тогда фактическая подключенная функция просто проходит через $wp_plugin_directories. Если в одном из недавно зарегистрированных каталогов есть плагины, мы добавим его на экран.

function views( $views )
{
    global $wp_plugin_directories;

    // bail if we don't have any extra dirs
    if( empty( $wp_plugin_directories ) ) return $views;

    // Add our directories to the action links
    foreach( $wp_plugin_directories as $key => $info )
    {
        if( ! count( $this->plugins[$key] ) ) continue;
        $class = $this->get_plugin_status() == $key ? ' class="current" ' : '';
        $views[$key] = sprintf( 
            '<a href="%s"' . $class . '>%s <span class="count">(%d)</span></a>',
            add_query_arg( 'plugin_status', $key, 'plugins.php' ),
            esc_html( $info['label'] ),
            count( $this->plugins[$key] )
        );
    }
    return $views;
}

Первое, что нам нужно сделать, если мы просматриваем пользовательскую страницу каталога плагинов, это снова отфильтровать представления. Нам нужно избавиться от inactiveсчета, потому что он не будет точным. Следствием этого является отсутствие фильтров там, где они нам нужны. Подцепить снова ...

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
}

И быстрый сброс

function views_again( $views )
{
    if( isset( $views['inactive'] ) ) unset( $views['inactive'] );
    return $views;
}

Далее, давайте избавимся от плагинов, которые вы в противном случае видели бы в таблице списка, и замените их нашими пользовательскими плагинами. Крюк в all_plugins.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
}

Поскольку мы уже настроили наши плагины и данные (см. setup_pluginsВыше), filter_pluginsметод just (1) сохраняет счетчик для всех плагинов для последующего использования, а (2) заменяет плагины в таблице списка.

function filter_plugins( $plugins )
{
    if( $key = $this->get_plugin_status() )
    {
        $this->all_count = count( $plugins );
        $plugins = $this->plugins[$key];
    }
    return $plugins;
}

А теперь мы убьем массовые действия. Полагаю, их легко можно поддержать?

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
}

Ссылки на действия плагина по умолчанию не будут работать для нас. Поэтому вместо этого нам нужно настроить свои собственные (с настраиваемыми действиями и т. Д.). В initфункции.

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
    add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
}

Единственное, что здесь изменилось: (1) мы меняем действия, (2) сохраняем статус плагина и (3) немного меняем одноразовые имена.

function action_links( $links, $plugin_file )
{
    $context = $this->get_plugin_status();

    // let's just start over
    $links = array();
    $links['activate'] = sprintf(
        '<a href="%s" title="Activate this plugin">%s</a>',
        wp_nonce_url( 'plugins.php?action=custom_activate&amp;plugin=' . $plugin_file . '&amp;plugin_status=' . esc_attr( $context ), 'custom_activate-' . $plugin_file ),
        __( 'Activate' )
    );

    $active = get_option( 'active_plugins_' . $context, array() );
    if( in_array( $plugin_file, $active ) )
    {
        $links['deactivate'] = sprintf(
            '<a href="%s" title="Deactivate this plugin" class="cd-apd-deactivate">%s</a>',
            wp_nonce_url( 'plugins.php?action=custom_deactivate&amp;plugin=' . $plugin_file . '&amp;plugin_status=' . esc_attr( $context ), 'custom_deactivate-' . $plugin_file ),
            __( 'Deactivate' )
        );
    }
    return $links;
}

И, наконец, нам просто нужно поставить в очередь немного JavaScript, чтобы завершить его. В initфункции снова (все вместе на этот раз).

if( $this->get_plugin_status() )
{
    add_filter( 'views_' . $screen->id, array( &$this, 'views_again' ) );
    add_filter( 'all_plugins', array( &$this, 'filter_plugins' ) );
    // TODO: support bulk actions
    add_filter( 'bulk_actions-' . $screen->id, '__return_empty_array' );
    add_filter( 'plugin_action_links', array( &$this, 'action_links' ), 10, 2 );
    add_action( 'admin_enqueue_scripts', array( &$this, 'scripts' ) );
}

При постановке в очередь нашего JS, мы также будем использовать, wp_localize_scriptчтобы получить значение общего количества «всех плагинов».

function scripts()
{
    wp_enqueue_script(
        'cd-apd-js',
        CD_APD_URL . 'js/apd.js',
        array( 'jquery' ),
        null
    );
    wp_localize_script(
        'cd-apd-js',
        'cd_apd',
        array(
            'count' => esc_js( $this->all_count )
        )
    );
}

И, конечно же, JS - это всего лишь несколько приятных хаков, чтобы заставить список таблиц активных / неактивных плагинов правильно отображаться. Мы также добавим правильное количество всех плагинов обратно в Allссылку.

jQuery(document).ready(function(){
    jQuery('li.all a').removeClass('current').find('span.count').html('(' + cd_apd.count + ')');
    jQuery('.wp-list-table.plugins tr').each(function(){
        var is_active = jQuery(this).find('a.cd-apd-deactivate');
        if(is_active.length) {
            jQuery(this).removeClass('inactive').addClass('active');
            jQuery(this).find('div.plugin-version-author-uri').removeClass('inactive').addClass('active');
        }
    });
});

Заворачивать

Фактическая загрузка дополнительных каталогов плагинов довольно неинтересна. Получение таблицы списка для правильного отображения является более сложной частью. Я до сих пор не совсем доволен тем, как это получилось, но, возможно, кто-то может улучшить код

chrisguitarguy
источник
1
Впечатляет! Действительно хорошая работа. В выходные я уделю время изучению вашего кода. Примечание: есть функция __return_empty_array().
Fuxia
Благодарность! Обратная связь всегда приветствуется. Включил __return_empty_arrayфункцию!
chrisguitarguy
1
Вы должны собрать список всех мест, где простой основной фильтр спас бы вас от отдельной функции. А потом ... отправьте билет Trac.
Fuxia
Это действительно здорово. Было бы еще круче, если бы мы могли сделать это как библиотеку внутри Темы (см. Мой комментарий к Github: github.com/chrisguitarguy/WP-Plugin-Directories/issues/4 )
julien_c
1
+1 Не могу поверить, что я пропустил этот ответ - отличная работа! Я посмотрю ваш код более подробно в выходные дни :). @Julien_c - зачем тебе использовать это в теме?
Стивен Харрис
2

Лично я не заинтересован в изменении пользовательского интерфейса, но мне бы хотелось иметь более организованную структуру файловой системы по нескольким причинам.

Для этого другим подходом будет использование символических ссылок.

wp-content
    |-- plugins
        |-- acme-widgets               -> ../plugins-custom/acme-widgets
        |-- acme-custom-post-types     -> ../plugins-custom/acme-custom-post-types
        |-- acme-business-logic        -> ../plugins-custom/acme-business-logic
        |-- google-authenticator       -> ../plugins-external/google-authenticator
        |-- rest-api                   -> ../plugins-external/rest-api
        |-- quick-navigation-interface -> ../plugins-external/quick-navigation-interface
    |-- plugins-custom
        |-- acme-widgets
        |-- acme-custom-post-types
        |-- acme-business-logic
    |-- plugins-external
        |-- google-authenticator
        |-- rest-api
        |-- quick-navigation-interface

Вы можете настроить свои собственные плагины plugins-custom, которые могут быть частью репозитория управления версиями вашего проекта.

Затем вы можете установить сторонние зависимости в plugins-external(через Composer, или субмодули Git, или что вы предпочитаете).

Тогда у вас может быть простой скрипт Bash или команда WP-CLI, которая сканирует дополнительные каталоги и создает символическую ссылку pluginsдля каждой найденной подпапки.

pluginsвсе равно будет загроможден, но это не будет иметь значения, потому что вам нужно будет только взаимодействовать с plugins-customи plugins-external.

Масштабирование до nдополнительных каталогов будет следовать тому же процессу, что и первые два.

Ян Данн
источник
-3

Или вы также можете использовать COMPOSER с настраиваемым путем к каталогу, указывающим на папку wp-content. Если это не прямой ответ на ваш вопрос, это новый способ мышления WordPress, перейдите к композитору, прежде чем он съест вас.

Franzscisco Mai
источник
Перешел на Composer давно. Пожалуйста, посмотрите дату этого вопроса. Помимо этого: это не совсем ответ. Может быть, показать, как на самом деле настроить это?
Кайзер