PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / ajax / charitable-ajax-functions.php

charitable-ajax-functions.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/ajax/charitable-ajax-functions.php

440 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable AJAX Functions.
4 *
5 * Functions used with ajax hooks.
6 *
7 * @package Charitable/Functions/AJAX
8 * @author David Bisset
9 * @copyright Copyright (c) 2023, WP Charitable LLC
10 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
11 * @since 1.2.3
12 * @version 1.8.12.4
13 */
14
15 // Exit if accessed directly.
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 if ( ! function_exists( 'charitable_ajax_get_donation_form' ) ) :
21
22 /**
23 * Returns the donation form content for a particular campaign, through AJAX.
24 *
25 * @since 1.2.3
26 *
27 * @return void
28 */
29 function charitable_ajax_get_donation_form() { // phpcs:ignore WordPress.Security.NonceVerification.Missing
30 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verification handled by caller
31 if ( ! isset( $_POST['campaign_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
32 wp_send_json_error();
33 }
34
35 /* Load the template files. */
36 require_once( charitable()->get_path( 'includes' ) . 'public/charitable-template-functions.php' );
37 require_once( charitable()->get_path( 'includes' ) . 'public/charitable-template-hooks.php' );
38
39 $campaign = new Charitable_Campaign( $_POST['campaign_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
40
41 ob_start();
42
43 $campaign->get_donation_form()->render();
44
45 /**
46 * Strip any shortcodes that haven't been rendered yet.
47 *
48 * @see https://github.com/Charitable/Charitable/issues/708
49 */
50 $output = preg_replace( '~(?:\[/?)[^/\]]+/?\]~s', '', ob_get_clean() );
51
52 wp_send_json_success( $output );
53
54 die();
55 }
56
57 endif;
58
59 if ( ! function_exists( 'charitable_plupload_image_upload' ) ) :
60
61 /**
62 * Upload an image via plupload.
63 *
64 * @return void
65 */
66 function charitable_plupload_image_upload() {
67 // absint(), not FILTER_SANITIZE_NUMBER_INT: that filter strips non-digits rather than
68 // rejecting them, so "1e5" resolves to post 15 and "a1b2" to post 12.
69 $post_id = absint( filter_input( INPUT_POST, 'post_id' ) );
70 $field_id = (string) filter_input( INPUT_POST, 'field_id' );
71
72 check_ajax_referer( 'charitable-upload-images-' . $field_id );
73
74 /*
75 * Default-refuse anonymous callers.
76 *
77 * This handler is registered on wp_ajax_nopriv_, so it is reachable by logged-out
78 * visitors. The nonce is not an access control for them: wp_create_nonce() resolves
79 * against user 0 with an empty session token, so the token printed into any public
80 * picture field is identical for every anonymous visitor and valid for ~24h. One GET
81 * of a public form yields a token anyone can reuse to write files into the media
82 * library. charitable_picture_uploads_enabled() holds the opt-in, and the picture
83 * field template asks it the same question so the form cannot draw a working
84 * uploader for a visitor this endpoint will refuse.
85 *
86 * Errors are returned with a 200 on purpose. plupload's HTML5 runtime treats any
87 * status >= 400 as an HTTP error and fires Error instead of FileUploaded, and the
88 * Error binding in charitable-plupload-fields.js only logs to the console - so a
89 * status code here would hang the "Uploading..." row with nothing shown to the
90 * visitor. A 200 lands on the ! r.success branch, which renders the message.
91 */
92 if ( ! charitable_picture_uploads_enabled( $field_id, $post_id ) ) {
93 wp_send_json_error( array( 'message' => __( 'Please log in or create an account to upload an image.', 'charitable' ) ) );
94 }
95
96 /*
97 * A request can pass the nonce and still carry no usable file - either nothing at all,
98 * or an array shape (async-upload[]) that makes core compare an array against an int
99 * and then use it as an array offset. Require the one thing every step below needs:
100 * a single non-empty tmp_name string.
101 */
102 if ( ! isset( $_FILES['async-upload']['tmp_name'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
103 || ! is_string( $_FILES['async-upload']['tmp_name'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
104 || '' === $_FILES['async-upload']['tmp_name'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
105 wp_send_json_error( array( 'message' => __( 'No file was uploaded.', 'charitable' ) ) );
106 }
107
108 $file = $_FILES['async-upload']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
109
110 /*
111 * This is a picture field, so restrict the server-side allowlist to images. Without a
112 * mimes array, wp_handle_upload() falls back to the full get_allowed_mime_types() set
113 * (PDF, ZIP, MP4, MP3, ...) for any caller; the client-side plupload extension filter
114 * is otherwise the only thing keeping non-images out, and a direct request bypasses it.
115 *
116 * Seeded from the same list the non-AJAX form path already uses in
117 * Charitable_Form::get_file_overrides(), plus webp, so the same picture field does not
118 * accept one set of types through the uploader and a different set on form submit.
119 */
120 $charitable_default_mimes = array(
121 'jpg|jpeg|jpe' => 'image/jpeg',
122 'gif' => 'image/gif',
123 'png' => 'image/png',
124 'bmp' => 'image/bmp',
125 'tif|tiff' => 'image/tiff',
126 'ico' => 'image/x-icon',
127 'webp' => 'image/webp',
128 );
129
130 $mimes = apply_filters( 'charitable_picture_upload_allowed_mimes', $charitable_default_mimes, $field_id );
131
132 /*
133 * Never hand core an empty or non-array list. wp_check_filetype() reads an empty
134 * $mimes as "no restriction" and falls back to get_allowed_mime_types(), so
135 * normalising a bad filter return to array() would quietly widen this endpoint back
136 * to every type the site allows - the exact opposite of what it looks like it does.
137 */
138 if ( ! is_array( $mimes ) || empty( $mimes ) ) {
139 $mimes = $charitable_default_mimes;
140 }
141
142 $file_attr = wp_handle_upload(
143 $file,
144 array(
145 'test_form' => false,
146 'mimes' => $mimes,
147 )
148 );
149
150 if ( isset( $file_attr['error'] ) ) {
151 /*
152 * Core's upload errors quote server configuration back to the caller: the PHP
153 * size limit setting by name, or the uploads directory and whether its parent is
154 * writable. This endpoint answers logged-out visitors on public forms, so report
155 * our own wording instead.
156 */
157 wp_send_json_error( array( 'message' => __( 'Sorry, that file could not be uploaded. Please choose an image and try again.', 'charitable' ) ) );
158 }
159
160 /*
161 * Re-assert against what actually landed on disk.
162 *
163 * The list above is not the last word on type. wp_handle_upload() resolves the type
164 * through wp_check_filetype_and_ext(), and any theme or plugin can rewrite that
165 * function's result through the filter of the same name - the usual way SVG support
166 * gets bolted onto a site, and it ignores the mimes array completely. It also falls
167 * back to the browser-supplied Content-Type for a caller holding unfiltered_upload.
168 * So resolve the type from the final on-disk filename with wp_check_filetype(), which
169 * is extension matching with no filter anywhere in its path, and require an image.
170 *
171 * SVG is refused here even on a site that permits it elsewhere: it is script-bearing
172 * markup served from this origin, which through a picture field is stored XSS.
173 */
174 $charitable_final = wp_check_filetype( $file_attr['file'] );
175 $charitable_final_type = ! empty( $charitable_final['type'] ) ? (string) $charitable_final['type'] : '';
176
177 if ( 0 !== strpos( $charitable_final_type, 'image/' ) || preg_match( '/svg|xml/i', $charitable_final_type ) ) {
178 /*
179 * Assert the cleanup rather than assuming it. wp_delete_file() runs the path
180 * through its own filter, which backup and media-offload plugins do intercept and
181 * can blank - and a file left behind here is the very thing being refused, sitting
182 * at a public URL.
183 */
184 if ( ! wp_delete_file( $file_attr['file'] ) && file_exists( $file_attr['file'] ) ) {
185 // wp_delete_file() is the first attempt above; this is the backstop for when
186 // its filter blanked the path, so the alternative-function sniff does not apply.
187 @unlink( $file_attr['file'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink,WordPress.PHP.NoSilencedErrors.Discouraged
188
189 if ( file_exists( $file_attr['file'] ) ) {
190 charitable_log(
191 'Picture upload refused but the file could not be removed',
192 $file_attr['file'],
193 array( 'level' => 'error' )
194 );
195 }
196 }
197
198 wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to upload this file type.', 'charitable' ) ) );
199 }
200
201 /*
202 * Do not trust the caller-supplied post_id as the attachment parent. Honour it only
203 * when the current user can actually edit that post; otherwise parent to 0. For an
204 * anonymous opt-in upload there is no such user, so this always resolves to 0.
205 */
206 $parent_id = ( $post_id && current_user_can( 'edit_post', $post_id ) ) ? $post_id : 0;
207
208 $attachment = array(
209 'guid' => $file_attr['url'],
210 'post_mime_type' => $file_attr['type'],
211 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $file['name'] ) ),
212 'post_content' => '',
213 'post_status' => 'inherit',
214 );
215
216 /**
217 * Insert the file as an attachment.
218 */
219 $attachment_id = wp_insert_attachment( $attachment, $file_attr['file'], $parent_id );
220
221 if ( is_wp_error( $attachment_id ) ) {
222 wp_send_json_error();
223 }
224
225 wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file_attr['file'] ) );
226
227 $size = (string) filter_input( INPUT_POST, 'size' );
228 $max_uploads = (int) filter_input( INPUT_POST, 'max_uploads', FILTER_SANITIZE_NUMBER_INT );
229
230 if ( ! $size ) {
231 $size = 'thumbnail';
232 }
233
234 ob_start();
235
236 charitable_template( 'form-fields/picture-preview.php', array(
237 'image' => $attachment_id,
238 'field' => array(
239 'key' => $field_id,
240 'size' => $size,
241 'max_uploads' => $max_uploads,
242 ),
243 ) );
244
245 wp_send_json_success( ob_get_clean() );
246 }
247
248 endif;
249
250 /**
251 * Get donor data, given a donor ID.
252 *
253 * @since 1.6.28
254 *
255 * @return void
256 */
257 function charitable_ajax_get_donor_data() {
258 $donor_id = (int) filter_input( INPUT_POST, 'donor_id', FILTER_SANITIZE_NUMBER_INT );
259
260 if ( ! check_ajax_referer( 'donor-select', 'nonce' ) ) {
261 wp_send_json_error( 'nonce check failed', '403' );
262 }
263
264 $fields = array_key_exists( 'fields', $_POST ) ? $_POST['fields'] : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
265 $donor = new Charitable_Donor( $donor_id );
266 $data = [];
267
268 foreach ( $fields as $field ) {
269 $data[ $field ] = $donor->get_donor_meta( $field );
270 }
271
272 wp_send_json_success( $data );
273 }
274
275 /**
276 * Receives an AJAX request to load session content and returns
277 * the output to be loaded.
278 *
279 * @since 1.5.0
280 *
281 * @return void
282 */
283 function charitable_ajax_get_session_content() { // phpcs:ignore WordPress.Security.NonceVerification.Missing
284 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verification handled by caller
285 if ( ! array_key_exists( 'templates', $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
286 wp_send_json_error( __( 'Missing templates in request.', 'charitable' ) );
287 }
288
289 $output = array();
290
291 foreach ( $_POST['templates'] as $i => $template_args ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
292 if ( empty( $template_args ) || ! array_key_exists( 'template', $template_args ) ) {
293 continue;
294 }
295
296 /**
297 * Get the output for the session content item.
298 *
299 * @since 1.5.0
300 *
301 * @param false|string $content The content to return, or a false in case of failure.
302 * @param array $args Mixed set of arguments.
303 */
304 $output[ $i ] = apply_filters( 'charitable_session_content_' . $template_args['template'], false, $template_args );
305 }
306
307 wp_send_json_success( $output );
308 }
309
310 /**
311 * Return the donation receipt.
312 *
313 * @since 1.5.0
314 *
315 * @param string|false $content Content to return, or false in case of failure.
316 * @param array $args Mixed array of args.
317 * @return string|false
318 */
319 function charitable_ajax_get_session_donation_receipt( $content, $args ) {
320 if ( ! array_key_exists( 'donation_id', $args ) ) {
321 return $content;
322 }
323
324 $donation = charitable_get_donation( $args['donation_id'] );
325
326 if ( ! $donation ) {
327 return $content;
328 }
329
330 return charitable_template_donation_receipt_output( '', $donation );
331 }
332
333 /**
334 * Return the donation form's amount field.
335 *
336 * @since 1.5.0
337 *
338 * @param string|false $content Content to return, or false in case of failure.
339 * @param array $args Mixed array of args.
340 * @return string|false
341 */
342 function charitable_ajax_get_session_donation_form_amount_field( $content, $args ) {
343 if ( ! array_key_exists( 'campaign_id', $args ) ) {
344 return $content;
345 }
346
347 if ( ! array_key_exists( 'form_id', $args ) ) {
348 return $content;
349 }
350
351 ob_start();
352
353 charitable_template(
354 'donation-form/donation-amount-list.php',
355 array(
356 'campaign' => charitable_get_campaign( $args['campaign_id'] ),
357 'form_id' => $args['form_id'],
358 )
359 );
360
361 return ob_get_clean();
362 }
363
364 /**
365 * Return the donation form's amount field.
366 *
367 * @since 1.5.0
368 *
369 * @param string|false $content Content to return, or false in case of failure.
370 * @param array $args Mixed array of args.
371 * @return string|false
372 */
373 function charitable_ajax_get_session_donation_form_current_amount_text( $content, $args ) {
374 if ( ! array_key_exists( 'campaign_id', $args ) ) {
375 return $content;
376 }
377
378 if ( ! array_key_exists( 'form_id', $args ) ) {
379 return $content;
380 }
381
382 $amount = charitable_get_campaign( $args['campaign_id'] )->get_donation_amount_in_session();
383
384 return charitable_template_donation_form_current_amount_text( $amount, $args['form_id'], $args['campaign_id'] );
385 }
386
387 /**
388 * Return the error messages.
389 *
390 * @since 1.5.0
391 *
392 * @param string|false $content Content to return, or false in case of failure.
393 * @return string|false
394 */
395 function charitable_ajax_get_session_errors( $content ) {
396 $errors = charitable_get_notices()->get_errors();
397
398 if ( empty( $errors ) ) {
399 return $content;
400 }
401
402 ob_start();
403
404 charitable_template(
405 'form-fields/errors.php',
406 array(
407 'errors' => $errors,
408 )
409 );
410
411 return ob_get_clean();
412 }
413
414 /**
415 * Return the notices
416 *
417 * @since 1.5.0
418 *
419 * @param string|false $content Content to return, or false in case of failure.
420 * @return string|false
421 */
422 function charitable_ajax_get_session_notices( $content ) {
423 $notices = charitable_get_notices()->get_notices();
424
425 if ( empty( $notices ) ) {
426 return $content;
427 }
428
429 ob_start();
430
431 charitable_template(
432 'form-fields/notices.php',
433 array(
434 'notices' => $notices,
435 )
436 );
437
438 return ob_get_clean();
439 }
440