tags with file:// URIs and load external resources. * Ensure any modifications only use trusted, escaped content. * * @param string $html Receipt HTML. * @param array $donation Donation data. * @param array|null $donor Donor data. * @since 1.0.0 */ $html = apply_filters( 'suredonation_receipt_html', $html, $donation, $donor ); // Ensure receipts directory exists. Pdf_Utils::ensure_receipts_dir(); $receipts_dir = Pdf_Utils::get_receipts_dir(); $filename = sprintf( 'suredonation-receipt-%d-%s.pdf', $donation_id, wp_generate_password( 8, false ) ); $filepath = $receipts_dir . '/' . $filename; try { $mpdf = new \Mpdf\Mpdf( self::get_mpdf_config() ); $mpdf->WriteHTML( $html ); $mpdf->Output( $filepath, \Mpdf\Output\Destination::FILE ); } catch ( \Exception $e ) { return false; } // Store the relative path in the donation record (portable across domain changes). $upload_dir = wp_upload_dir(); $relative_path = str_replace( $upload_dir['basedir'] . '/', '', $filepath ); Donations::update( $donation_id, [ 'receipt_pdf_url' => $relative_path ] ); return $filepath; } /** * Delete a receipt PDF file by its stored uploads-relative path. * * Used by the personal-data eraser: the receipt is generated from the donor's * name/email/address, so an erasure must remove the file from disk, not just * the database columns. * * @since 1.2.0 * @param string $relative_path Relative path within the uploads directory. * @return bool True when no file remains (deleted or never existed), false when it survived deletion. */ public static function delete_receipt( $relative_path ) { $filepath = self::relative_to_path( $relative_path ); if ( false === $filepath || ! file_exists( $filepath ) ) { return true; } wp_delete_file( $filepath ); // Re-check with is_file() (not file_exists()) — wp_delete_file() has a // filesystem side effect PHPStan can't see, so re-calling the already // narrowed file_exists() reads as always-false to it. clearstatcache( true, $filepath ); return ! is_file( $filepath ); } /** * Get the mPDF configuration. * * @return array * @since 1.0.0 */ private static function get_mpdf_config() { return [ 'mode' => 'utf-8', 'format' => 'A4', 'orientation' => 'P', 'margin_left' => 15, 'margin_right' => 15, 'margin_top' => 15, 'margin_bottom' => 15, 'default_font' => 'dejavusans', 'tempDir' => Pdf_Utils::get_temp_dir(), // mPDF defaults allow and remote http(s) // resource fetching. The receipt HTML is server-templated with // escaped fields, but the suredonation_receipt_html filter (and // any future ucfirst-only gateway label) would still surface // donor / gateway data into HTML that mPDF processes. Disable // the dangerous resource-loading defaults so a malicious string // in any rendered field can't become SSRF (remote fetch) or // LFI (local file read into the PDF) regardless of the source. 'allow_remote_dir_in_links_filesystem' => false, 'curlAllowUnsafeSslRequests' => false, ]; } /** * Build the receipt HTML template. * * @param array $donation Donation data. * @param array|null $donor Donor data. * @param string $campaign_title Campaign title. * @return string HTML content. * @since 1.0.0 */ private static function build_receipt_html( $donation, $donor, $campaign_title ) { $site_name = esc_html( get_bloginfo( 'name' ) ); $site_url = esc_url( site_url() ); $donation_id = Helper::get_integer_value( $donation['id'] ?? 0 ); $donor_name = esc_html( Helper::get_string_value( $donor['name'] ?? '' ) ); $donor_email = esc_html( Helper::get_string_value( $donor['email'] ?? '' ) ); $payment_status = esc_html( ucfirst( Helper::get_string_value( $donation['payment_status'] ?? '' ) ) ); $payment_method = esc_html( ucfirst( Helper::get_string_value( $donation['gateway'] ?? '' ) ) ); $transaction_id = esc_html( Helper::get_string_value( $donation['transaction_id'] ?? '' ) ); $currency = Helper::get_string_value( $donation['currency'] ?? 'USD' ); $total = Helper::get_float_value( $donation['amount'] ?? 0 ); $fees_covered = Helper::get_float_value( $donation['fees_covered'] ?? 0 ); $amount = $total - $fees_covered; $date = Helper::get_string_value( $donation['created_at'] ?? '' ); if ( ! empty( $date ) ) { $date_format = Helper::get_string_value( get_option( 'date_format' ) ); $timestamp = strtotime( $date ); $formatted_date = false !== $timestamp ? wp_date( $date_format, $timestamp ) : false; $date = is_string( $formatted_date ) ? $formatted_date : $date; } $campaign_title = esc_html( $campaign_title ); // Format amounts. $formatted_amount = self::format_currency( $amount, $currency ); $formatted_fees = self::format_currency( $fees_covered, $currency ); $formatted_total = self::format_currency( $total, $currency ); // Build transaction ID row. $transaction_row = ''; if ( ! empty( $transaction_id ) ) { $transaction_row = sprintf( '%s %s', esc_html__( 'Transaction ID', 'suredonation' ), $transaction_id ); } // Build fees row. $fees_row = ''; if ( $fees_covered > 0 ) { $fees_row = sprintf( '%s %s', esc_html__( 'Fees Covered', 'suredonation' ), $formatted_fees ); } return '

' . $site_name . '

' . esc_html__( 'Donation Receipt', 'suredonation' ) . '

' . esc_html__( 'Receipt', 'suredonation' ) . ' #' . $donation_id . '

' . esc_html__( 'Donor Name', 'suredonation' ) . ' ' . $donor_name . '
' . esc_html__( 'Donor Email', 'suredonation' ) . ' ' . $donor_email . '
' . $transaction_row . ' ' . $fees_row . '
' . esc_html__( 'Campaign Name', 'suredonation' ) . ' ' . $campaign_title . '
' . esc_html__( 'Payment Status', 'suredonation' ) . ' ' . $payment_status . '
' . esc_html__( 'Payment Method', 'suredonation' ) . ' ' . $payment_method . '
' . esc_html__( 'Donation Amount', 'suredonation' ) . ' ' . $formatted_amount . '
' . esc_html__( 'Donation Total', 'suredonation' ) . ' ' . $formatted_total . '

' . esc_html__( 'Date', 'suredonation' ) . ': ' . esc_html( $date ) . '

' . sprintf( /* translators: 1: Site name, 2: Site URL. */ esc_html__( 'Generated by %1$s · %2$s', 'suredonation' ), $site_name, $site_url ) . '

'; } /** * Format a monetary amount with currency symbol. * * @param float $amount Amount to format. * @param string $currency Currency code. * @return string Formatted amount. * @since 1.0.0 */ private static function format_currency( $amount, $currency = 'USD' ) { // Delegate to the single source of truth so the currency symbol, // decimal handling and sign position match every other surface // (this replaces a divergent local symbol map). return Payment_Helper::format_amount( $amount, $currency ); } /** * Convert a relative path to an absolute file path. * * @param string $relative_path Relative path within the uploads directory. * @return string|false Absolute file path or false. * @since 1.0.0 */ private static function relative_to_path( $relative_path ) { if ( empty( $relative_path ) ) { return false; } $upload_dir = wp_upload_dir(); return $upload_dir['basedir'] . '/' . $relative_path; } }