Agregar múltiples directorios de complementos

39

La tarea

Puede registrarse para agregar directorios de Temas adicionales utilizando register_theme_directory()para su instalación de WP. Lamentablemente, el núcleo no ofrece la misma funcionalidad para los complementos. Ya tenemos MU-Plugin, Drop-Ins, Plugins y Temas. Pero necesitamos más para una mejor organización de archivos.

Aquí está la lista de tareas para lograr:

  • Agregar un directorio de complementos adicional
  • Para cada directorio de complementos, se necesita una nueva "pestaña" como se muestra aquí [1]
  • El directorio adicional tendría la misma funcionalidad que el directorio del complemento predeterminado.

¿Qué hay para ti?

La mejor y más completa respuesta recibirá una recompensa.


[1] Pestaña adicional para una nueva carpeta / directorio de complementos

emperador
fuente
3
Dado que la estructura del directorio está bastante ligada a las constantes del directorio, tengo dudas de que hacer esto en el nivel del sistema de archivos sea práctico (sin la adopción del núcleo). La capa virtual de organización en administración podría ser más fácil de lograr en el nivel de extensión.
Rarst
@Rarst que no deben sostener que detrás de la adición de sus pensamientos :)
Kaiser
Esto sería una gran característica.
ltfishie
La característica suena bien. Solo necesito realizar ingeniería inversa en el núcleo, descubrir cómo se debe hacer (a la manera de WP) y luego enviar un parche a los desarrolladores ... desearía consultar register_theme_directory () - search_theme_directories () - get_raw_theme_root () - get_theme_roots () - get_theme () - get_themes ()
Sterling Hamilton
2
Chicos: ¿ Enviar qué ? Esta es una pregunta, no una respuesta con código completo :) FYI: Un nuevo boleto en trac para reescribirget_themes() a una clase.
kaiser

Respuestas:

28

Bien, voy a apuñalar esto. Algunas limitaciones que encontré en el camino:

  1. No hay muchos filtros en las subclases de WP_List_Table, al menos no donde necesitamos que estén.

  2. Debido a la falta de filtros, realmente no podemos mantener una lista precisa de tipos de complementos en la parte superior.

  3. También tenemos que usar algunos hacks de JavaScript impresionantes (léase: sucios) para mostrar los complementos como activos.

Envolví mi código de área de administración dentro de una clase, por lo que los nombres de mis funciones no tienen prefijo. Puedes ver todo este código aquí . Por favor contribuya!

API central

Solo una función simple que configura una variable global que contendrá nuestros directorios de complementos en una matriz asociativa. El $keyva a ser algo que se usa internamente en busca de plugins, etc $dires una ruta o algo lleno en relación con el wp-contentdirectorio. $labelva a ser para nuestra visualización en el área de administración (por ejemplo, una cadena traducible).

<?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
    );
}

Entonces, por supuesto, necesitamos cargar los complementos. Conéctate plugins_loadedtarde y ve a través de los complementos activos, cargando cada uno.

Área de administración

Configuremos nuestra funcionalidad dentro de una clase.

<?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

Nos conectaremos plugins_loadedmuy pronto y configuraremos las "acciones" permitidas que usaremos. Estos manejarán la activación y desactivación del complemento ya que las funciones integradas no pueden hacerlo con directorios personalizados.

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

Luego está la función enganchada load-plugins.php. Esto hace todo tipo de cosas divertidas.

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' ) );
    }
}

Veamos esa cosa a la vez. El get_pluginsmétodo, es un contenedor alrededor de otra función. Llena el atributo pluginscon datos.

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_pluginses una copia de la get_pluginsfunción incorporada sin el hardcoded WP_CONTENT_DIRy el pluginsnegocio. Básicamente: obtenga el directorio del $wp_plugin_directoriesglobal, ábralo, encuentre todos los archivos de complemento. Guárdelos en el caché para más tarde.

<?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;
}

El siguiente es el molesto negocio de activar y desactivar complementos. Para hacer esto, usamos el handle_actionsmétodo. Esto es, nuevamente, descaradamente arrancado de la parte superior del wp-admin/plugins.phparchivo central .

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;
    }

}

Un par de funciones personalizadas aquí nuevamente. cd_apd_activate_plugin(arrancado de activate_plugin) y cd_apd_deactivate_plugins(arrancado de deactivate_plugins). Ambos son los mismos que sus respectivas funciones "principales" sin los directorios codificados.

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;
}

Y la función de desactivación

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 );
}

También hay una cd_apd_validate_pluginfunción, que por supuesto, es una estafa validate_pluginsin la basura codificada.

<?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;
}

Muy bien, con eso fuera del camino. Podemos comenzar a hablar sobre la visualización de la tabla de listas

