Nell’area admin di WordPress c’è una pagina ‘segreta‘, non esiste in tale area un link diretto, ma è accessibile solo tramite il suo url ‘/wp-admin/options.php
‘, dove è possibile consultare e aggiornare le opzioni memorizzate nella wp_options
table del database, ma in cui non è possibile rimuoverle.
Personalmente, più volte, nel corso dello sviluppo di plugins, mi è capitato di ‘inquinare‘ la suddetta table con nuove entries da rimuovere durante e/o alla fine del lavoro. Oppure, certi plugins quando vengono disinstallati non cancellano le proprie opzioni. Ovviamente per fare un po’ di pulizia si può procedere in vari modi, dall’uso di SQL
a quello della funzione delete_option
() della Options API, ma l’operazione è sicuramente più veloce e pratica se eseguita all’interno della pagina delle opzioni, specialmente se già la si consulta abitualmente durante lo sviluppo del plugin.
Comunque, a prescindere dalla concreta utilità nel proprio workflow di sviluppo, il plugin presentava l’opportunità di esercitare varie tecniche, che nel seguito esamineremo, e così mi sono messo al lavoro;)
Features e utilizzo
Per rendere più agevole l’accesso alla pagina delle opzioni Il plugin aggiunge un link ad essa all’admin Toolbar:
Nella pagina compare un ‘Delete‘ link accanto ad ogni opzione:
Cliccando sul link viene richiesta per precauzione una conferma a procedere:
Se confermiamo l’operazione, an Ajax spinner è visualizzato per la durata della transazione Ajax:
Al termine di quest’ultima, se la cancellazione è andata a buon fine, la riga della HTML table dell’opzione scompare; se invece si verifica un errore nella transazione, lo spinner è sostituito con un generico messaggio di errore:
Requisiti
Il plugin richiede una versione di WordPress uguale o superiore alla 3.5. Inoltre, usando Ajax, JavaScript deve essere abilitato nel browser.
Download
Il plugin è scaricabile dal repository GitHub.
Installazione
Niente di nuovo qui, è il procedimento standard di installazione e attivazione plugins.
Lo sviluppo del plugin
Nel seguito useremo dei brani estratti dal codice del plugin che può essere consultato, scaricato, modificato e quant’altro sul relativo repository GitHub. Più precisamente, per evitare discrepanze tra i brani seguenti e le modifiche ai sorgenti sul repository successive alla stesura di questo articolo, essi si riferiscono al branch article.
All’interno della directory del plugin ci sono due sub-directories php
e js
e il file ‘master’ del plugin(plugin.php
), con l’intestazione con i meta data richiesti da WP per l’identificazione del plugin.
La logica del plugin è implementata nelle classi definite nei files presenti nella dir php
, così plugin.php si limita ad includere tali files e ad instanziare l’oggetto della classe mgDeleteOptions
per dare il ‘via alle danze‘.
Il nucleo del codice lato server del plugin risiede nella classe DeleteOptions
(mg è un prefisso per evitare potenziali conflitti con altri identificatori dichiarati da altri plugin attivi). Essa eredita alcune funzionalità base dalla clase DeleteOptionsBase
, come la computazione del prefisso del plugin(ancora per scopi di namespacing), dei paths e urls del plugin, e un metodo helper per registrare i metodi della classe per gli action hooks.
La registrazione di tali hooks avviene nel construttore di DeleteOptions
:
function __construct() { if (!is_admin()) return; parent::__construct(array()); $this->wp_ajax_action = $this->plugin_prefix . 'delete'; $this->nonce_action_string = $this->plugin_prefix . 'delete'; $this->add_action('admin_bar_menu', 'on_admin_bar_menu'); $this->add_action('load-options.php', 'inject_js'); $this->add_action("wp_ajax_{$this->wp_ajax_action}", 'on_ajax_delete'); }
Come si vede dal codice, tre sono gli hooks utilizzati:
‘admin_bar_menu’ per registrare tramite la Toolbar API il link alla pagina delle opzioni:
function on_admin_bar_menu($wp_admin_bar) { $wp_admin_bar->add_menu(array( 'id' => 'mg_wp_delete_options_hidden_page', 'title' => 'WP Options', 'href' => admin_url('options.php'), 'parent' => 'top-secondary' ) ); }
Tramite il methodo add_menu dell’oggetto $wp_admin_bar
aggiungiamo il link all’url della pagina: admin_url('options.php')
. Da notare l’argomento top-secondary
assegnato al parametro parent
per visualizzare il link nella parte destra dell’admin bar.
Ricorriamo all’action hook load-options.php
invece per caricare il file JavaScript solamente quando serve, cioè solo quando viene richiesta la pagina options.php
:
function inject_js() { $js_handle = $this->plugin_prefix . 'js'; wp_enqueue_script( $js_handle, "{$this->url['js']}script.js", array('jquery'), '', true ); $params = array( 'ajaxEndpoint' => admin_url('admin-ajax.php'), 'wpAjaxAction' => $this->wp_ajax_action, 'ajaxSpinnerUrl' => admin_url('images/wpspin_light.gif'), 'yesBtnUrl' => admin_url('images/yes.png'), 'noBtnUrl' => admin_url('images/no.png'), 'nonce' => wp_create_nonce($this->nonce_action_string) ); wp_localize_script($js_handle, $this->plugin_prefix . 'args', $params); }
Dopo aver richiesto l’inclusione dello script con wp_enqueue_script()
, registriamo dei parametri di configurazione per esso con wp_localize_script()
. Il codice Js potrà accedere a questi valori generati del server tramite l’oggetto denominato $this->plugin_prefix . 'args'
. La key nonce
sarà esaminata più tardi quando arriveremo alla sezione sulla security del plugin. Le altre keys sono per accedere all’endpoint Ajax di WP e per passare allo script jQuery gli urls di alcune immagini usate dall’admin UI di WP, così da ottenere rapidamente un look’n’feel nativo.
Infine, on_ajax_delete
è l’handler per la richiesta Ajax per cancellare un opzione. Lo vedremo dopo aver descritto lo script client-side.
Notare che la funzione costruttore ritorna immediatamente se non si è in admin per evitare un inutile setup.
Ora passiamo ad esaminare il lato client di questo plugin Ajax.
Per accedere all’oggetto jQuery
con l’usuale identificatore $
e per attendere che il DOM sia ‘pronto all’uso’. Il codice è opportunamente racchiuso nel seguente blocco:
(function($) { $(function() { ... }); })(jQuery);
La prima cosa che facciamo è aggiungere i button links per cancellare le opzioni:
$('tr').append( '<td>' + '<span class="panel delete"><a class="btn" href="#">Delete</a></span>' + '<span class="panel confirm" style="display: none;"><a class="yes" title="Confirm" href="#"><img alt="Confirm" src="' + mgdeleteoptions_args.yesBtnUrl + '"></a> <a class="no" title="Cancel" href="#"><img alt="Cancel" src="' + mgdeleteoptions_args.noBtnUrl + '"></a></span>' + '<span class="panel processing" style="display: none;"><img src="' + mgdeleteoptions_args.ajaxSpinnerUrl + '"></span>' + '<span class="panel error" style="display: none;"><a class="ok" href="#" style="color: red;">Error</a></span>' + '</td>' );
Ad ogni riga <tr>
della <table>
delle opzioni aggiungiamo una nuova cella <td>
con quattro nodi DOM che rappresentano i quattro stati in cui si può trovare l’interazione con l’utente:
- Delete button link
- Confirm
- Ajax activity indicator(lo spinner)
- Possibile messaggio di errore
Inizialmente solo il nodo corrispondente al delete button è visibile. I nodi vengono nascosti e mostrati a seconda dello stato dell’interfaccia(vedi codice seguente).
Notare l’uso della variabile mgdeleteoptions_args
: è l’oggetto generato dal codice PHP precedente e passato allo script con la wp_localize_script()
.
Adesso registriamo una serie di event handlers che stanno alla base dell’interazione con l’utente.
Usiamo il metodo .on()
dell’oggetto jQuery
, che dalla versione 1.7 della libreria fornisce tutte le funzionalità necessarie alla registrazione degli events handlers. Questi , utilizzando la feature degli Eventi Delegati
di .on()
, sono ‘attached’ al DOM node corrispondente al <body>
e vengono invocati solo quando gli eventi in questione(solo ‘click’ nel nostro caso) hanno il loro punto di origine in uno specifico sotto-albero del DOM. Per esempio, per mostrare il messaggio di conferma quando l’utente clicca sul delete button link:
$('body') .on('click', '.panel.delete .btn', function(e) { $(e.target).closest('.panel').hide().siblings('.panel.confirm').show(); return false; })
Tutti gli handlers sono registrati in cascata sul body con il method chaining fornito da jQuery.
Per mostrare e nascondere i vari ‘pannelli’ dell’interfaccia utilizziamo il metodo .show()
ed il suo alter-ego .hide()
. I pannelli vengono individuati con alcuni metodi di jQuery per ‘navigare’ nell’albero del DOM, quali .closest()
, .siblings()
e .parent()
.
Ora esaminiamo il codice che effettua la richiesta Ajax al server:
.on('click', '.panel.confirm .yes', function(e) { var td = $(e.target).closest('.panel').parent(), tr = td.parent() ; var option_name = td.prev().children().first().attr('name'); var processingPanel; $.ajax({ url: mgdeleteoptions_args.ajaxEndpoint, type: 'POST', data: { action: mgdeleteoptions_args.wpAjaxAction, option_name: option_name, _wpnonce: mgdeleteoptions_args.nonce }, dataType: 'json', beforeSend: function() { processingPanel = $(e.target).closest('.panel').hide().siblings('.panel.processing'); processingPanel.show(); } }) .done(function(response) { if (!response.success) processingPanel.hide().siblings('.panel.error').show(); else tr.hide(500, function() { tr.remove(); }); }) .fail(function() { processingPanel.hide().siblings('.panel.error').show(); }) ; return false; })
Per prima cosa individuiamo la cella TD
e la riga TR
corrispondente all’opzione che l’utente desidera rimuovere. Notare l’uso di $(e.target)
per risalire all’elemento DOM ove è originato il click.
Poi, navigando nell’albero alla cella precedente, recuperiamo, per mezzo dell’attributo name dell’input o della textarea che visualizzano ilvalore corrente dell’opzione, il nome dell’opzione, cioè il valore del campo option_name nella tabella wp_options
.
A questo punto siamo pronti ad avviare la transazione Ajax, utilizzando a tal scopo il metodo jQuery.ajax()
:
var processingPanel; $.ajax({ url: mgdeleteoptions_args.ajaxEndpoint, type: 'POST', data: { action: mgdeleteoptions_args.wpAjaxAction, option_name: option_name, _wpnonce: mgdeleteoptions_args.nonce }, dataType: 'json', beforeSend: function() { processingPanel = $(e.target).closest('.panel').hide().siblings('.panel.processing'); processingPanel.show(); } }) .done(function(response) { if (!response.success) processingPanel.hide().siblings('.panel.error').show(); else tr.hide(500, function() { tr.remove(); }); }) .fail(function() { processingPanel.hide().siblings('.panel.error').show(); }) ;
Effettuiamo una richiesta HTTP POST
all’endpoint Ajax di WP, il cui url ci è stato passato dal server con la wp_localize_script()
. Nel payload del POST
passiamo l’action hook, il nome dell’opzione da cancellare e il valore del nonce(vedi seguito).
Registriamo un callback per l’evento beforeSend
per attivare lo spinner Ajax come feedback visivo per l’utente, per informarlo che la transazione è in corso.
Infine, utilizzando i metodi dell’interfaccia Premise
implementata dall’oggetto jqXHR
ritornato dalla jQuery.ajax()
, implementiano i due possibili esiti della transazione: successo oppure errore.
Nel primo caso, nascondiamo la riga TR
dell’opzione cancellata con un’animazione al fine di dare all’utente la possibilità di percepire la rimozione; dopodichè, rimoviamo dal DOM tree la riga, per evitare che l’opzione venga ricreata nel database se l’utente dovesse cliccare sul bottone Update della pagina opzioni.
Nel secondo caso, visualizziamo il pannello di errore per avvertire che la transazione non è andata a buon fine.
Ritorniamo brevemente sul server per dare un’occhiata all’handler Ajax:
function on_ajax_delete() { $ok = current_user_can('manage_options') && check_ajax_referer($this->nonce_action_string, '_wpnonce', false) && delete_option($_POST['option_name']) ; if (!$ok) wp_send_json_error(); else wp_send_json_success(); }
Dopo alcuni test di sicurezza, che esamineremo nel prossimo paragrafo, la rimozione dell’opzione dalla table wp_options
avviene con l’invocazione della funzione delete_option()
. Dopodiché generiamo ed inviamo al client la risposta HTTP della transazione Ajax, con un paylod JSON, con due helper functions fornite da WordPress a partire dalla versione 3.5: wp_send_json_success() e wp_send_json_error() .
Infine, diamo una veloce occhiata alle precauzioni prese per evitare problemi di sicurezza.
Nell’ultimo brano di codice, controlliamo l’autorizzazione(e quindi implicitamente l’autenticazione) del client che effettua la richiesta con current_user_can()
. Poi verifichiamo la reale intenzione della richiesta(Cross Site Request Forgery) controllando il valore del nonce inviato, grazie alla funzione WP check_ajax_referer()
.
Conclusione
Ok, siamo giunti alla fine di questa overview sul funzionamente di questo plugin. Spero sia stato interessante. Concludiamo menzionando due plugins per la ‘pulizia’ delle opzioni WP:
- http://wordpress.org/extend/plugins/options-optimizer/ , che oltre al controllo dell’autoloading delle opzioni, permette di cancellare manualmente le ‘opzioni di scarto’
- http://wordpress.org/extend/plugins/clean-options/ , che cerca di identificare automaticamente le opzioni orfane, cioè quelle che, a seguito di una scansione del codice, non risultano ‘citate’.