Ajouter plusieurs répertoires de plugins

39

La tâche

Vous pouvez enregistrer des répertoires de thèmes supplémentaires en utilisant register_theme_directory()pour votre installation WP. Malheureusement, le noyau ne fournit pas la même fonctionnalité pour les plugins. Nous avons déjà MU-Plugin, Drop-Ins, Plugins et Thèmes. Mais nous avons besoin de plus pour une meilleure organisation des fichiers.

Voici la liste des tâches à accomplir:

  • Ajouter un répertoire de plugin supplémentaire
  • Pour chaque répertoire de plugin, un nouvel "onglet" est nécessaire, comme indiqué ici [1]
  • Le répertoire supplémentaire aurait les mêmes fonctionnalités que le répertoire du plugin par défaut

Qu'y a-t-il pour vous?

La meilleure et la plus complète des réponses se verra attribuer une prime.


[1] Onglet supplémentaire pour un nouveau dossier / répertoire de plugin

kaiser
la source
3
Étant donné que la structure de répertoires est assez liée aux constantes de répertoires, j'ai des doutes sur le fait que le faire au niveau du système de fichiers est pratique (sans l'adoption fondamentale). La couche d'organisation virtuelle dans admin peut être plus facile à réaliser au niveau extension.
Rarst
@Rarst Ce qui ne devrait pas vous empêcher d'ajouter vos pensées :)
kaiser
Ce serait une excellente fonctionnalité.
ltfishie
La fonctionnalité sonne bien. Il vous suffit de faire l’ingénierie inverse, de déterminer comment procéder (de la même manière que WP), puis de soumettre un correctif aux développeurs ... vous voudrez peut-être consulter register_theme_directory () - search_theme_directories () - get_raw_theme_root () - get_theme_roots () - get_theme () - get_themes ()
Sterling Hamilton
2
Les gars: Soumettre quoi ? Ceci est une question, pas une réponse avec du code complet explicite :) FYI: Un nouveau ticket sur trac pour réécrireget_themes() dans une classe.
Kaiser

Réponses:

28

D'accord, je vais tenter un coup. Quelques limitations que j'ai rencontrées en cours de route:

  1. Il n'y a pas beaucoup de filtres dans les sous-classes de WP_List_Table, du moins aucun endroit où nous en avons besoin.

  2. En raison de ce manque de filtres, nous ne pouvons pas vraiment maintenir une liste précise des types de plugins en haut.

  3. Nous devons également utiliser des hacks JavaScript géniaux (read: dirty) pour afficher les plugins comme actifs.

J'ai enveloppé mon code d'administrateur dans une classe afin que mes noms de fonctions ne soient pas préfixés. Vous pouvez voir tout ce code ici . S'il vous plaît contribuer!

API centrale

Juste une simple fonction qui configure une variable globale qui contiendra nos répertoires de plugins dans un tableau associatif. Cela $keyva être quelque chose utilisé en interne pour chercher des plugins, etc. $direst soit un chemin complet, soit quelque chose de relatif au wp-contentrépertoire. $labelva être pour notre affichage dans la zone d'administration (par exemple, une chaîne traduisible).

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

Ensuite, bien sûr, nous devons charger les plugins. Accédez plugins_loadedtardivement et parcourez les plugins actifs, en les chargeant chacun.

Zone Admin

Mettons en place nos fonctionnalités dans une classe.

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

Nous allons nous atteler plugins_loadedtrès tôt et mettre en place les "actions" autorisées que nous utiliserons. Ceux-ci géreront l'activation et la désactivation du plug-in, car les fonctions intégrées ne peuvent pas le faire avec des répertoires personnalisés.

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

Ensuite, il y a la fonction accrochée dans load-plugins.php . Cela fait toutes sortes de choses amusantes.

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

Passons en revue cette chose à la fois. la get_pluginsméthode est une enveloppe autour d'une autre fonction. Il remplit l'attribut pluginsavec des données.

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_pluginsest une arnaque de la get_pluginsfonction intégrée sans le codé en dur WP_CONTENT_DIRet les pluginsaffaires. Fondamentalement: récupérez le répertoire depuis le $wp_plugin_directoriesglobal, ouvrez-le, recherchez tous les fichiers du plugin. Stockez-les dans la mémoire cache pour plus tard.

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

La prochaine étape consiste à activer et à désactiver les plugins. Pour ce faire, nous utilisons la handle_actionsméthode. Ceci, encore une fois, est arraché de manière flagrante au sommet du wp-admin/plugins.phpfichier principal .

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

}

Quelques fonctions personnalisées ici encore. cd_apd_activate_plugin(arraché de activate_plugin) et cd_apd_deactivate_plugins(arraché dedeactivate_plugins ). Les deux sont les mêmes que leurs fonctions "parent" respectives sans les répertoires codés en dur.

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

Et la fonction de désactivation

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

Il y a aussi la cd_apd_validate_pluginfonction, qui bien sûr, est une arnaque validate_pluginsans ordure dure.

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

D'accord, avec ça à l'écart. Nous pouvons réellement commencer à parler de la affichage de liste

Étape 1: ajoutez nos vues à la liste en haut du tableau. Ceci est fait en filtrant à l' views_{$screen->id}intérieur de notre initfonction.

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

Ensuite, la fonction accrochée passe en boucle $wp_plugin_directories. Si l'un des répertoires nouvellement enregistrés contient des plugins, nous l'inclurons dans l'affichage.

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

La première chose à faire si nous visionnons une page de répertoire de plugin personnalisée est de filtrer à nouveau les vues. Nous devons nous débarrasser de lainactive décompte, car il ne sera pas précis. Une conséquence de l'absence de filtres là où nous en avons besoin. Accrocher à nouveau ...

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

Et un rapide non réglé ...

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

Ensuite, supprimons les plugins que vous auriez autrement vus dans la liste, et remplacez-les par nos plugins personnalisés. Crochet dans all_plugins.

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

Puisque nous avons déjà configuré nos plugins et nos données (voir setup_pluginsci - dessus), la filter_pluginsméthode just (1) enregistre le nombre de tous les plugins pour plus tard, et (2) remplace les plugins dans la liste.

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

Et maintenant, nous allons tuer les actions en masse. Ceux-ci pourraient facilement être pris en charge, je suppose?

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

Les liens des actions de plug-in par défaut ne vont pas fonctionner pour nous. Nous devons donc créer nos propres actions (avec les actions personnalisées, etc.). Dans la initfonction.

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

Les seules choses qui changent ici sont (1) nous changeons les actions, (2) conservons le statut du plug-in et (3) modifions un peu les noms de 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;
}

Et enfin, nous avons juste besoin de mettre en file d'attente du JavaScript pour couronner le tout. Dans la initfonction à nouveau (tous ensemble cette fois).

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

Lors de la mise en file d'attente de notre serveur JS, nous utiliserons également wp_localize_scriptla valeur du nombre total de "tous les plugins".

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

Et bien sûr, le JS n'est que quelques astuces pour obtenir l'affichage correct des plugins active / inactive de la table de liste. Nous allons également réintégrer le nombre correct de plugins dans le Alllien.

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

Emballer

Le chargement réel de répertoires de plugins supplémentaires est assez peu excitant. Obtenir la table de liste pour afficher correctement est la partie la plus difficile. Je ne suis toujours pas complètement satisfait du résultat, mais peut-être que quelqu'un peut améliorer le code

chrisguitarguy
la source
1
Impressionnant! Vraiment du bon travail. Je prendrai un peu de temps la fin de semaine pour étudier votre code. Note: Il y a une fonction __return_empty_array().
fuxia
Merci! Les commentaires sont toujours les bienvenus. Incorporé la __return_empty_arrayfonction!
chrisguitarguy
1
Vous devriez rassembler une liste de tous les endroits où un simple filtre de base vous aurait sauvé une fonction distincte. Et ensuite… soumettez un ticket Trac.
fuxia
C'est vraiment génial. Ce serait encore plus cool si nous pouvions créer une bibliothèque à l'intérieur d'un thème (voir mon commentaire sur Github: github.com/chrisguitarguy/WP-Plugin-Directories/issues/4 )
julien_c
1
+1 Je ne croyais pas avoir manqué cette réponse - excellent travail! Je vais regarder votre code plus en détail au cours du week-end :). @Julien_c - pourquoi voudriez-vous utiliser cela dans un thème?
Stephen Harris
2

Personnellement, je n'ai aucun intérêt à modifier l'interface utilisateur, mais j'aimerais une structure de système de fichiers plus organisée, pour plusieurs raisons.

À cette fin, une autre approche consisterait à utiliser des liens symboliques.

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

Vous pouvez configurer vos plugins personnalisés dans plugins-custom, qui pourraient faire partie du référentiel de contrôle de version de votre projet.

Vous pouvez ensuite installer des dépendances tierces dans plugins-external(via les sous-modules Composer, Git ou ce que vous préférez).

Ensuite, vous pouvez avoir un simple script Bash ou une commande WP-CLI qui analyse les répertoires supplémentaires et crée un lien symbolique dans plugins pour chaque sous-dossier trouvé.

pluginsserait toujours encombré, mais cela n’aurait pas d’importance parce que vous n’auriez besoin que d’interagir avec plugins-customet plugins-external.

La mise à l'échelle de nrépertoires supplémentaires suivrait le même processus que les deux premiers.

Ian Dunn
la source
-3

Vous pouvez également utiliser COMPOSER avec un chemin de répertoire personnalisé défini pour pointer vers le dossier wp-content. Si ce n’est pas une réponse directe à votre question est une nouvelle façon de penser wordpress, passez au compositeur avant qu’il ne vous mange.

Franzscisco Mai
la source
Fait de passer à Compositeur il y a longtemps. Veuillez rechercher la date de cette question. En dehors de cela: ce n'est pas vraiment une réponse. Peut-être montrer comment mettre cela en place?
Kaiser