PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.0.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.0.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / pdf / receipt-generator.php

receipt-generator.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.0.0, at inc/pdf/receipt-generator.php

308 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * PDF Receipt Generator.
4 *
5 * Generates PDF donation receipts using the mPDF library.
6 *
7 * @package SureDonation
8 */
9
10 namespace SureDonation\Inc\Pdf;
11
12 use SureDonation\Inc\Database\Tables\Donations;
13 use SureDonation\Inc\Database\Tables\Donors;
14 use SureDonation\Inc\Helper;
15
16 // Exit if accessed directly.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Receipt_Generator class.
23 *
24 * @since 1.0.0
25 */
26 class Receipt_Generator {
27 /**
28 * Get an existing receipt PDF or generate a new one.
29 *
30 * @param int $donation_id Donation ID.
31 * @return string|false File path on success, false on failure.
32 * @since 1.0.0
33 */
34 public static function get_or_generate( $donation_id ) {
35 $donation = Donations::get( $donation_id );
36
37 if ( ! $donation ) {
38 return false;
39 }
40
41 // Check if a cached PDF exists.
42 $existing_relative = $donation['receipt_pdf_url'] ?? '';
43
44 if ( ! empty( $existing_relative ) ) {
45 $existing_path = self::relative_to_path( Helper::get_string_value( $existing_relative ) );
46
47 if ( $existing_path && file_exists( $existing_path ) ) {
48 return $existing_path;
49 }
50 }
51
52 return self::generate( $donation_id );
53 }
54
55 /**
56 * Generate a PDF receipt for a donation.
57 *
58 * @param int $donation_id Donation ID.
59 * @return string|false File path on success, false on failure.
60 * @since 1.0.0
61 */
62 public static function generate( $donation_id ) {
63 if ( ! Pdf_Utils::check_if_library_exists() || ! Pdf_Utils::is_php_compatible() ) {
64 return false;
65 }
66
67 // Load the mPDF autoloader.
68 require_once Pdf_Utils::get_library_path() . '/vendor/autoload.php';
69
70 $donation = Donations::get( $donation_id );
71
72 if ( ! $donation ) {
73 return false;
74 }
75
76 $donor_id = Helper::get_integer_value( $donation['donor_id'] ?? 0 );
77 $donor = $donor_id ? Donors::get( $donor_id ) : null;
78
79 $campaign_id = Helper::get_integer_value( $donation['campaign_id'] ?? 0 );
80 $campaign_title = $campaign_id ? (string) get_the_title( $campaign_id ) : '';
81
82 // Build the receipt HTML.
83 $html = self::build_receipt_html( $donation, $donor, $campaign_title );
84
85 /**
86 * Filter the receipt HTML before PDF generation.
87 *
88 * SECURITY NOTE: The returned HTML is passed directly to mPDF's WriteHTML().
89 * mPDF can process <img> tags with file:// URIs and load external resources.
90 * Ensure any modifications only use trusted, escaped content.
91 *
92 * @param string $html Receipt HTML.
93 * @param array $donation Donation data.
94 * @param array|null $donor Donor data.
95 * @since 1.0.0
96 */
97 $html = apply_filters( 'suredonation_receipt_html', $html, $donation, $donor );
98
99 // Ensure receipts directory exists.
100 Pdf_Utils::ensure_receipts_dir();
101
102 $receipts_dir = Pdf_Utils::get_receipts_dir();
103 $filename = sprintf( 'suredonation-receipt-%d-%s.pdf', $donation_id, wp_generate_password( 8, false ) );
104 $filepath = $receipts_dir . '/' . $filename;
105
106 try {
107 $mpdf = new \Mpdf\Mpdf( self::get_mpdf_config() );
108 $mpdf->WriteHTML( $html );
109 $mpdf->Output( $filepath, \Mpdf\Output\Destination::FILE );
110 } catch ( \Exception $e ) {
111 return false;
112 }
113
114 // Store the relative path in the donation record (portable across domain changes).
115 $upload_dir = wp_upload_dir();
116 $relative_path = str_replace( $upload_dir['basedir'] . '/', '', $filepath );
117 Donations::update( $donation_id, [ 'receipt_pdf_url' => $relative_path ] );
118
119 return $filepath;
120 }
121
122 /**
123 * Get the mPDF configuration.
124 *
125 * @return array<string,mixed>
126 * @since 1.0.0
127 */
128 private static function get_mpdf_config() {
129 return [
130 'mode' => 'utf-8',
131 'format' => 'A4',
132 'orientation' => 'P',
133 'margin_left' => 15,
134 'margin_right' => 15,
135 'margin_top' => 15,
136 'margin_bottom' => 15,
137 'default_font' => 'dejavusans',
138 'tempDir' => Pdf_Utils::get_temp_dir(),
139 // mPDF defaults allow <img src="file:///..."> and remote http(s)
140 // resource fetching. The receipt HTML is server-templated with
141 // escaped fields, but the suredonation_receipt_html filter (and
142 // any future ucfirst-only gateway label) would still surface
143 // donor / gateway data into HTML that mPDF processes. Disable
144 // the dangerous resource-loading defaults so a malicious string
145 // in any rendered field can't become SSRF (remote fetch) or
146 // LFI (local file read into the PDF) regardless of the source.
147 'allow_remote_dir_in_links_filesystem' => false,
148 'curlAllowUnsafeSslRequests' => false,
149 ];
150 }
151
152 /**
153 * Build the receipt HTML template.
154 *
155 * @param array<string, mixed> $donation Donation data.
156 * @param array<string, mixed>|null $donor Donor data.
157 * @param string $campaign_title Campaign title.
158 * @return string HTML content.
159 * @since 1.0.0
160 */
161 private static function build_receipt_html( $donation, $donor, $campaign_title ) {
162 $site_name = esc_html( get_bloginfo( 'name' ) );
163 $site_url = esc_url( site_url() );
164
165 $donation_id = Helper::get_integer_value( $donation['id'] ?? 0 );
166 $donor_name = esc_html( Helper::get_string_value( $donor['name'] ?? '' ) );
167 $donor_email = esc_html( Helper::get_string_value( $donor['email'] ?? '' ) );
168 $payment_status = esc_html( ucfirst( Helper::get_string_value( $donation['payment_status'] ?? '' ) ) );
169 $payment_method = esc_html( ucfirst( Helper::get_string_value( $donation['gateway'] ?? '' ) ) );
170 $transaction_id = esc_html( Helper::get_string_value( $donation['transaction_id'] ?? '' ) );
171 $currency = Helper::get_string_value( $donation['currency'] ?? 'USD' );
172 $total = Helper::get_float_value( $donation['amount'] ?? 0 );
173 $fees_covered = Helper::get_float_value( $donation['fees_covered'] ?? 0 );
174 $amount = $total - $fees_covered;
175 $date = Helper::get_string_value( $donation['created_at'] ?? '' );
176
177 if ( ! empty( $date ) ) {
178 $date_format = Helper::get_string_value( get_option( 'date_format' ) );
179 $timestamp = strtotime( $date );
180 $formatted_date = false !== $timestamp ? wp_date( $date_format, $timestamp ) : false;
181 $date = is_string( $formatted_date ) ? $formatted_date : $date;
182 }
183
184 $campaign_title = esc_html( $campaign_title );
185
186 // Format amounts.
187 $formatted_amount = self::format_currency( $amount, $currency );
188 $formatted_fees = self::format_currency( $fees_covered, $currency );
189 $formatted_total = self::format_currency( $total, $currency );
190
191 // Build transaction ID row.
192 $transaction_row = '';
193 if ( ! empty( $transaction_id ) ) {
194 $transaction_row = sprintf(
195 '<tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;">%s</td>
196 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">%s</td></tr>',
197 esc_html__( 'Transaction ID', 'suredonation' ),
198 $transaction_id
199 );
200 }
201
202 // Build fees row.
203 $fees_row = '';
204 if ( $fees_covered > 0 ) {
205 $fees_row = sprintf(
206 '<tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;">%s</td>
207 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">%s</td></tr>',
208 esc_html__( 'Fees Covered', 'suredonation' ),
209 $formatted_fees
210 );
211 }
212
213 return '
214 <div style="max-width:560px;margin:0 auto;font-family:DejaVu Sans,sans-serif;color:#111827;">
215 <div style="text-align:center;margin-bottom:24px;padding-bottom:20px;border-bottom:2px solid #e5e7eb;">
216 <h1 style="font-size:20px;margin:0 0 4px;color:#111827;">' . $site_name . '</h1>
217 <p style="color:#6b7280;font-size:13px;margin:0;">' . esc_html__( 'Donation Receipt', 'suredonation' ) . '</p>
218 </div>
219
220 <p style="color:#6b7280;font-size:12px;margin:0 0 16px;text-align:right;">'
221 . esc_html__( 'Receipt', 'suredonation' ) . ' #' . $donation_id . '</p>
222
223 <table style="width:100%;border-collapse:collapse;margin-bottom:20px;border:1px solid #e5e7eb;border-radius:6px;">
224 <tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;width:40%;">'
225 . esc_html__( 'Donor Name', 'suredonation' ) . '</td>
226 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">' . $donor_name . '</td></tr>
227 <tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;">'
228 . esc_html__( 'Donor Email', 'suredonation' ) . '</td>
229 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">' . $donor_email . '</td></tr>
230 </table>
231
232 <table style="width:100%;border-collapse:collapse;margin-bottom:20px;border:1px solid #e5e7eb;border-radius:6px;">
233 <tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;width:40%;">'
234 . esc_html__( 'Campaign Name', 'suredonation' ) . '</td>
235 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">' . $campaign_title . '</td></tr>
236 <tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;">'
237 . esc_html__( 'Payment Status', 'suredonation' ) . '</td>
238 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">' . $payment_status . '</td></tr>
239 <tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;">'
240 . esc_html__( 'Payment Method', 'suredonation' ) . '</td>
241 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">' . $payment_method . '</td></tr>
242 ' . $transaction_row . '
243 <tr><td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;">'
244 . esc_html__( 'Donation Amount', 'suredonation' ) . '</td>
245 <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px;color:#111827;">' . $formatted_amount . '</td></tr>
246 ' . $fees_row . '
247 <tr><td style="padding:10px 12px;background:#f9fafb;font-weight:bold;color:#111827;font-size:13px;">'
248 . esc_html__( 'Donation Total', 'suredonation' ) . '</td>
249 <td style="padding:10px 12px;background:#f9fafb;font-weight:bold;font-size:13px;color:#111827;">' . $formatted_total . '</td></tr>
250 </table>
251
252 <p style="color:#6b7280;font-size:12px;margin:0 0 4px;">'
253 . esc_html__( 'Date', 'suredonation' ) . ': ' . esc_html( $date ) . '</p>
254
255 <div style="margin-top:30px;padding-top:16px;border-top:1px solid #e5e7eb;text-align:center;">
256 <p style="color:#9ca3af;font-size:11px;margin:0;">'
257 . sprintf(
258 /* translators: 1: Site name, 2: Site URL. */
259 esc_html__( 'Generated by %1$s · %2$s', 'suredonation' ),
260 $site_name,
261 $site_url
262 ) . '</p>
263 </div>
264 </div>';
265 }
266
267 /**
268 * Format a monetary amount with currency symbol.
269 *
270 * @param float $amount Amount to format.
271 * @param string $currency Currency code.
272 * @return string Formatted amount.
273 * @since 1.0.0
274 */
275 private static function format_currency( $amount, $currency = 'USD' ) {
276 $symbols = [
277 'USD' => '$',
278 'EUR' => '',
279 'GBP' => '£',
280 'CAD' => 'CA$',
281 'AUD' => 'A$',
282 'INR' => '',
283 'JPY' => '¥',
284 ];
285
286 $symbol = $symbols[ strtoupper( $currency ) ] ?? esc_html( $currency ) . ' ';
287
288 return $symbol . number_format( $amount, 2 );
289 }
290
291 /**
292 * Convert a relative path to an absolute file path.
293 *
294 * @param string $relative_path Relative path within the uploads directory.
295 * @return string|false Absolute file path or false.
296 * @since 1.0.0
297 */
298 private static function relative_to_path( $relative_path ) {
299 if ( empty( $relative_path ) ) {
300 return false;
301 }
302
303 $upload_dir = wp_upload_dir();
304
305 return $upload_dir['basedir'] . '/' . $relative_path;
306 }
307 }
308