Contents
Introducción
Generar PDFs desde PHP dentro de WordPress y forzar su descarga es una necesidad habitual: facturas, tickets, contratos o resúmenes que el usuario debe obtener en su equipo. Este artículo explica, con todo lujo de detalles, cómo hacerlo correctamente, qué bibliotecas usar, cómo integrarlo en un plugin o en un tema, y cómo forzar la descarga garantizando seguridad, compatibilidad y manejo correcto de cabeceras.
Resumen de enfoques
- Generar y forzar descarga en memoria: crear el PDF en memoria y enviar al navegador (streaming), ideal para PDFs temporales o únicos.
- Generar archivo en disco y luego forzar descarga: práctico si quieres conservar una copia o generar versiones para auditoría.
- Usar biblioteca con función de descarga: algunas bibliotecas (TCPDF, Dompdf, FPDF con extensiones) permiten directamente emitir la descarga.
Bibliotecas recomendadas
- Dompdf: buena para convertir HTML/CSS a PDF. Sencillo para plantillas HTML que ya uses en WordPress.
- TCPDF: potente y completo, más código para maquetado pero muy estable.
- FPDF o FPDI: ligero y útil si el PDF es programático (texto, imágenes, tablas).
Requisitos y buenas prácticas
- Protege el endpoint con nonces y/o capacidades (current_user_can) cuando el PDF incluya datos sensibles.
- No permitas rutas arbitrarias ni nombres de archivo no sanitizados (evita Path Traversal).
- Evita el output previo (whitespace, echoes) antes de enviar cabeceras. Usa ob_end_clean() cuando proceda.
- Usa Content-Length cuando sea posible para que el navegador muestre progreso correcto.
- Después de enviar el PDF siempre termina la ejecución (exit or die) para evitar añadir HTML adicional.
Ejemplo 1 — Usar Dompdf y forzar descarga (plugin sencillo)
Este ejemplo asume que has instalado Dompdf dentro de tu plugin usando Composer (vendor/autoload.php). Crea un archivo de plugin y añade este código. El endpoint será ?download_pdf=1ampid=123ampnonce=… y el plugin generará el PDF y lo enviará al navegador con descarga forzada.
403 ) ) } id = isset( _GET[id] ) ? intval( _GET[id] ) : 0 if ( id <= 0 ) { wp_die( ID inválido, Error, array( response => 400 ) ) } // Ejemplo de autorización: sólo usuarios logueados pueden descargar (ajusta según necesidad) if ( ! is_user_logged_in() ) { auth_redirect() } // Preparar contenido HTML para el PDF. Aquí puedes cargar plantilla con get_template_part o locate_template. html =Ejemplo PDF para ID . esc_html( id ) .
html .=Generado en . esc_html( date( c ) ) .
// Cargar Dompdf (asegúrate de que vendor/autoload.php existe en el plugin) autoload = __DIR__ . /vendor/autoload.php if ( ! file_exists( autoload ) ) { wp_die( Autoload no encontrado. Instala Dompdf con Composer., Error, array( response => 500 ) ) } require_once autoload // Usa Dompdf options = new DompdfOptions() options->set(isRemoteEnabled, true) dompdf = new DompdfDompdf(options) dompdf->loadHtml( html ) dompdf->setPaper(A4, portrait) dompdf->render() // Nombre de archivo seguro filename = documento- . id . .pdf // Limpiar cualquier buffer y enviar cabeceras correctas if ( ob_get_length() ) { ob_end_clean() } // Forzar descarga: Output en Dompdf puede usarse directamente con D (descarga). // Pero para tener control de headers, tomaremos el output y lo enviaremos. pdfOutput = dompdf->output() header( Content-Type: application/pdf ) header( Content-Transfer-Encoding: binary ) // Manejo de filename para caracteres no ascii filename_utf8 = rawurlencode( filename ) header( Content-Disposition: attachment filename={filename} filename=UTF-8{filename_utf8} ) header( Content-Length: . strlen( pdfOutput ) ) // Cache-control header( Pragma: public ) header( Cache-Control: must-revalidate, post-check=0, pre-check=0 ) echo pdfOutput exit } // Función que genera enlace seguro (usa ejemplo en plantilla) function pdfg_get_download_link( id ) { nonce = wp_create_nonce( pdfg_download ) url = add_query_arg( array( download_pdf => 1, id => intval( id ), nonce => nonce, ), home_url( / ) ) return url } ?>
En una plantilla o shortcode puedes usar la función pdfg_get_download_link( id ) para mostrar un enlace:
Notas sobre este enfoque
- Usar isRemoteEnabled en Dompdf permite cargar imágenes externas (siempre validar).
- Si el PDF es grande, considera guardar en disco temporal y luego usar readfile para menor uso de memoria.
Ejemplo 2 — TCPDF: usar la opción integrada para forzar descarga
TCPDF incluye un método Output que recibe un modo. El modo D fuerza la descarga inmediata. Ejemplo minimalista dentro de WordPress:
AddPage() pdf->SetFont(helvetica, , 12) pdf->Write(0, Texto de ejemplo para TCPDF) filename = reporte-tcpdf.pdf // D fuerza descarga pdf->Output( filename, D ) // No debe haber echo ni HTML después exit ?>
Ejemplo 3 — Generar el archivo en disco y servir con readfile()
Útil para mantener historiales. Generas el archivo en una carpeta protegida (fuera de la raíz pública si es posible), luego lo sirves con cabeceras y readfile.
stream(, array(Attachment => 0)) // ejemplo no usado file_put_contents( path, dompdf->output() ) // Ahora servirlo de forma segura if ( ! file_exists( path ) ) { wp_die( Archivo no encontrado, Error, array( response => 404 ) ) } // Comprobaciones de seguridad aquí (nonce, permisos, etc.) if ( ob_get_length() ) { ob_end_clean() } filesize = filesize( path ) filename = basename( path ) // Forzar descarga header( Content-Description: File Transfer ) header( Content-Type: application/pdf ) header( Content-Disposition: attachment filename= . filename . filename=UTF-8 . rawurlencode( filename ) ) header( Content-Transfer-Encoding: binary ) header( Expires: 0 ) header( Cache-Control: must-revalidate ) header( Pragma: public ) header( Content-Length: . filesize ) readfile( path ) exit ?>
Cabeceras importantes y recomendaciones
- Content-Type: application/pdf
- Content-Disposition: attachment filename=archivo.pdf filename=UTF-8%encoded% para nombres con UTF-8
- Content-Length: longitud en bytes cuando se conoce
- Eliminar cualquier salida previa con ob_get_length() y ob_end_clean()
- Si usas plugins de cache o Nginx/Apache con buffer, comprueba que no interfieran con las cabeceras (excluye la URL del cache si es necesario).
Seguridad y escalabilidad
- Valida permisos: evita que usuarios no autorizados descarguen PDFs con datos ajenos.
- Protege las rutas: guarda PDFs temporales en directorios fuera de la web público cuando sea posible.
- Limita tamaño y tiempo de vida de PDFs temporales limpia con cron o wp_cron.
- Rate limiting si el endpoint se va a exponer públicamente y puede generar cargas pesadas.
Depuración y problemas comunes
- Errores de headers enviados: suele ocurrir por output previo (whitespace en archivos PHP). Revisa BOM o includes que hagan echo.
- PDF corrupto: puede deberse a texto HTML adicional al final. Asegúrate de exit después de enviar PDF.
- Imágenes no muestran en Dompdf: activa isRemoteEnabled y usa rutas absolutas o data URIs.
- Problemas con nombres con acentos: usa rawurlencode en filename y doble cabecera filename por compatibilidad con navegadores viejos y nuevos.
Ejemplo práctico: Shortcode que genera un enlace y permite descarga (resumen)
Shortcode [pdf_download id=123] que muestra un enlace con nonce seguro:
0 ), atts, pdf_download ) id = intval( atts[id] ) if ( id <= 0 ) { return } nonce = wp_create_nonce( pdfg_download ) url = add_query_arg( array( download_pdf => 1, id => id, nonce => nonce, ), home_url( / ) ) return lta href= . esc_url( url ) . gtDescargar PDFlt/agt } ) ?>
Conclusión
Generar PDFs y forzar su descarga en WordPress es totalmente factible y flexible. La elección de la biblioteca depende de si trabajas con HTML/CSS (Dompdf), un maquetado más programático (TCPDF, FPDF) o necesitas manipular plantillas existentes. Siempre aplica validaciones, nonces y controles de permisos, evita outputs previos y usa las cabeceras correctas para una experiencia de descarga robusta y compatible con navegadores.
Enlaces útiles
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |