Plugin ACF v5 : créer des pages d’options

class AcfOptionsPages{
	 
	public static function hooks(){
		add_action('plugins_loaded', array(__CLASS__,'plugins_loaded'));
	}
	 
	public static function plugins_loaded(){
		
		if( function_exists('acf_add_options_page')) { 
	 
			acf_add_options_page();
			acf_add_options_sub_page('Header');
			acf_add_options_sub_page('Footer');
		 
			//Optionnel : régler qui a droit de voir la page d'options :
			acf_set_options_page_capability('edit_posts');
			
			//Optionnel : libellé de la page d'option mère dans le menu
			acf_set_options_page_menu('My options'); 
			
			//Optionnel : libellé des sous pages d'options dans le menu
			acf_update_options_page(array(
				'menu_slug'		=> 'acf-options-header',
				'menu_title'	=> 'My header'
			));
			
			//Optionnel : titre affiché dans la page d'option mère :
			acf_set_options_page_title('My options title');
			
			//Optionnel : titre affiché dans les sous page d'options :
			acf_update_options_page(array(
				'menu_slug'		=> 'acf-options-header',
				'page_title'	=> 'My header option page title'
			));
		}
		
	}
	
	 
}
AcfOptionsPages::hooks();

Gestion des roles et capabilities WordPress

<?php
/*
Plugin Name: Test WordPress roles and capabilities
Description: Basic use of WordPress roles and capabilities management API
Version: 1.0
*/

if( !class_exists('wp_test_roles_and_capabilities') ){
class wp_test_roles_and_capabilities{
	
	public static function hooks(){
		
		//Création des roles dans "admin_init" uniquement pour le débug :
		//utiliser le hook d'activation de plugin lors de l'utilisation finale,
		//comme expliqué ici : http://codex.wordpress.org/Function_Reference/add_role
		add_action('admin_init',array(__CLASS__,'create_role'));
		
		//To add / remove menu panels :
		add_action('admin_menu', array(__CLASS__,'adjust_wp_menu'), 999);
	}	
	
	public static function create_role(){
		
		//Pour le détail des roles et capabilities, voir :
		//http://codex.wordpress.org/Roles_and_Capabilities
		
		//Récupération des capabilities d'un role existant :
		//$author_role = get_role('author');
		//$author_role['capabilities']; 

		//Ajouter/supprimer une capability à un role existant :
		//$author_role->add_cap('edit_others_posts');
		//$author_role->remove_cap('edit_others_posts');
		
		//Création d'un nouveau role "Contributeur spécial" pour tester les capabilities :
		remove_role( 'special_contributor' );
		$new_role = add_role(
				'special_contributor',
				'Contributeur spécial',
				array(
						
					//Default WP "Author" role capabilities :
					'read' => true,
					'edit_posts' => true,
					'delete_posts' => true,
					'publish_posts' => true,
					'delete_published_posts' => true,
					'edit_published_posts' => true,
					'upload_files' => true,

					//Additional roles :
		
					//Droits de création de nouvelles catégories (ou hierachical custom taxonomies). 
					//Permet de masquer le lien "Add new category" et la gestion de catégorie dans l'onglet "Posts" du BO.
					//N'agit pas sur la création de tags.
					'manage_categories' => false, 
						
					//Widgets :
					//'edit_theme_options' ajoute le menu "Appearance"
					//Voir ci-dessous self::adjust_wp_menu() pour le masquage des éléments du menu "Appearance" non souhaités
					'edit_theme_options' => true, 
					
				)
		);
		
		if( $new_role == null ){
			echo 'Erreur à la création du role "Contributeur spécial"';
		}

	}
	
	public static function adjust_wp_menu(){
		global $current_user;
		
		//Pour voir les identifiants des éléments de menu et sous menu : 
		//global $menu,$submenu;
		//var_dump($menu,$submenu);
		
		//On ne laisse que le sous menu widgets du menu "Appearance" pour les utilisateurs ayant le role "Contributeur spécial" :
		if( $current_user && in_array('special_contributor', $current_user->roles) ){
			remove_submenu_page( 'themes.php', 'themes.php' );
			remove_submenu_page( 'themes.php', 'customize.php' );
			//remove_submenu_page( 'themes.php', 'widgets.php' );
			remove_submenu_page( 'themes.php', 'nav-menus.php' );
			remove_submenu_page( 'themes.php', 'custom-header' );
			remove_submenu_page( 'themes.php', 'custom-background' );
			remove_submenu_page( 'themes.php', 'theme-editor.php' );
		}
	}
}
wp_test_roles_and_capabilities::hooks();
}

Voir http://codex.wordpress.org/Roles_and_Capabilities pour le détail des roles et capabilities WordPress.

