¿Cómo puedo forzar la descarga de un archivo en el backend de WordPress?
Me gustaría agregar un botón "Click para descargar" a uno de mis plugins de WordPress, y no estoy seguro de qué hook usar. Hasta ahora, enganchar 'admin_init' a este código parece funcionar:
// Configurar los headers para forzar la descarga
header("Content-type: application/x-msdownload");
header("Content-Disposition: attachment; filename=data.csv");
header("Pragma: no-cache");
header("Expires: 0");
echo 'data';
exit();
Esto parece funcionar, pero solo quiero ver si existe alguna mejor práctica.
Gracias, Dave

Si te entiendo correctamente, deseas tener una URL similar a la siguiente cuya respuesta al navegador será el contenido que generes, es decir, tu archivo .CSV
y sin contenido generado por WordPress?
http://example.com/download/data.csv
Creo que estás buscando el hook 'template_redirect'
. Puedes encontrar 'template_redirect'
en /wp-includes/template-loader.php
, que es un archivo que todos los desarrolladores de WordPress deberían conocer; es corto y sencillo y enruta cada carga de página que no sea de administración, así que asegúrate de echarle un vistazo.
Solo añade lo siguiente al archivo functions.php
de tu tema o en otro archivo que incluyas
en functions.php
:
add_action('template_redirect','yoursite_template_redirect');
function yoursite_template_redirect() {
if ($_SERVER['REQUEST_URI']=='/downloads/data.csv') {
header("Content-type: application/x-msdownload",true,200);
header("Content-Disposition: attachment; filename=data.csv");
header("Pragma: no-cache");
header("Expires: 0");
echo 'data';
exit();
}
}
Observa la comprobación de la URL '/downloads/data.csv'
mediante la inspección de $_SERVER['REQUEST_URI']
. También observa el añadido de ,true,200
a tu llamada header()
donde estableces el Content-type
; esto es porque WordPress habrá establecido el código de estado 404
"No encontrado" porque no reconoce la URL. No hay problema, ya que el true
le dice a header()
que reemplace el 404
que WordPress había establecido y que use el código de estado HTTP 200
"OK" en su lugar.
Y así es como se ve en FireFox (Nota: la captura de pantalla no tiene un directorio virtual /downloads/
porque después de tomar y anotar la captura de pantalla pareció una buena idea añadir un directorio virtual '/downloads/'
):
(fuente: mikeschinkel.com)
ACTUALIZACIÓN
Si deseas que la descarga se maneje desde una URL que tenga el prefijo /wp-admin/
para dar al usuario la indicación visual de que está protegida por un inicio de sesión, también puedes hacerlo; a continuación se describe una forma.
Esta vez lo encapsulé en una clase llamada DownloadCSV
, y creé una "capacidad" de usuario llamada 'download_csv'
para el rol de 'administrator'
(lee sobre Roles y Capacidades aquí). Podrías simplemente aprovechar el rol predefinido 'export'
si lo prefieres, y si es así, solo busca y reemplaza 'download_csv'
con 'export'
y elimina la llamada a register_activation_hook()
y la función activate()
. Por cierto, la necesidad de un hook de activación es una de las razones por las que trasladé esto a un plugin en lugar de mantenerlo en el archivo functions.php
del tema.*
También añadí una opción de menú "Descargar CSV" en el menú "Herramientas" usando add_submenu_page()
y la vinculé a la capacidad 'download_csv'
.
Por último, elegí el hook 'plugins_loaded'
porque era el hook apropiado más temprano que podía usar. Podrías usar 'admin_init'
pero ese hook se ejecuta mucho más tarde (llamada 1130 del hook vs. la llamada 3 del hook), así que ¿por qué dejar que WordPress haga más trabajo innecesario del necesario? (Usé mi plugin Instrument Hooks para averiguar qué hook usar.)
En el hook, verifico que mi URL comience con /wp-admin/tools.php
inspeccionando la variable $pagenow
, verifico que current_user_can('download_csv')
y si eso pasa, entonces pruebo $_GET['download']
para ver si contiene data.csv
; si es así, ejecutamos prácticamente el mismo código que antes. También elimino el ,true,200
de la llamada a header()
en el ejemplo anterior porque aquí WordPress sabe que es una URL válida, así que aún no había establecido el estado 404. Así que aquí está tu código:
<?php
/*
Plugin Name: Descargar CSV
Author: Mike Schinkel
Author URI: http://mikeschinkel.com
*/
if (!class_exists('DownloadCSV')) {
class DownloadCSV {
static function on_load() {
add_action('plugins_loaded',array(__CLASS__,'plugins_loaded'));
add_action('admin_menu',array(__CLASS__,'admin_menu'));
register_activation_hook(__FILE__,array(__CLASS__,'activate'));
}
static function activate() {
$role = get_role('administrator');
$role->add_cap('download_csv');
}
static function admin_menu() {
add_submenu_page('tools.php', // Menú Padre
'Descargar CSV', // Título de la Página
'Descargar CSV', // Etiqueta de la Opción del Menú
'download_csv', // Capacidad
'tools.php?download=data.csv');// URL de la Opción relativa a /wp-admin/
}
static function plugins_loaded() {
global $pagenow;
if ($pagenow=='tools.php' &&
current_user_can('download_csv') &&
isset($_GET['download']) &&
$_GET['download']=='data.csv') {
header("Content-type: application/x-msdownload");
header("Content-Disposition: attachment; filename=data.csv");
header("Pragma: no-cache");
header("Expires: 0");
echo 'data';
exit();
}
}
}
DownloadCSV::on_load();
}
Y aquí hay una captura de pantalla del plugin activado:
(fuente: mikeschinkel.com)
Y finalmente, aquí hay una captura de pantalla del disparador de la descarga:
(fuente: mikeschinkel.com)

