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 +267 -176 1.7.0.9 → 1.8.13 View file →
@@ -3,9 +3,9 @@
3 3 * Class that manages the display and processing of the profile form.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Profile_Form
6 6 * @author David Bisset
7 - * @copyright Copyright (c) 2022, WP Charitable LLC
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 10 * @version 1.6.51
11 11 */
@@ -68,9 +68,9 @@
68 68
69 69 /* For backwards-compatibility */
70 70 add_action( 'charitable_form_field', array( $this, 'render_field' ), 10, 6 );
71 71
72 - add_filter( 'charitable_user_fields', array( $this, 'add_avatar_field' ), 10, 2 );
72 + add_filter( 'charitable_user_fields', array( $this, 'add_avatar_field' ), 10, 2 );
73 73 add_filter( 'charitable_profile_update_values', array( $this, 'save_avatar' ), 10, 3 );
74 74 }
75 75
76 76 /**
@@ -91,8 +91,9 @@
91 91 /**
92 92 * Returns the value of a particular key.
93 93 *
94 94 * @since 1.0.0
95 + * @version 1.8.9.1
95 96 *
96 97 * @param string $key The key of the data we are searching for.
97 98 * @param string $default Optional. The value that will be used if none is set.
98 99 * @return mixed
@@ -97,11 +98,13 @@
97 98 * @param string $default Optional. The value that will be used if none is set.
98 99 * @return mixed
99 100 */
100 101 public function get_user_value( $key, $default = '' ) {
102 + // phpcs:disable WordPress.Security.NonceVerification.Missing
101 103 if ( isset( $_POST[ $key ] ) ) {
102 - return $_POST[ $key ];
104 + return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
103 105 }
106 + // phpcs:enable WordPress.Security.NonceVerification.Missing
104 107
105 108 $value = $this->get_user_prop_or_default( $key, $default );
106 109
107 110 /**
@@ -137,54 +140,58 @@
137 140 *
138 141 * @return array
139 142 */
140 143 public function get_user_fields() {
141 - $user_fields = apply_filters( 'charitable_user_fields', array(
142 - 'username' => array(
143 - 'type' => 'paragraph',
144 - 'priority' => '',
145 - 'content' => sprintf(
146 - /* translators: %s: username */
147 - __( 'Username: %s.', 'charitable' ),
148 - $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 + ),
149 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 + ),
150 191 ),
151 - 'first_name' => array(
152 - 'label' => __( 'First name', 'charitable' ),
153 - 'type' => 'text',
154 - 'priority' => 4,
155 - 'required' => true,
156 - 'value' => $this->get_user_value( 'first_name' ),
157 - ),
158 - 'last_name' => array(
159 - 'label' => __( 'Last name', 'charitable' ),
160 - 'type' => 'text',
161 - 'priority' => 6,
162 - 'required' => true,
163 - 'value' => $this->get_user_value( 'last_name' ),
164 - ),
165 - 'user_email' => array(
166 - 'label' => __( 'Email', 'charitable' ),
167 - 'type' => 'email',
168 - 'required' => true,
169 - 'priority' => 8,
170 - 'value' => $this->get_user_value( 'user_email' ),
171 - ),
172 - 'organisation' => array(
173 - 'label' => __( 'Organization', 'charitable' ),
174 - 'type' => 'text',
175 - 'priority' => 10,
176 - 'required' => false,
177 - 'value' => $this->get_user_value( 'organisation' ),
178 - ),
179 - 'description' => array(
180 - 'label' => __( 'Bio', 'charitable' ),
181 - 'type' => 'textarea',
182 - 'required' => false,
183 - 'priority' => 12,
184 - 'value' => $this->get_user_value( 'description' ),
185 - ),
186 - ), $this );
192 + $this
193 + );
187 194
188 195 uasort( $user_fields, 'charitable_priority_sort' );
189 196
190 197 return $user_fields;
@@ -275,31 +282,35 @@
275 282 *
276 283 * @return array
277 284 */
278 285 public function get_social_fields() {
279 - $social_fields = apply_filters( 'charitable_user_social_fields', array(
280 - 'user_url' => array(
281 - 'label' => __( 'Your Website', 'charitable' ),
282 - 'type' => 'url',
283 - 'priority' => 42,
284 - 'required' => false,
285 - '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 + ),
286 310 ),
287 - 'twitter' => array(
288 - 'label' => __( 'Twitter', 'charitable' ),
289 - 'type' => 'text',
290 - 'priority' => 44,
291 - 'required' => false,
292 - 'value' => $this->get_user_value( 'twitter' ),
293 - ),
294 - 'facebook' => array(
295 - 'label' => __( 'Facebook', 'charitable' ),
296 - 'type' => 'text',
297 - 'priority' => 46,
298 - 'required' => false,
299 - 'value' => $this->get_user_value( 'facebook' ),
300 - ),
301 - ), $this );
311 + $this
312 + );
302 313
303 314 uasort( $social_fields, 'charitable_priority_sort' );
304 315
305 316 return $social_fields;
@@ -319,10 +330,10 @@
319 330 }
320 331
321 332 $donor = $this->get_user()->get_donor();
322 333
323 - if ( array_key_exists( 'contact_consent', $_POST ) ) {
324 - $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
325 336 } elseif ( is_null( $donor ) ) {
326 337 $consent = false;
327 338 } else {
328 339 $consent = $donor->contact_consent;
@@ -335,17 +346,21 @@
335 346 *
336 347 * @param array $communication_fields List of fields.
337 348 * @param Charitable_Profile_Form $form Instance of `Charitable_Profile_Form`.
338 349 */
339 - $communication_fields = apply_filters( 'charitable_user_communication_preferences_fields', array(
340 - 'contact_consent' => array(
341 - 'type' => 'checkbox',
342 - 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
343 - 'priority' => 8,
344 - 'required' => false,
345 - '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 + ),
346 360 ),
347 - ), $this );
361 + $this
362 + );
348 363
349 364 if ( empty( $communication_fields ) ) {
350 365 return $fields;
351 366 }
@@ -351,16 +366,19 @@
351 366 }
352 367
353 368 uasort( $fields, 'charitable_priority_sort' );
354 369
355 - return array_merge( $fields, array(
356 - 'communication_fields' => array(
357 - 'legend' => __( 'Communication Preferences', 'charitable' ),
358 - 'type' => 'fieldset',
359 - 'fields' => $communication_fields,
360 - 'priority' => 60,
361 - ),
362 - ) );
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 + );
363 381 }
364 382
365 383 /**
366 384 * Profile fields to be displayed.
@@ -369,34 +387,38 @@
369 387 *
370 388 * @return array[]
371 389 */
372 390 public function get_fields() {
373 - $fields = apply_filters( 'charitable_user_profile_fields', array(
374 - 'user_fields' => array(
375 - 'legend' => __( 'Your Details', 'charitable' ),
376 - 'type' => 'fieldset',
377 - 'fields' => $this->get_user_fields(),
378 - '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 + ),
379 418 ),
380 - 'password_fields' => array(
381 - 'legend' => __( 'Change Your Password', 'charitable' ),
382 - 'type' => 'fieldset',
383 - 'fields' => $this->get_password_fields(),
384 - 'priority' => 10,
385 - ),
386 - 'address_fields' => array(
387 - 'legend' => __( 'Your Address', 'charitable' ),
388 - 'type' => 'fieldset',
389 - 'fields' => $this->get_address_fields(),
390 - 'priority' => 20,
391 - ),
392 - 'social_fields' => array(
393 - 'legend' => __( 'Your Social Profiles', 'charitable' ),
394 - 'type' => 'fieldset',
395 - 'fields' => $this->get_social_fields(),
396 - 'priority' => 40,
397 - ),
398 - ), $this );
419 + $this
420 + );
399 421
400 422 $fields = $this->maybe_get_communication_preferences_fields( $fields );
401 423
402 424 uasort( $fields, 'charitable_priority_sort' );
@@ -411,11 +433,14 @@
411 433 *
412 434 * @return array
413 435 */
414 436 public function get_hidden_fields() {
415 - return array_merge( parent::get_hidden_fields(), array(
416 - 'current_email' => $this->get_user()->get_email(),
417 - ) );
437 + return array_merge(
438 + parent::get_hidden_fields(),
439 + array(
440 + 'current_email' => $this->get_user()->get_email(),
441 + )
442 + );
418 443 }
419 444
420 445 /**
421 446 * The fields displayed on the password form.
@@ -424,29 +449,33 @@
424 449 *
425 450 * @return array[]
426 451 */
427 452 public function get_password_fields() {
428 - $password_fields = apply_filters( 'charitable_user_profile_password_fields', array(
429 - 'current_pass' => array(
430 - 'priority' => 2,
431 - 'type' => 'password',
432 - 'label' => __( 'Current Password (leave blank to leave unchanged)', 'charitable' ),
433 - 'value' => '',
434 - '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 + ),
435 475 ),
436 - 'user_pass' => array(
437 - 'priority' => 4,
438 - 'type' => 'password',
439 - 'label' => __( 'New Password (leave blank to leave unchanged)', 'charitable' ),
440 - 'required' => false,
441 - ),
442 - 'user_pass_repeat' => array(
443 - 'priority' => 6,
444 - 'type' => 'password',
445 - 'label' => __( 'New Password (again)', 'charitable' ),
446 - 'required' => false,
447 - ),
448 - ), $this );
476 + $this
477 + );
449 478
450 479 uasort( $password_fields, 'charitable_priority_sort' );
451 480
452 481 return $password_fields;
@@ -518,13 +547,13 @@
518 547 *
519 548 * @return boolean
520 549 */
521 550 public function is_changing_password() {
522 - 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
523 552 return false;
524 553 }
525 554
526 - 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
527 556 return false;
528 557 }
529 558
530 559 return true;
@@ -538,15 +567,15 @@
538 567 * @return boolean
539 568 */
540 569 public function validate_password_change() {
541 570 /* The current password must be correct. */
542 - 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
543 572 charitable_get_notices()->add_error( __( 'Current password is incorrect.', 'charitable' ) );
544 573 return false;
545 574 }
546 575
547 576 /* The new password must match the repeat (if set). */
548 - 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
549 578 charitable_get_notices()->add_error( __( 'New passwords did not match.', 'charitable' ) );
550 579 return false;
551 580 }
552 581
@@ -560,17 +589,17 @@
560 589 *
561 590 * @return boolean
562 591 */
563 592 public function is_changing_email() {
564 - 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
565 594 return false;
566 595 }
567 596
568 - if ( ! isset( $_POST['current_email'] ) ) {
569 - 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
570 599 }
571 600
572 - return $_POST['user_email'] != $_POST['current_email'];
601 + return $_POST['user_email'] !== $_POST['current_email']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
573 602 }
574 603
575 604 /**
576 605 * Validate the email change.
@@ -575,13 +604,22 @@
575 604 /**
576 605 * Validate the email change.
577 606 *
578 607 * @since 1.6.2
608 + * @version 1.8.9.1
579 609 *
580 610 * @return boolean
581 611 */
582 612 public function validate_email_change() {
583 - 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 ) ) {
584 622 charitable_get_notices()->add_error(
585 623 __( 'This email address is already in use.', 'charitable' )
586 624 );
587 625 return false;
@@ -623,9 +661,9 @@
623 661 __( 'charitable_user_profile_after_fields hook has been removed. Use charitable_form_after_fields instead.', 'charitable' ),
624 662 '1.4.0'
625 663 );
626 664
627 - if ( 'Charitable_Profile_Form' == get_class( $form ) ) {
665 + if ( 'Charitable_Profile_Form' == get_class( $form ) ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
628 666 do_action( 'charitable_user_profile_after_fields', $form );
629 667 }
630 668 }
631 669
@@ -631,10 +669,10 @@
631 669
632 670 /**
633 671 * Add avatar section to user profile form.
634 672 *
635 - * @param array[] $fields
636 - * @param Charitable_Profile_Form $form
673 + * @param array[] $fields The fields in the form.
674 + * @param Charitable_Profile_Form $form The profile form object.
637 675 * @return array[]
638 676 * @access public
639 677 * @since 1.0.0
640 678 */
@@ -646,17 +684,20 @@
646 684 if ( ! $avatar && charitable_user_has_gravatar( $user->user_email ) ) {
647 685 $avatar = get_avatar( $user->ID, 100 );
648 686 }
649 687
650 - $fields['avatar'] = apply_filters( 'charitable_user_avatar_field_args', array(
651 - 'label' => __( 'Your Profile Photo', 'charitable-user-avatar' ),
652 - 'type' => 'picture',
653 - 'uploader' => true,
654 - 'size' => 100,
655 - 'value' => $avatar,
656 - 'priority' => 14,
657 - 'fullwidth' => true,
658 - ) );
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 + );
659 700
660 701 return $fields;
661 702 }
662 703
@@ -670,43 +711,93 @@
670 711 * @access public
671 712 * @since 1.0.0
672 713 */
673 714 public function save_avatar( $submitted, $fields, $form ) {
674 - if ( isset( $_FILES ) && isset( $_FILES['avatar'] ) ) {
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 );
675 720
721 + if ( $has_upload ) {
676 722 $attachment_id = $form->upload_post_attachment( 'avatar', 0 );
677 723
678 - if ( ! is_wp_error( $attachment_id ) ) {
724 + if ( is_wp_error( $attachment_id ) ) {
725 + return $submitted;
726 + }
679 727
680 - $submitted['avatar'] = $attachment_id;
728 + $submitted['avatar'] = (int) $attachment_id;
681 729
682 - /* Delete the previously upload avatar. */
683 - $old_avatar = get_user_meta( $form->get_user()->ID, 'avatar', true );
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 + }
684 739
685 - if ( ! empty( $old_avatar ) ) {
740 + update_user_meta( $user_id, 'avatar', (int) $attachment_id );
686 741
687 - wp_delete_attachment( $old_avatar );
742 + return $submitted;
743 + }
688 744
689 - }
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;
690 757
691 - update_user_meta( $form->get_user()->ID, 'avatar', $attachment_id );
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 + }
692 766
693 - } else {
694 - /**
695 - * Handle image upload error.
696 - *
697 - * @todo
698 - */
699 - }//end if
700 - } elseif ( ! array_key_exists( 'avatar', $submitted ) ) {
767 + $posted = (int) $posted;
701 768
702 - /* The picture has been removed. */
703 - $submitted['avatar'] = '';
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 + }
704 775
705 - }//end if
776 + $submitted['avatar'] = $posted;
706 777
707 778 return $submitted;
708 779 }
709 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;
800 + }
710 801 }
711 802
712 803 endif;