Création d’un role
Utilisation la fonction add_role() pour créer un nouveau role avec des capabilities personnalisées.

Permissions de création de catégories :
Utiliser la capability « manage_categories » pour gérer la permission d’ajout de catégorie (voir create_role() dans le plugin ci-dessus).
Elle fonctionne pour les custom taxonomies hiérarchiques (mais pas pour les taxonomies non hiérarchiques, comme les tags, pour lesquelles on peut toujours ajouter un terme depuis l’édition d’article).

Permissions de création de tags :
Dans l’édition d’articles, permettre à un utilisateur d’assigner des tags sans pouvoir en créer n’est pas possible avec l’interface par défaut de la gestion de tags wp.
> il faut remplacer la métabox de tag par une autre customisée qui ne permet pas l’ajout de tag, via un plugin.

Test et ajout de capabilities :

  • la fonction permettant de savoir si un utilisateur a une capability est current_user_can(‘capability_a_tester’) qui retourne vraie si l’utilisateur a bien la capability dans son role.
  • on peut « inventer » des capabilities : pas de fonction spécifique pour créer une capability, juste tester if(current_user_can(‘notre_capability’)) pour afficher des métabox / panels de menus / widgets seulement pour les utilisateurs ayant un role incluant la capability « notre_capability ».

Affichage/masquage d’une métabox suivant l’utilisateur :

  • si c’est une métabox créée par un plugin à nous, c’est simple : on peut créer une capability (ou utiliser une capability existante) et la tester pour voir si on affiche ou non la métabox.
  • si c’est une métabox d’un autre plugin ou métabox par défaut wp, on peut soit faire un remove_meta_box, soit faire un hack CSS si pas d’autre moyen.

Affichage/masquage d’un panel ou sous panel BO (Comme « Posts », « Settings », « Tools » …) :

  • si c’est un panel créé par un plugin à nous, c’est simple : on peut créer une capability (ou utiliser une capability existante) et la tester pour voir si on affiche ou non le panel.
  • si c’est un panel d’un autre plugin ou panel par défaut wp on utilise remove_menu_page() ou remove_submenu_page()

Affichage/masquage des widgets :
Pour maîtriser l’apparition du panel « Widgets » dans le menu : utilisation de la capability « edit_theme_options » et du masquage de sous menus (voir adjust_wp_menu() dans la classe ci-dessus)
Pour masquer des widgets spécifiques :

  • Si c’est un widget d’un plugin à nous, même principe que pour les métaboxes et panels
  • Sinon, utilisation de la fonction unregister_widget()

Squelette plugin se chargeant dans le hook plugins_loaded

<?php
/*
Plugin Name: My contents
Description: Create custom contents 
Version: 1.0
*/

if( !class_exists('MyContents') ){
class MyContents{
			
	public static function hooks(){
		add_action('plugins_loaded', array(__CLASS__,'plugins_loaded'));
	}
	
	public static function plugins_loaded(){
		self::lib_require();
	}
	
	protected static function lib_require(){
		require_once(dirname(__FILE__) .'/lib/my-content-1.php');
		require_once(dirname(__FILE__) .'/lib/my-content-2.php');
	}
	
}
MyContents::hooks();
}

Création d’un widget avec upload d’image utilisant la boite de dialogue média de WordPress

Déclaration d’une sidebar

register_sidebar( array('name'=>'My sidebar name', 'id' => "my_sidebar_id" ) );

Déclaration du widget

add_action('widgets_init', 'my_widget_init');
function my_widget_init() {
    register_widget("My_Widget"); //My_Widget is the class defined hereunder, extending WP_Widget 
}

Insertion dans le template

<?php if( dynamic_sidebar('my_sidebar_id') ): ?>
    <!-- Display something after the sidebar widgets contents -->
<?php else : ?>
    <!-- Display something if the sidebar is not defined -->
<?php endif; ?>

Le Widget

Note : javascript et styles sont inclus directement dans le template html ici, la bonne pratique serait de les déplacer dans des fichiers séparés inclus par wp_enqueue_script() / wp_enqueue_style().

<?php

//Enqueue WordPress media JS to handle image upload dialog box
add_action('admin_enqueue_scripts', 'my_widget_scripts');
function my_widget_scripts() {
    global $pagenow;
    if( $pagenow == 'widgets.php' ){
        wp_enqueue_media();
    }
}

//Create a class extending WP_Widget
class My_Widget extends WP_Widget {

    /**
     * Register widget with WordPress.
     */
    public function __construct() {
        parent::__construct(
            'my_widget', // Base ID
            'My widget', // Name
            array( 'description' => __( 'My widget description' ) ) // Args
        );
    }

    /**
     * Widget Front-end display
     */
    public function widget( $args, $instance ) {
        
        $img = $instance['img'];
        $url = $instance['url'];
        $title = $instance['title'];
       
        if( !empty($img) ){
            ?>
                <a href="<?php echo $url ?>" title="<?php echo $title ?>">
                    <div class="my-widget-img" style="background-image: url('<?php echo $img ?>');"></div>
                </a>
            <?php
        }
    }

    /**
     * Sanitize widget form values as they are saved.
     */
    public function update( $new_instance, $old_instance ) {
        $instance = array();

        $instance['img'] = strip_tags( $new_instance['img'] );
        $instance['url'] = strip_tags( $new_instance['url'] );
        $instance['title'] = strip_tags( $new_instance['title'] );

        return $instance;
    }

    /**
     * Back-end widget form.
     */
    public function form( $instance ) {
        
        $img = isset( $instance[ 'img' ] ) ? $instance[ 'img' ] : '';
        $url = isset( $instance[ 'url' ] ) ? $instance[ 'url' ] : '';
        $title = isset( $instance[ 'title' ] ) ? $instance[ 'title' ] : '';
        
        $img_field_id = $this->get_field_id('img');
        
        ?>
        
        <p>
            <label for="<?php echo $img_field_id ?>">Image :</label><br/>
            <input class="upload_image" id="<?php echo $img_field_id ?>" type="hidden" name="<?php echo $this->get_field_name('img') ?>" value="<?php echo $img ?>" />
            <input class="upload_image_button" id="<?php echo $img_field_id ?>_button" type="button" value="Charger une image" data-field-id="<?php echo $img_field_id ?>" />
            <div id="<?php echo $img_field_id ?>_img" class="upload_image_wrapper">
                <?php if( !empty($img) ):?>
                    <img src="<?php echo $img ?>" />
                    <a href="#" class="upload_image_delete" data-field-id="<?php echo $img_field_id ?>">Supprimer l'image</a>
                <?php endif ?>
            </div>
        </p>
        
        <p>
            <label for="<?php echo $this->get_field_id('url') ?>">URL du lien :</label>
            <input class="widefat" id="<?php echo $this->get_field_id('url') ?>" name="<?php echo $this->get_field_name('url') ?>" type="text" value="<?php echo esc_attr($url) ?>" />
        </p>
        
        <p>
            <label for="<?php echo $this->get_field_id('title') ?>">Message au survol de l'image :</label>
            <input class="widefat" id="<?php echo $this->get_field_id('title') ?>" name="<?php echo $this->get_field_name('title') ?>" type="text" value="<?php echo esc_attr($title) ?>" />
        </p>
        
        <style>
            .upload_image_wrapper img{ width:100% }
        </style>
        
        <script>
                //Le code suivant doit normalement être positionné dans un fichier Javascript séparé,
                //notamment dans le cas où on positionne plusieurs fois le widget dans l'interface, 
                //sinon le "jQuery(document).ready()" ci-dessous est lancé autant de fois que le nombre  
                //de widgets positionnés. Ca fonctionne car on fait des "unbind()" mais 
                //ce n'est pas optimisé.

                var upload_image_custom_uploaders = upload_image_custom_uploaders || {};
            
                jQuery(document).ready(function($){
 
                        $('.upload_image_button').unbind().click(function(e) {
 
                                e.preventDefault();
 
                                var field_id = $(this).data('field-id');
                    
                                //If the uploader object has already been created, reopen the dialog
                                if( upload_image_custom_uploaders.hasOwnProperty( field_id ) ) {
                                        upload_image_custom_uploaders[field_id].open();
                                        return;
                                }
              
                                //Extend the wp.media object
                                var upload_image_custom_uploader = wp.media.frames.file_frame = wp.media({
                                        title: 'Choisir une image',
                                        button: {
                                            text: 'Choisir cette image'
                                        },
                                        multiple: false
                                });
                    
                                upload_image_custom_uploaders[field_id] = upload_image_custom_uploader;
              
                                //When a file is selected, grab the URL and set it as the text field's value
                                upload_image_custom_uploader.on('select', function() {
                                        attachment = upload_image_custom_uploader.state().get('selection').first().toJSON();
                                        $('#'+ field_id).val(attachment.url);
                                        $('#'+ field_id +'_img').html('<img src="'+ attachment.url +'" />');
                                });
              
                                //Open the uploader dialog
                                upload_image_custom_uploader.open();
              
                        });
 
                        $('.upload_image_delete').unbind().click(function(e){
                                e.preventDefault();
                                var field_id = $(this).data('field-id');
                                $('#'+ field_id).val('');
                                $('#'+ field_id +'_img').html('');
                        });
              
                });
        </script>
        
        <?php
    }

}

