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
← All changes | includes/forms/class-charitable-profile-form.php +392 -221 1.6.7 → 1.8.13 View file →
@@ -2,13 +2,13 @@
2 2 /**
3 3 * Class that manages the display and processing of the profile form.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Profile_Form
6 - * @author Eric Daams
7 - * @copyright Copyright (c) 2018, Studio 164a
6 + * @author David Bisset
7 + * @copyright Copyright (c) 2023, WP Charitable LLC
8 8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 9 * @since 1.0.0
10 - * @version 1.5.1
10 + * @version 1.6.51
11 11 */
12 12
13 13 // Exit if accessed directly.
14 14 if ( ! defined( 'ABSPATH' ) ) {
@@ -67,8 +67,11 @@
67 67 $this->shortcode_args = $args;
68 68
69 69 /* For backwards-compatibility */
70 70 add_action( 'charitable_form_field', array( $this, 'render_field' ), 10, 6 );
71 +
72 + add_filter( 'charitable_user_fields', array( $this, 'add_avatar_field' ), 10, 2 );
73 + add_filter( 'charitable_profile_update_values', array( $this, 'save_avatar' ), 10, 3 );
71 74 }
72 75
73 76 /**
74 77 * Return the current user's Charitable_User object.
@@ -88,8 +91,9 @@
88 91 /**
89 92 * Returns the value of a particular key.
90 93 *
91 94 * @since 1.0.0
95 + * @version 1.8.9.1
92 96 *
93 97 * @param string $key The key of the data we are searching for.
94 98 * @param string $default Optional. The value that will be used if none is set.
95 99 * @return mixed
@@ -94,11 +98,13 @@
94 98 * @param string $default Optional. The value that will be used if none is set.
95 99 * @return mixed
96 100 */
97 101 public function get_user_value( $key, $default = '' ) {
102 + // phpcs:disable WordPress.Security.NonceVerification.Missing
98 103 if ( isset( $_POST[ $key ] ) ) {
99 - return $_POST[ $key ];
104 + return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
100 105 }
106 + // phpcs:enable WordPress.Security.NonceVerification.Missing
101 107
102 108 $value = $this->get_user_prop_or_default( $key, $default );
103 109
104 110 /**
@@ -134,54 +140,58 @@
134 140 *
135 141 * @return array
136 142 */
137 143 public function get_user_fields() {
138 - $user_fields = apply_filters( 'charitable_user_fields', array(
139 - 'username' => array(
140 - 'type' => 'paragraph',
141 - 'priority' => '',
142 - 'content' => sprintf(
143 - /* translators: %s: username */
144 - __( 'Username: %s.', 'charitable' ),
145 - $this->get_user()->user_login
144 + $user_fields = apply_filters(
145 + 'charitable_user_fields',
146 + array(
147 + 'username' => array(
148 + 'type' => 'paragraph',
149 + 'priority' => '',
150 + 'content' => sprintf(
151 + /* translators: %s: username */
152 + __( 'Username: %s.', 'charitable' ),
153 + $this->get_user()->user_login
154 + ),
146 155 ),
156 + 'first_name' => array(
157 + 'label' => __( 'First name', 'charitable' ),
158 + 'type' => 'text',
159 + 'priority' => 4,
160 + 'required' => true,
161 + 'value' => $this->get_user_value( 'first_name' ),
162 + ),
163 + 'last_name' => array(
164 + 'label' => __( 'Last name', 'charitable' ),
165 + 'type' => 'text',
166 + 'priority' => 6,
167 + 'required' => true,
168 + 'value' => $this->get_user_value( 'last_name' ),
169 + ),
170 + 'user_email' => array(
171 + 'label' => __( 'Email', 'charitable' ),
172 + 'type' => 'email',
173 + 'required' => true,
174 + 'priority' => 8,
175 + 'value' => $this->get_user_value( 'user_email' ),
176 + ),
177 + 'organisation' => array(
178 + 'label' => __( 'Organization', 'charitable' ),
179 + 'type' => 'text',
180 + 'priority' => 10,
181 + 'required' => false,
182 + 'value' => $this->get_user_value( 'organisation' ),
183 + ),
184 + 'description' => array(
185 + 'label' => __( 'Bio', 'charitable' ),
186 + 'type' => 'textarea',
187 + 'required' => false,
188 + 'priority' => 12,
189 + 'value' => $this->get_user_value( 'description' ),
190 + ),
147 191 ),
148 - 'first_name' => array(
149 - 'label' => __( 'First name', 'charitable' ),
150 - 'type' => 'text',
151 - 'priority' => 4,
152 - 'required' => true,
153 - 'value' => $this->get_user_value( 'first_name' ),
154 - ),
155 - 'last_name' => array(
156 - 'label' => __( 'Last name', 'charitable' ),
157 - 'type' => 'text',
158 - 'priority' => 6,
159 - 'required' => true,
160 - 'value' => $this->get_user_value( 'last_name' ),
161 - ),
162 - 'user_email' => array(
163 - 'label' => __( 'Email', 'charitable' ),
164 - 'type' => 'email',
165 - 'required' => true,
166 - 'priority' => 8,
167 - 'value' => $this->get_user_value( 'user_email' ),
168 - ),
169 - 'organisation' => array(
170 - 'label' => __( 'Organization', 'charitable' ),
171 - 'type' => 'text',
172 - 'priority' => 10,
173 - 'required' => false,
174 - 'value' => $this->get_user_value( 'organisation' ),
175 - ),
176 - 'description' => array(
177 - 'label' => __( 'Bio', 'charitable' ),
178 - 'type' => 'textarea',
179 - 'required' => false,
180 - 'priority' => 12,
181 - 'value' => $this->get_user_value( 'description' ),
182 - ),
183 - ), $this );
192 + $this
193 + );
184 194
185 195 uasort( $user_fields, 'charitable_priority_sort' );
186 196
187 197 return $user_fields;
@@ -194,60 +204,72 @@
194 204 *
195 205 * @return array
196 206 */
197 207 public function get_address_fields() {
198 - $address_fields = apply_filters( 'charitable_user_address_fields', array(
199 - 'address' => array(
200 - 'label' => __( 'Address', 'charitable' ),
201 - 'type' => 'text',
202 - 'priority' => 22,
203 - 'required' => false,
204 - 'value' => $this->get_user_value( 'donor_address' ),
208 + /**
209 + * Filter the list of user address fields.
210 + *
211 + * @since 1.0.0
212 + *
213 + * @param array $fields The fields in the form.
214 + * @param Charitable_Profile_Form $form The profile form.
215 + */
216 + $address_fields = apply_filters(
217 + 'charitable_user_address_fields',
218 + array(
219 + 'address' => array(
220 + 'label' => __( 'Address', 'charitable' ),
221 + 'type' => 'text',
222 + 'priority' => 22,
223 + 'required' => false,
224 + 'value' => $this->get_user_value( 'donor_address' ),
225 + ),
226 + 'address_2' => array(
227 + 'label' => __( 'Address 2', 'charitable' ),
228 + 'type' => 'text',
229 + 'priority' => 24,
230 + 'required' => false,
231 + 'value' => $this->get_user_value( 'donor_address_2' ),
232 + ),
233 + 'city' => array(
234 + 'label' => __( 'City', 'charitable' ),
235 + 'type' => 'text',
236 + 'priority' => 26,
237 + 'required' => false,
238 + 'value' => $this->get_user_value( 'donor_city' ),
239 + ),
240 + 'state' => array(
241 + 'label' => __( 'State', 'charitable' ),
242 + 'type' => 'text',
243 + 'priority' => 28,
244 + 'required' => false,
245 + 'value' => $this->get_user_value( 'donor_state' ),
246 + ),
247 + 'postcode' => array(
248 + 'label' => __( 'Postcode / ZIP code', 'charitable' ),
249 + 'type' => 'text',
250 + 'priority' => 30,
251 + 'required' => false,
252 + 'value' => $this->get_user_value( 'donor_postcode' ),
253 + ),
254 + 'country' => array(
255 + 'label' => __( 'Country', 'charitable' ),
256 + 'type' => 'select',
257 + 'options' => charitable_get_location_helper()->get_countries(),
258 + 'priority' => 32,
259 + 'required' => false,
260 + 'value' => $this->get_user_value( 'donor_country', charitable_get_option( 'country' ) ),
261 + ),
262 + 'phone' => array(
263 + 'label' => __( 'Phone', 'charitable' ),
264 + 'type' => 'text',
265 + 'priority' => 34,
266 + 'required' => false,
267 + 'value' => $this->get_user_value( 'donor_phone' ),
268 + ),
205 269 ),
206 - 'address_2' => array(
207 - 'label' => __( 'Address 2', 'charitable' ),
208 - 'type' => 'text',
209 - 'priority' => 24,
210 - 'required' => false,
211 - 'value' => $this->get_user_value( 'donor_address_2' ),
212 - ),
213 - 'city' => array(
214 - 'label' => __( 'City', 'charitable' ),
215 - 'type' => 'text',
216 - 'priority' => 26,
217 - 'required' => false,
218 - 'value' => $this->get_user_value( 'donor_city' ),
219 - ),
220 - 'state' => array(
221 - 'label' => __( 'State', 'charitable' ),
222 - 'type' => 'text',
223 - 'priority' => 28,
224 - 'required' => false,
225 - 'value' => $this->get_user_value( 'donor_state' ),
226 - ),
227 - 'postcode' => array(
228 - 'label' => __( 'Postcode / ZIP code', 'charitable' ),
229 - 'type' => 'text',
230 - 'priority' => 30,
231 - 'required' => false,
232 - 'value' => $this->get_user_value( 'donor_postcode' ),
233 - ),
234 - 'country' => array(
235 - 'label' => __( 'Country', 'charitable' ),
236 - 'type' => 'select',
237 - 'options' => charitable_get_location_helper()->get_countries(),
238 - 'priority' => 32,
239 - 'required' => false,
240 - 'value' => $this->get_user_value( 'donor_country', charitable_get_option( 'country' ) ),
241 - ),
242 - 'phone' => array(
243 - 'label' => __( 'Phone', 'charitable' ),
244 - 'type' => 'text',
245 - 'priority' => 34,
246 - 'required' => false,
247 - 'value' => $this->get_user_value( 'donor_phone' ),
248 - ),
249 - ), $this );
270 + $this
271 + );
250 272
251 273 uasort( $address_fields, 'charitable_priority_sort' );
252 274
253 275 return $address_fields;
@@ -260,31 +282,35 @@
260 282 *
261 283 * @return array
262 284 */
263 285 public function get_social_fields() {
264 - $social_fields = apply_filters( 'charitable_user_social_fields', array(
265 - 'user_url' => array(
266 - 'label' => __( 'Your Website', 'charitable' ),
267 - 'type' => 'url',
268 - 'priority' => 42,
269 - 'required' => false,
270 - 'value' => $this->get_user_value( 'user_url' ),
286 + $social_fields = apply_filters(
287 + 'charitable_user_social_fields',
288 + array(
289 + 'user_url' => array(
290 + 'label' => __( 'Your Website', 'charitable' ),
291 + 'type' => 'url',
292 + 'priority' => 42,
293 + 'required' => false,
294 + 'value' => $this->get_user_value( 'user_url' ),
295 + ),
296 + 'twitter' => array(
297 + 'label' => __( 'Twitter / X', 'charitable' ),
298 + 'type' => 'text',
299 + 'priority' => 44,
300 + 'required' => false,
301 + 'value' => $this->get_user_value( 'twitter' ),
302 + ),
303 + 'facebook' => array(
304 + 'label' => __( 'Facebook', 'charitable' ),
305 + 'type' => 'text',
306 + 'priority' => 46,
307 + 'required' => false,
308 + 'value' => $this->get_user_value( 'facebook' ),
309 + ),
271 310 ),
272 - 'twitter' => array(
273 - 'label' => __( 'Twitter', 'charitable' ),
274 - 'type' => 'text',
275 - 'priority' => 44,
276 - 'required' => false,
277 - 'value' => $this->get_user_value( 'twitter' ),
278 - ),
279 - 'facebook' => array(
280 - 'label' => __( 'Facebook', 'charitable' ),
281 - 'type' => 'text',
282 - 'priority' => 46,
283 - 'required' => false,
284 - 'value' => $this->get_user_value( 'facebook' ),
285 - ),
286 - ), $this );
311 + $this
312 + );
287 313
288 314 uasort( $social_fields, 'charitable_priority_sort' );
289 315
290 316 return $social_fields;
@@ -304,10 +330,10 @@
304 330 }
305 331
306 332 $donor = $this->get_user()->get_donor();
307 333
308 - if ( array_key_exists( 'contact_consent', $_POST ) ) {
309 - $consent = $_POST['contact_consent'];
334 + if ( array_key_exists( 'contact_consent', $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
335 + $consent = sanitize_text_field( wp_unslash( $_POST['contact_consent'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
310 336 } elseif ( is_null( $donor ) ) {
311 337 $consent = false;
312 338 } else {
313 339 $consent = $donor->contact_consent;
@@ -320,17 +346,21 @@
320 346 *
321 347 * @param array $communication_fields List of fields.
322 348 * @param Charitable_Profile_Form $form Instance of `Charitable_Profile_Form`.
323 349 */
324 - $communication_fields = apply_filters( 'charitable_user_communication_preferences_fields', array(
325 - 'contact_consent' => array(
326 - 'type' => 'checkbox',
327 - 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
328 - 'priority' => 8,
329 - 'required' => false,
330 - 'checked' => $consent,
350 + $communication_fields = apply_filters(
351 + 'charitable_user_communication_preferences_fields',
352 + array(
353 + 'contact_consent' => array(
354 + 'type' => 'checkbox',
355 + 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
356 + 'priority' => 8,
357 + 'required' => false,
358 + 'checked' => $consent,
359 + ),
331 360 ),
332 - ), $this );
361 + $this
362 + );
333 363
334 364 if ( empty( $communication_fields ) ) {
335 365 return $fields;
336 366 }
@@ -336,16 +366,19 @@
336 366 }
337 367
338 368 uasort( $fields, 'charitable_priority_sort' );
339 369
340 - return array_merge( $fields, array(
341 - 'communication_fields' => array(
342 - 'legend' => __( 'Communication Preferences', 'charitable' ),
343 - 'type' => 'fieldset',
344 - 'fields' => $communication_fields,
345 - 'priority' => 60,
346 - ),
347 - ) );
370 + return array_merge(
371 + $fields,
372 + array(
373 + 'communication_fields' => array(
374 + 'legend' => __( 'Communication Preferences', 'charitable' ),
375 + 'type' => 'fieldset',
376 + 'fields' => $communication_fields,
377 + 'priority' => 60,
378 + ),
379 + )
380 + );
348 381 }
349 382
350 383 /**
351 384 * Profile fields to be displayed.
@@ -354,34 +387,38 @@
354 387 *
355 388 * @return array[]
356 389 */
357 390 public function get_fields() {
358 - $fields = apply_filters( 'charitable_user_profile_fields', array(
359 - 'user_fields' => array(
360 - 'legend' => __( 'Your Details', 'charitable' ),
361 - 'type' => 'fieldset',
362 - 'fields' => $this->get_user_fields(),
363 - 'priority' => 0,
391 + $fields = apply_filters(
392 + 'charitable_user_profile_fields',
393 + array(
394 + 'user_fields' => array(
395 + 'legend' => __( 'Your Details', 'charitable' ),
396 + 'type' => 'fieldset',
397 + 'fields' => $this->get_user_fields(),
398 + 'priority' => 0,
399 + ),
400 + 'password_fields' => array(
401 + 'legend' => __( 'Change Your Password', 'charitable' ),
402 + 'type' => 'fieldset',
403 + 'fields' => $this->get_password_fields(),
404 + 'priority' => 10,
405 + ),
406 + 'address_fields' => array(
407 + 'legend' => __( 'Your Address', 'charitable' ),
408 + 'type' => 'fieldset',
409 + 'fields' => $this->get_address_fields(),
410 + 'priority' => 20,
411 + ),
412 + 'social_fields' => array(
413 + 'legend' => __( 'Your Social Profiles', 'charitable' ),
414 + 'type' => 'fieldset',
415 + 'fields' => $this->get_social_fields(),
416 + 'priority' => 40,
417 + ),
364 418 ),
365 - 'password_fields' => array(
366 - 'legend' => __( 'Change Your Password', 'charitable' ),
367 - 'type' => 'fieldset',
368 - 'fields' => $this->get_password_fields(),
369 - 'priority' => 10,
370 - ),
371 - 'address_fields' => array(
372 - 'legend' => __( 'Your Address', 'charitable' ),
373 - 'type' => 'fieldset',
374 - 'fields' => $this->get_address_fields(),
375 - 'priority' => 20,
376 - ),
377 - 'social_fields' => array(
378 - 'legend' => __( 'Your Social Profiles', 'charitable' ),
379 - 'type' => 'fieldset',
380 - 'fields' => $this->get_social_fields(),
381 - 'priority' => 40,
382 - ),
383 - ), $this );
419 + $this
420 + );
384 421
385 422 $fields = $this->maybe_get_communication_preferences_fields( $fields );
386 423
387 424 uasort( $fields, 'charitable_priority_sort' );
@@ -396,11 +433,14 @@
396 433 *
397 434 * @return array
398 435 */
399 436 public function get_hidden_fields() {
400 - return array_merge( parent::get_hidden_fields(), array(
401 - 'current_email' => $this->get_user()->get_email(),
402 - ) );
437 + return array_merge(
438 + parent::get_hidden_fields(),
439 + array(
440 + 'current_email' => $this->get_user()->get_email(),
441 + )
442 + );
403 443 }
404 444
405 445 /**
406 446 * The fields displayed on the password form.
@@ -409,29 +449,33 @@
409 449 *
410 450 * @return array[]
411 451 */
412 452 public function get_password_fields() {
413 - $password_fields = apply_filters( 'charitable_user_profile_password_fields', array(
414 - 'current_pass' => array(
415 - 'priority' => 2,
416 - 'type' => 'password',
417 - 'label' => __( 'Current Password (leave blank to leave unchanged)', 'charitable' ),
418 - 'value' => '',
419 - 'required' => false,
453 + $password_fields = apply_filters(
454 + 'charitable_user_profile_password_fields',
455 + array(
456 + 'current_pass' => array(
457 + 'priority' => 2,
458 + 'type' => 'password',
459 + 'label' => __( 'Current Password (leave blank to leave unchanged)', 'charitable' ),
460 + 'value' => '',
461 + 'required' => false,
462 + ),
463 + 'user_pass' => array(
464 + 'priority' => 4,
465 + 'type' => 'password',
466 + 'label' => __( 'New Password (leave blank to leave unchanged)', 'charitable' ),
467 + 'required' => false,
468 + ),
469 + 'user_pass_repeat' => array(
470 + 'priority' => 6,
471 + 'type' => 'password',
472 + 'label' => __( 'New Password (again)', 'charitable' ),
473 + 'required' => false,
474 + ),
420 475 ),
421 - 'user_pass' => array(
422 - 'priority' => 4,
423 - 'type' => 'password',
424 - 'label' => __( 'New Password (leave blank to leave unchanged)', 'charitable' ),
425 - 'required' => false,
426 - ),
427 - 'user_pass_repeat' => array(
428 - 'priority' => 6,
429 - 'type' => 'password',
430 - 'label' => __( 'New Password (again)', 'charitable' ),
431 - 'required' => false,
432 - ),
433 - ), $this );
476 + $this
477 + );
434 478
435 479 uasort( $password_fields, 'charitable_priority_sort' );
436 480
437 481 return $password_fields;
@@ -437,30 +481,8 @@
437 481 return $password_fields;
438 482 }
439 483
440 484 /**
441 - * Returns all fields as a merged array.
442 - *
443 - * @since 1.0.0
444 - *
445 - * @return array[]
446 - */
447 - public function get_merged_fields() {
448 - $fields = array();
449 -
450 - foreach ( $this->get_fields() as $key => $section ) {
451 -
452 - if ( isset( $section['fields'] ) ) {
453 - $fields = array_merge( $fields, $section['fields'] );
454 - } else {
455 - $fields[ $key ] = $section;
456 - }
457 - }
458 -
459 - return $fields;
460 - }
461 -
462 - /**
463 485 * Update profile after form submission.
464 486 *
465 487 * @since 1.0.0
466 488 *
@@ -469,9 +491,9 @@
469 491 public static function update_profile() {
470 492 $form = new Charitable_Profile_Form();
471 493
472 494 if ( ! $form->validate_nonce() || ! $form->validate_honeypot() ) {
473 - charitable_get_notices()->add_error( __( 'There was an error with processing your form submission. Please reload the page and try again.', 'charitable' ) );
495 + charitable_get_notices()->add_error( __( 'Unfortunately, we were unable to verify your form submission. Please reload the page and try again.', 'charitable' ) );
474 496 return;
475 497 }
476 498
477 499 $user = $form->get_user();
@@ -482,9 +504,17 @@
482 504 }
483 505
484 506 $fields = $form->get_merged_fields();
485 507
486 - $submitted = apply_filters( 'charitable_profile_update_values', $_POST, $fields, $form );
508 + /**
509 + * Filter the profile values.
510 + */
511 + $submitted = apply_filters(
512 + 'charitable_profile_update_values',
513 + $form->get_submitted_values(),
514 + $fields,
515 + $form
516 + );
487 517
488 518 /* Remove the current_pass and user_pass_repeat fields, if set. */
489 519 unset(
490 520 $submitted['current_pass'],
@@ -517,13 +547,13 @@
517 547 *
518 548 * @return boolean
519 549 */
520 550 public function is_changing_password() {
521 - if ( ! isset( $_POST['user_pass'] ) || empty( $_POST['user_pass'] ) ) {
551 + if ( ! isset( $_POST['user_pass'] ) || empty( $_POST['user_pass'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
522 552 return false;
523 553 }
524 554
525 - if ( ! isset( $_POST['current_pass'] ) || empty( $_POST['current_pass'] ) ) {
555 + if ( ! isset( $_POST['current_pass'] ) || empty( $_POST['current_pass'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
526 556 return false;
527 557 }
528 558
529 559 return true;
@@ -537,15 +567,15 @@
537 567 * @return boolean
538 568 */
539 569 public function validate_password_change() {
540 570 /* The current password must be correct. */
541 - if ( false == wp_check_password( $_POST['current_pass'], $this->get_user()->user_pass ) ) {
571 + if ( false == wp_check_password( $_POST['current_pass'], $this->get_user()->user_pass ) ) { // phpcs:ignore
542 572 charitable_get_notices()->add_error( __( 'Current password is incorrect.', 'charitable' ) );
543 573 return false;
544 574 }
545 575
546 576 /* The new password must match the repeat (if set). */
547 - if ( isset( $_POST['user_pass_repeat'] ) && $_POST['user_pass_repeat'] != $_POST['user_pass'] ) {
577 + if ( isset( $_POST['user_pass_repeat'] ) && $_POST['user_pass_repeat'] != $_POST['user_pass'] ) { // phpcs:ignore
548 578 charitable_get_notices()->add_error( __( 'New passwords did not match.', 'charitable' ) );
549 579 return false;
550 580 }
551 581
@@ -559,17 +589,17 @@
559 589 *
560 590 * @return boolean
561 591 */
562 592 public function is_changing_email() {
563 - if ( ! isset( $_POST['user_email'] ) || empty( $_POST['user_email'] ) ) {
593 + if ( ! isset( $_POST['user_email'] ) || empty( $_POST['user_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
564 594 return false;
565 595 }
566 596
567 - if ( ! isset( $_POST['current_email'] ) ) {
568 - return $_POST['user_email'] != $this->get_user()->get_email();
597 + if ( ! isset( $_POST['current_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
598 + return $_POST['user_email'] !== $this->get_user()->get_email(); // phpcs:ignore WordPress.Security.NonceVerification.Missing
569 599 }
570 600
571 - return $_POST['user_email'] != $_POST['current_email'];
601 + return $_POST['user_email'] !== $_POST['current_email']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
572 602 }
573 603
574 604 /**
575 605 * Validate the email change.
@@ -574,13 +604,22 @@
574 604 /**
575 605 * Validate the email change.
576 606 *
577 607 * @since 1.6.2
608 + * @version 1.8.9.1
578 609 *
579 610 * @return boolean
580 611 */
581 612 public function validate_email_change() {
582 - if ( 0 != charitable_get_donor_id_by_email( $_POST['user_email'] ) ) {
613 + // phpcs:disable WordPress.Security.NonceVerification.Missing
614 + $user_email = isset( $_POST['user_email'] ) ? sanitize_email( wp_unslash( $_POST['user_email'] ) ) : '';
615 + // phpcs:enable WordPress.Security.NonceVerification.Missing
616 +
617 + if ( empty( $user_email ) ) {
618 + return false;
619 + }
620 +
621 + if ( 0 != charitable_get_donor_id_by_email( $user_email ) ) {
583 622 charitable_get_notices()->add_error(
584 623 __( 'This email address is already in use.', 'charitable' )
585 624 );
586 625 return false;
@@ -622,11 +661,143 @@
622 661 __( 'charitable_user_profile_after_fields hook has been removed. Use charitable_form_after_fields instead.', 'charitable' ),
623 662 '1.4.0'
624 663 );
625 664
626 - if ( 'Charitable_Profile_Form' == get_class( $form ) ) {
665 + if ( 'Charitable_Profile_Form' == get_class( $form ) ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
627 666 do_action( 'charitable_user_profile_after_fields', $form );
628 667 }
668 + }
669 +
670 + /**
671 + * Add avatar section to user profile form.
672 + *
673 + * @param array[] $fields The fields in the form.
674 + * @param Charitable_Profile_Form $form The profile form object.
675 + * @return array[]
676 + * @access public
677 + * @since 1.0.0
678 + */
679 + public function add_avatar_field( $fields, $form ) {
680 + $user = $form->get_user();
681 + $user_avatar = new Charitable_User_Avatar();
682 + $avatar = $user_avatar->get_user_avatar( false, $user );
683 +
684 + if ( ! $avatar && charitable_user_has_gravatar( $user->user_email ) ) {
685 + $avatar = get_avatar( $user->ID, 100 );
686 + }
687 +
688 + $fields['avatar'] = apply_filters(
689 + 'charitable_user_avatar_field_args',
690 + array(
691 + 'label' => __( 'Your Profile Photo', 'charitable' ),
692 + 'type' => 'picture',
693 + 'uploader' => true,
694 + 'size' => 100,
695 + 'value' => $avatar,
696 + 'priority' => 14,
697 + 'fullwidth' => true,
698 + )
699 + );
700 +
701 + return $fields;
702 + }
703 +
704 + /**
705 + * Upload avatar and add file fields to the submitted fields.
706 + *
707 + * @param array $submitted The values submitted by the user.
708 + * @param array[] $fields The fields in the form.
709 + * @param Charitable_Profile_Form $form The profile form object.
710 + * @return array
711 + * @access public
712 + * @since 1.0.0
713 + */
714 + public function save_avatar( $submitted, $fields, $form ) {
715 + $user_id = (int) $form->get_user()->ID;
716 + $files_avatar = isset( $_FILES['avatar'] ) ? $_FILES['avatar'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
717 + $has_upload = is_array( $files_avatar )
718 + && ! empty( $files_avatar['name'] )
719 + && UPLOAD_ERR_NO_FILE !== (int) ( $files_avatar['error'] ?? UPLOAD_ERR_NO_FILE );
720 +
721 + if ( $has_upload ) {
722 + $attachment_id = $form->upload_post_attachment( 'avatar', 0 );
723 +
724 + if ( is_wp_error( $attachment_id ) ) {
725 + return $submitted;
726 + }
727 +
728 + $submitted['avatar'] = (int) $attachment_id;
729 +
730 + // Delete the previously stored avatar — only if it belongs to this user.
731 + // Without this ownership check, a poisoned 'avatar' user-meta value
732 + // (planted by a no-file submission, see below) would chain into
733 + // wp_delete_attachment() on an attacker-chosen attachment ID.
734 + $old_avatar = get_user_meta( $user_id, 'avatar', true );
735 + if ( ! empty( $old_avatar )
736 + && $this->user_owns_attachment( $user_id, (int) $old_avatar ) ) {
737 + wp_delete_attachment( (int) $old_avatar );
738 + }
739 +
740 + update_user_meta( $user_id, 'avatar', (int) $attachment_id );
741 +
742 + return $submitted;
743 + }
744 +
745 + // No file in $_FILES. The avatar uploader (plupload) uploads the image
746 + // asynchronously and posts the resulting attachment ID as a hidden field
747 + // value, so the normal "change avatar" flow lands here with the new ID and
748 + // no $_FILES — we must not reject it.
749 + //
750 + // Block meta poisoning: only persist an attachment the submitting user
751 + // actually owns (a no-op re-save of their current avatar is also allowed).
752 + // An attacker-supplied ID owned by someone else is discarded so it cannot
753 + // be stored in user meta and later weaponised into wp_delete_attachment()
754 + // on a subsequent $_FILES submission.
755 + $current = (int) get_user_meta( $user_id, 'avatar', true );
756 + $posted = array_key_exists( 'avatar', $submitted ) ? $submitted['avatar'] : null;
757 +
758 + // Absent, empty, or non-scalar value means the avatar was cleared. The
759 + // non-scalar guard blocks array poisoning (e.g. avatar[]=foo would
760 + // otherwise coerce via (int) to 1 and bypass the ownership check below).
761 + if ( null === $posted || '' === $posted || ! is_scalar( $posted ) ) {
762 + /* The picture has been removed. */
763 + $submitted['avatar'] = '';
764 + return $submitted;
765 + }
766 +
767 + $posted = (int) $posted;
768 +
769 + // Discard anything that is neither the current avatar nor an attachment
770 + // this user owns.
771 + if ( $posted !== $current && ! $this->user_owns_attachment( $user_id, $posted ) ) {
772 + $submitted['avatar'] = $current ?: '';
773 + return $submitted;
774 + }
775 +
776 + $submitted['avatar'] = $posted;
777 +
778 + return $submitted;
779 + }
780 +
781 + /**
782 + * Verify the given user owns the given attachment.
783 + *
784 + * @since 1.8.11.2
785 + *
786 + * @param int $user_id The user ID.
787 + * @param int $attachment_id The attachment ID.
788 + * @return bool
789 + */
790 + private function user_owns_attachment( $user_id, $attachment_id ) {
791 + if ( $user_id <= 0 || $attachment_id <= 0 ) {
792 + return false;
793 + }
794 +
795 + $attachment = get_post( $attachment_id );
796 +
797 + return $attachment instanceof WP_Post
798 + && 'attachment' === $attachment->post_type
799 + && (int) $attachment->post_author === (int) $user_id;
629 800 }
630 801 }
631 802
632 803 endif;