# easy-invoice/2.4.4/includes/Services/PdfDownload.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.4. 199 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.4/code/includes/Services/PdfDownload.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.4/raw/includes/Services/PdfDownload.php
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.4/code/includes/Services/PdfDownload.php#L10-L20`.

```php
<?php
/**
 * Serves the PDF download from the server when it can.
 *
 * @package Easy_Invoice
 * @subpackage Services
 */

namespace EasyInvoice\Services;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Upgrades `?auto_download_pdf=1` to a real, server-rendered PDF.
 *
 * Why intercept the existing URL
 * ------------------------------
 * That query argument is already the download link everywhere — the invoice and
 * quote listings, the single-document page, and the links inside emails that
 * have already been sent. Introducing a second URL would leave every one of
 * those on the old path, and the ones already in customers' inboxes could never
 * be changed at all. Taking over the existing route upgrades all of them at
 * once, including links that predate this code.
 *
 * What changes for the reader
 * ---------------------------
 * Until now a "PDF" from this plugin was a screenshot. The browser rasterised
 * the page with html2canvas and wrapped the image in a PDF, so the result had
 * no selectable text, no searchable content, nothing a screen reader could
 * announce, and a file size measured in megabytes. It also varied by browser,
 * which is why PDF rendering bugs kept recurring across releases.
 *
 * The server-rendered document is real text.
 *
 * Why the old path stays
 * ----------------------
 * dompdf is a Composer dependency, and sites deployed by copying files around
 * can arrive without `vendor/`. If the renderer is missing, or rendering fails
 * for this particular document, this hook simply returns and the page loads and
 * does what it always did. A merchant never sees a download stop working
 * because the better implementation was unavailable — the worst case is the
 * result they were already getting.
 *
 * Access control
 * --------------
 * None here, deliberately. `TemplateLoader::enforceDocumentAccess` runs on the
 * same hook at priority 1 and has already decided whether this visitor may see
 * this document; anything still executing at priority 5 is past that gate.
 * Repeating the check here would mean two places to keep in agreement, and the
 * quieter failure is the one that forgets to deny.
 */
class PdfDownload {

    /** Query argument that asks for the PDF. */
    const TRIGGER = 'auto_download_pdf';

    /**
     * Hook the interceptor.
     *
     * @return void
     */
    public static function init(): void {
        // Priority 5: after enforceDocumentAccess (1), before anything renders.
        add_action( 'template_redirect', [ __CLASS__, 'maybeServe' ], 5 );
    }

    /**
     * Serve the PDF if this request is asking for one and we can produce it.
     *
     * @return void
     */
    public static function maybeServe(): void {
        if ( ! isset( $_GET[ self::TRIGGER ] ) || '1' !== (string) $_GET[ self::TRIGGER ] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a read-only download of a document the access gate has already cleared.
            return;
        }

        if ( ! is_singular( [ 'easy_invoice', 'easy_invoice_quote' ] ) ) {
            return;
        }

        /**
         * Filter whether the server renders the PDF for this request.
         *
         * Returning false falls back to the browser-side renderer, which is the
         * behaviour every version before this one had.
         *
         * @param bool $enabled Whether to render server-side.
         */
        if ( ! apply_filters( 'easy_invoice_server_pdf_enabled', true ) ) {
            return;
        }

        // The site chooses how the download button makes its PDF. "Browser"
        // captures the page exactly as it is drawn — every design, custom
        // template and watermark included — which is what the button did
        // before 2.4.0 and what it does by default. "Server" renders the
        // document with dompdf: real text and a small file. Emails, the REST
        // API and e-invoicing always use the server, since they have no browser.
        if ( 'server' !== self::downloadMethod() ) {
            return;
        }

        if ( ! PdfRenderer::isAvailable() ) {
            return;
        }

        $post = get_queried_object();
        if ( ! $post instanceof \WP_Post ) {
            return;
        }

        $is_quote = 'easy_invoice_quote' === $post->post_type;

        try {
            $document = $is_quote
                ? new \EasyInvoice\Models\Quote( $post )
                : new \EasyInvoice\Models\Invoice( $post );

            $pdf = $is_quote
                ? PdfRenderer::renderQuote( $document )
                : PdfRenderer::renderInvoice( $document );
        } catch ( \Throwable $e ) {
            error_log( 'Easy Invoice: server-side PDF failed, falling back to the browser — ' . $e->getMessage() );
            return;
        }

        if ( is_wp_error( $pdf ) || ! is_string( $pdf ) || '' === $pdf ) {
            if ( is_wp_error( $pdf ) ) {
                error_log( 'Easy Invoice: server-side PDF failed, falling back to the browser — ' . $pdf->get_error_message() );
            }
            return;
        }

        self::stream( $pdf, self::filename( $document, $is_quote ) );
    }

    /**
     * How the download button produces its PDF: 'browser' or 'server'.
     *
     * @return string
     */
    public static function downloadMethod(): string {
        $method = (string) get_option( 'easy_invoice_pdf_download_method', 'browser' );
        /**
         * Filter how the Download as PDF button produces its file.
         *
         * @param string $method 'browser' (capture the page as shown) or 'server' (dompdf).
         */
        $method = (string) apply_filters( 'easy_invoice_pdf_download_method', $method );
        return 'server' === $method ? 'server' : 'browser';
    }

    /**
     * Send the bytes as a download and stop.
     *
     * @param string $pdf      PDF bytes.
     * @param string $filename Download filename.
     * @return void
     */
    private static function stream( string $pdf, string $filename ): void {
        // Any stray output — a notice, a plugin's whitespace — would corrupt the
        // file, and a corrupt PDF is worse than a slow one.
        if ( ob_get_length() ) {
            @ob_end_clean(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        }

        nocache_headers();
        header( 'Content-Type: application/pdf' );
        header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
        header( 'Content-Length: ' . strlen( $pdf ) );
        header( 'X-Content-Type-Options: nosniff' );

        echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput -- binary document.
        exit;
    }

    /**
     * Download filename for a document.
     *
     * @param object $document Invoice or Quote model.
     * @param bool   $is_quote Whether this is a quote.
     * @return string
     */
    private static function filename( $document, bool $is_quote ): string {
        $number = '';
        if ( is_callable( [ $document, 'getNumber' ] ) ) {
            $number = trim( (string) $document->getNumber() );
        }

        if ( '' === $number ) {
            $number = $is_quote ? 'quote' : 'invoice';
        }

        return sanitize_file_name( $number . '.pdf' );
    }
}

```