Externalisation du javascript

Dans le cas où l’on souhaite positionner le script dans un fichier séparé, il faut utiliser l’event delegation de jQuery puisque le HTML est rechargé dynamiquement.
Pour cela, remplacer :

$('.upload_image_button').unbind().click(function(e) {

par :

$('#wpbody,#customize-controls').on( 'click', '.upload_image_button', function(e) {

et :

$('.upload_image_delete').unbind().click(function(e){

par :

$('#wpbody,#customize-controls').on( 'click', '.upload_image_delete', function(e){

Doc

http://codex.wordpress.org/Widgets_API
http://www.webmaster-source.com/2013/02/06/using-the-wordpress-3-5-media-uploader-in-your-plugin-or-theme/

Afficher la page 404 en cas d’erreur lors d’une redirection template_redirect

class products_settings{
	 
	public static function hooks(){
		add_action('template_redirect',array(__CLASS__,'template_redirect'));
	}

	public static function template_redirect(){
		global $wp_query;
		if( "Tests sur $wp_query->query_vars[] échouent" ){
			//Affichage de la page 404 :
			global $wp_query;
			$wp_query->set_404();
			status_header(404);
			get_template_part(404);
			exit();
		}
	}

}

products_settings::hooks();

Entête d’un plugin

/*
Plugin Name: Name Of The Plugin
Plugin URI: http://URI_Of_Page_Describing_Plugin_and_Updates
Description: A brief description of the Plugin.
Version: The Plugin's Version Number, e.g.: 1.0
Author: Name Of The Plugin Author
Author URI: http://URI_Of_The_Plugin_Author
License: A &quot;Slug&quot; license name e.g. GPL2
*/

https://codex.wordpress.org/Writing_a_Plugin

Traitements à l’installation / désinstallation d’un plugin

class produits{
	
	public static function hooks(){
		register_activation_hook(__FILE__, array(__CLASS__,'activate'));
		register_deactivation_hook( __FILE__, array(__CLASS__, 'deactivate' ) );
		register_uninstall_hook( __FILE__, array(__CLASS__, 'uninstall' ) );
	}

	public static function activate(){	

	}
	
	public static function deactivate(){		

	}
	
	public static function uninstall(){
		//The uninstall hook will be called when the user clicks on the uninstall
		//link that calls for the plugin to uninstall itself. 
		//The link won't be active unless the plugin hooks into the action.
	}
}

produits::hooks();

http://codex.wordpress.org/Function_Reference/register_activation_hook
http://codex.wordpress.org/Function_Reference/register_deactivation_hook
http://codex.wordpress.org/Function_Reference/register_uninstall_hook

Ajout de règles de réécritures

class produits{

	public static function hooks(){
		add_action('init', array(__CLASS__,'rewrite_rules'));
		add_action('template_redirect',array(__CLASS__,'template_redirect'));

		register_activation_hook( __FILE__, array( __CLASS__, 'on_activation' ) );
		register_deactivation_hook( __FILE__, array( __CLASS__, 'on_deactivation' ) );
	}

	public static function rewrite_rules() {

		add_rewrite_tag('%is_produit%','([01])');
		add_rewrite_tag('%produit_type%','([^&]+)');

		add_rewrite_rule('^produit-type/(.*?)/?$','index.php?is_produit=1&produit_type=$matches[1]','top');

	}

	public static function template_redirect(){
		global $wp_query;
		
		if( isset($wp_query->query_vars['is_produit']) && $wp_query->query_vars['is_produit'] == 1 ){
			if( $wp_query->query_vars['produit_type'] == 'le-bon-type' ){
				load_template(...);
				exit;
			}
		}
	}

	public static function on_activation() {
		self::rewrite_rules();
		flush_rewrite_rules();
	}
	
	public static function on_deactivation() {
		flush_rewrite_rules();
	}
}

produits::hooks();

Wordrpress met les règles de réécritures en cache, il faut donc faire une flush de ce cache après avoir ajouté des règles de réécritures.

Attention!
Ne pas faire un flush des règles de réécriture systématique en appelant flush_rewrite_rules() ou $wp_rewrite->flush_rules() à chaque exécution!

Appeler flush_rewrite_rules() à l’activation d’un plugin ou manuellement via le back office « Réglages » > « Permaliens » > cliquer sur « Enregistrer les modifications », comme expliqué là : Function_Reference/flush_rewrite_rules et là Rewrite_API/flush_rules.

http://codex.wordpress.org/Rewrite_API/add_rewrite_tag
http://codex.wordpress.org/Rewrite_API/add_rewrite_rule