Paso 1: agregue nuestras vistas a la lista en la parte superior de la tabla. Esto se hace filtrando views_{$screen->id}dentro de nuestra initfunción.

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

Luego, la función enganchada real solo recorre el $wp_plugin_directories. Si uno de los directorios recién registrados tiene complementos, lo incluiremos en la pantalla.

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;
}

Lo primero que debemos hacer si estamos viendo una página de directorio de complementos personalizada es filtrar las vistas nuevamente. Necesitamos deshacernos del inactiveconteo porque no será preciso. Una consecuencia de que no hay filtros donde los necesitamos. Enganchar de nuevo ...

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

Y un rápido desarmado ...

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

A continuación, eliminemos los complementos que de otro modo habría visto en la tabla de lista y reemplácelos con nuestros complementos personalizados. Enganchar all_plugins.

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

Como ya configuramos nuestros complementos y datos (ver setup_pluginsarriba), el filter_pluginsmétodo solo (1) guarda el recuento de todos los complementos para más adelante, y (2) reemplaza los complementos en la tabla de la lista.

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

Y ahora mataremos las acciones masivas. Estos podrían ser fácilmente compatibles, supongo.

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' );
}

Los enlaces de acciones de complementos predeterminados no funcionarán para nosotros. Entonces, en cambio, necesitamos configurar el nuestro (con las acciones personalizadas, etc.). En la initfuncion.

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 );
}

Las únicas cosas que se cambian aquí son (1) estamos cambiando las acciones, (2) manteniendo el estado del complemento y (3) cambiando un poco los nombres nonce.

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;
}

Y finalmente, solo necesitamos poner en cola JavaScript para completarlo. En la initfunción de nuevo (todos juntos esta vez).

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' ) );
}

Al poner en cola ourJS, también usaremos wp_localize_scriptpara obtener el valor del recuento total de "todos los complementos".

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 )
        )
    );
}

Y, por supuesto, el JS es solo un buen truco para que la lista de complementos activos / inactivos de la tabla se muestre correctamente. También pegaremos el recuento correcto de todos los complementos en el Allenlace.

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');
        }
    });
});

Envolver

La carga real de directorios de complementos adicionales es bastante poco emocionante. Lograr que la tabla de la lista se muestre correctamente es la parte más difícil. Todavía no estoy completamente satisfecho con cómo resultó, pero tal vez alguien pueda mejorar el código

chrisguitarguy
fuente
1
¡Impresionante! Muy buen trabajo. Me tomaré un tiempo durante el fin de semana para estudiar tu código. Nota: hay una función __return_empty_array().
fuxia
¡Gracias! La retroalimentación siempre es bienvenida. Incorporado la __return_empty_arrayfunción!
chrisguitarguy
1
Debería recopilar una lista de todos los lugares donde un filtro central simple le hubiera guardado una función separada. Y luego ... envíe un boleto de Trac.
fuxia
Esto es realmente genial Sería aún más genial si pudiéramos esto como una biblioteca dentro de un Tema (ver mi comentario en Github: github.com/chrisguitarguy/WP-Plugin-Directories/issues/4 )
julien_c
1
+1 No puedo creer que me haya perdido esta respuesta, ¡gran trabajo! Revisaré tu código con más detalle durante el fin de semana :). @Julien_c: ¿por qué usarías esto dentro de un tema?
Stephen Harris
2

Personalmente no tengo ningún interés en modificar la interfaz de usuario, pero me encantaría un diseño de sistema de archivos más organizado, por varias razones.

Para ese fin, otro enfoque sería utilizar enlaces simbólicos.

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

Puede configurar sus complementos personalizados en plugins-custom, que podrían ser parte del repositorio de control de versiones de su proyecto.

Luego, puede instalar dependencias de terceros en plugins-external(a través de Composer, o submódulos Git, o lo que prefiera).

Entonces podría tener un simple script Bash o un comando WP-CLI que escanee los directorios adicionales y cree un enlace simbólico pluginspara cada subcarpeta que encuentre.

pluginstodavía estaría abarrotado, pero no importaría porque solo necesitarías interactuar con plugins-customy plugins-external.

Escalar a ndirectorios adicionales seguiría el mismo proceso que los dos primeros.

Ian Dunn
fuente
-3

O también puede usar COMPOSER con una ruta de directorio personalizada configurada para apuntar a la carpeta wp-content. Si no es una respuesta directa a su pregunta, es una nueva forma de pensar wordpress, pase al compositor antes de que se lo coma.

Franzscisco Mai
fuente
Terminé de mudarme a Composer hace mucho tiempo. Por favor, busque la fecha de esta pregunta. Aparte de eso: esto no es realmente una respuesta. Tal vez mostrar cómo configurar esto realmente?
kaiser