Mike, gracias por tu ayuda. El único problema con esta función es que me gustaría que el archivo se descargara desde el backend. Parece que template_redirect no funciona en el backend, y si no se supone que debo usar admin_init, me pregunto qué debería usar en su lugar. admin_init parece funcionar por ahora, podría quedarme con eso al menos a corto plazo. Es una función menor que solo van a usar unas pocas personas.

@Dave Morris - ¿Puedes definir a qué te refieres con "back end"? ¿Te refieres al servidor? Si es así, 'template_redirect'
definitivamente se ejecuta en el servidor. Si no, estoy totalmente confundido; ¿puedes aclarar la preocupación? Gracias de antemano.

@Dave: Si te refieres al área de administración como "back end", esto seguirá funcionando. La URL de descarga comienza con /downloads/data.csv
, que es un archivo inexistente, por lo que el "front end" de WordPress manejará esta solicitud y eventualmente llegará a template-redirect
. Simplemente creas un enlace en el área de administración que apunte a esta URL del front. (Debe mencionarse que de esta manera no obtienes la protección de inicio de sesión de administrador de forma gratuita: cualquiera que conozca la URL puede descargar el archivo, pero tal vez haya una manera fácil de solucionar eso?)

@Jan Fabry - Ah, ahora entiendo. Por "back end" se refería desde dentro del administrador, ¿verdad? Puede usar la función current_user_can()
con el código anterior o tomar otro enfoque. Después de este comentario agregaré una actualización a mi respuesta.

Sí, me disculpo, no estoy recibiendo alertas por correo electrónico de este sitio, por eso mi demora en responder. Me refería al área de administración de WordPress cuando dije "backend". Lo siento por eso. Intentaré usar template_redirect y veré qué sucede. ¡Gracias! ~Dave

@Dave Morris - template_redirect
es más para acceso externo. Prueba el segundo ejemplo que publiqué.

Esto es excelente. Me ahorraste muchos problemas. Solo quiero aclarar para otros que podrían no necesitar la página de submenú que todo lo que necesitas hacer es enganchar el método admin_menu
de la clase DownloadCSV
al gancho de acción con la llamada a add_action('admin_menu',array(__CLASS__,'admin_menu'));
y agregar tu código para forzar la descarga en el código del método.

Un plugin más útil para exportar a CSV. Puede ser útil para alguien
<?php
class CSVExport
{
/**
* Constructor
*/
public function __construct()
{
if(isset($_GET['download_report']))
{
$csv = $this->generate_csv();
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private", false);
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"report.csv\";" );
header("Content-Transfer-Encoding: binary");
echo $csv;
exit;
}
// Añadir elementos extra de menú para administradores
add_action('admin_menu', array($this, 'admin_menu'));
// Crear endpoints
add_filter('query_vars', array($this, 'query_vars'));
add_action('parse_request', array($this, 'parse_request'));
}
/**
* Añadir elementos extra de menú para administradores
*/
public function admin_menu()
{
add_menu_page('Descargar Reporte', 'Descargar Reporte', 'manage_options', 'download_report', array($this, 'download_report'));
}
/**
* Permitir variables de consulta personalizadas
*/
public function query_vars($query_vars)
{
$query_vars[] = 'download_report';
return $query_vars;
}
/**
* Analizar la solicitud
*/
public function parse_request(&$wp)
{
if(array_key_exists('download_report', $wp->query_vars))
{
$this->download_report();
exit;
}
}
/**
* Descargar reporte
*/
public function download_report()
{
echo '<div class="wrap">';
echo '<div id="icon-tools" class="icon32">
</div>';
echo '<h2>Descargar Reporte</h2>';
//$url = site_url();
echo '<p>Exportar los Usuarios';
}
/**
* Convertir datos a CSV
*/
public function generate_csv()
{
$csv_output = '';
$table = 'users';
$result = mysql_query("SHOW COLUMNS FROM ".$table."");
$i = 0;
if (mysql_num_rows($result) > 0) {
while ($row = mysql_fetch_assoc($result)) {
$csv_output = $csv_output . $row['Field'].",";
$i++;
}
}
$csv_output .= "\n";
$values = mysql_query("SELECT * FROM ".$table."");
while ($rowr = mysql_fetch_row($values)) {
for ($j=0;$j<$i;$j++) {
$csv_output .= $rowr[$j].",";
}
$csv_output .= "\n";
}
return $csv_output;
}
}
// Instanciar un singleton de este plugin
$csvExport = new CSVExport();

El hook admin_init o el hook load-(página) parecen funcionar, WordPress no ha establecido cabeceras en este estado. Estoy usando el hook load-(página) porque se ejecuta cuando se carga una página del menú de administración. Puedes cargar tu script para una página específica.
Puedes revisar el hook load-(página) en el Codex de WordPress
Si estás usando el hook admin_init, asegúrate de verificar el nonce usando check_admin_referer o algún otro script podría pasar la condición y generar la salida de tu archivo de descarga.
