PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 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
← All changes | inc/api/donations-api.php +83 -17 1.5.1 → 1.6.1 View file →
@@ -11,8 +11,9 @@
11 11 use SureDonation\Inc\Database\Tables\Donors;
12 12 use SureDonation\Inc\Emails\Email_Handler;
13 13 use SureDonation\Inc\Helper;
14 14 use SureDonation\Inc\Payments\Payment_Helper;
15 +use SureDonation\Inc\Pdf\Receipt_Generator;
15 16 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
16 17 use WP_Error;
17 18 use WP_REST_Request;
18 19 use WP_REST_Response;
@@ -541,8 +542,9 @@
541 542 'fees_covered',
542 543 'donation_type',
543 544 'is_anonymous',
544 545 'donor_comment',
546 + 'donor_comment_status',
545 547 'payment_status',
546 548 'gateway',
547 549 'transaction_id',
548 550 ];
@@ -662,8 +664,29 @@
662 664
663 665 $result = Donations::delete( $donation_id );
664 666
665 667 if ( ! $result ) {
668 + // A donation is kept, deliberately, when its receipt PDF could not
669 + // be removed, so that the pointer stays reachable for a retry
670 + // instead of the file being orphaned. That reads as an unexplained
671 + // failure unless it is named: the admin has to fix the filesystem,
672 + // not retry.
673 + //
674 + // Ask the helper again rather than inferring from the surviving
675 + // pointer. It is idempotent and reports whether a file is still
676 + // there, so this is the fact rather than a guess: a row whose
677 + // DELETE failed after its receipt was already removed would
678 + // otherwise be reported as an uploads-permissions problem.
679 + $donation = Donations::get( $donation_id );
680 +
681 + if ( is_array( $donation ) && ! Receipt_Generator::delete_receipt( Helper::get_string_value( $donation['receipt_pdf_url'] ?? '' ) ) ) {
682 + return new WP_Error(
683 + 'receipt_delete_failed',
684 + __( 'This donation was kept because its PDF receipt could not be removed from the uploads folder. Deleting the record on its own would leave the receipt behind. Check the permissions on the uploads folder, then try again.', 'suredonation' ),
685 + [ 'status' => 500 ]
686 + );
687 + }
688 +
666 689 return new WP_Error(
667 690 'delete_failed',
668 691 __( 'Failed to delete donation.', 'suredonation' ),
669 692 [ 'status' => 500 ]
@@ -1132,34 +1155,42 @@
1132 1155 * @since 0.0.1
1133 1156 */
1134 1157 private function get_donation_args( $required = true ) {
1135 1158 return [
1136 - 'campaign_id' => [
1159 + 'campaign_id' => [
1137 1160 'required' => $required,
1138 1161 'sanitize_callback' => 'absint',
1139 1162 ],
1140 - 'donor_name' => [
1163 + 'donor_name' => [
1141 1164 'sanitize_callback' => 'sanitize_text_field',
1142 1165 ],
1143 - 'donor_email' => [
1166 + 'donor_email' => [
1144 1167 'sanitize_callback' => 'sanitize_email',
1145 1168 ],
1146 - 'donor_phone' => [
1169 + 'donor_phone' => [
1147 1170 'sanitize_callback' => 'sanitize_text_field',
1148 1171 ],
1149 - 'amount' => [
1172 + 'amount' => [
1150 1173 'required' => $required,
1151 1174 'sanitize_callback' => static function ( $value ) {
1152 1175 return floatval( $value );
1153 1176 },
1154 1177 ],
1155 - 'fees_covered' => [
1178 + 'fees_covered' => [
1156 1179 'sanitize_callback' => static function ( $value ) {
1157 1180 return floatval( $value );
1158 1181 },
1159 1182 ],
1160 - 'donation_type' => [
1161 - 'default' => 'one-time',
1183 + // No 'default' on this or 'payment_status' below, deliberately. These args
1184 + // are shared with the update route, where WordPress fills an absent param
1185 + // with its declared default before the callback runs — so update_donation()'
1186 + // s `! is_null()` test passes and the field is written even though the
1187 + // client never sent it. A partial update (e.g. the Donor Comment panel
1188 + // sending only donor_comment_status) therefore reset payment_status to
1189 + // 'pending' and donation_type to 'one-time', un-completing the donation and
1190 + // downgrading a subscription. create_donation() supplies its own fallbacks
1191 + // (`?? 'pending'`, `?? 'one-time'`), so nothing depends on the defaults here.
1192 + 'donation_type' => [
1162 1193 'enum' => [ 'one-time', 'recurring', 'renewal' ],
1163 1194 'sanitize_callback' => 'sanitize_text_field',
1164 1195 'validate_callback' => static function ( $param ) {
1165 1196 return in_array( $param, [ 'one-time', 'recurring', 'renewal' ], true );
@@ -1164,25 +1195,48 @@
1164 1195 'validate_callback' => static function ( $param ) {
1165 1196 return in_array( $param, [ 'one-time', 'recurring', 'renewal' ], true );
1166 1197 },
1167 1198 ],
1168 - 'is_anonymous' => [
1199 + 'is_anonymous' => [
1169 1200 'sanitize_callback' => 'rest_sanitize_boolean',
1170 1201 ],
1171 - 'donor_comment' => [
1172 - 'sanitize_callback' => 'wp_kses_post',
1202 + 'donor_comment' => [
1203 + // sanitize_textarea_field, matching the capture path in
1204 + // Payment_Helper::get_mapped_donor_comment(). wp_kses_post() was
1205 + // actively destructive here: it parses anything tag-shaped, so a
1206 + // moderator saving the comment "a < b and 3 > 2" stored "a <b> 2"
1207 + // — losing " and 3 " — and any surviving markup then rendered as
1208 + // literal angle brackets, because the campaign page esc_html()s.
1209 + // Both sanitizers preserve the donor's newlines.
1210 + 'sanitize_callback' => 'sanitize_textarea_field',
1173 1211 ],
1174 - 'payment_status' => [
1175 - 'default' => 'pending',
1212 + 'donor_comment_status' => [
1213 + 'enum' => [ 'approved', 'pending', 'rejected' ],
1214 + 'sanitize_callback' => 'sanitize_text_field',
1215 + // A sanitize_callback silently disables `enum` enforcement, so the
1216 + // allowed set is checked here too — otherwise any string would reach
1217 + // the column and every comment would read as un-approved.
1218 + 'validate_callback' => static function ( $param ) {
1219 + return in_array( $param, Donations::get_valid_comment_statuses(), true );
1220 + },
1221 + ],
1222 + 'payment_status' => [
1176 1223 'type' => 'string',
1224 + // Deliberately no 'default'. These args are shared with the
1225 + // update route, and WordPress fills an absent param with its
1226 + // default before the callback runs — so update_donation()'s
1227 + // `! is_null()` test passes and the status is overwritten on a
1228 + // partial update the client never sent it in. dev carries the
1229 + // default; keeping it here would reinstate that bug. See
1230 + // Test_Donations_API::test_update_donation_ignores_unsent_fields().
1177 1231 'enum' => Donations::get_valid_statuses(),
1178 1232 'sanitize_callback' => 'sanitize_text_field',
1179 1233 'validate_callback' => 'rest_validate_request_arg',
1180 1234 ],
1181 - 'gateway' => [
1235 + 'gateway' => [
1182 1236 'sanitize_callback' => 'sanitize_text_field',
1183 1237 ],
1184 - 'transaction_id' => [
1238 + 'transaction_id' => [
1185 1239 'sanitize_callback' => 'sanitize_text_field',
1186 1240 ],
1187 1241 ];
1188 1242 }
@@ -1266,9 +1320,9 @@
1266 1320 // group is the parent block label (e.g. "Address") used to nest
1267 1321 // sub-fields on the entry screen; '' for standalone fields.
1268 1322 $submitted_fields = [];
1269 1323 if ( isset( $donation_data['fields'] ) && is_array( $donation_data['fields'] ) ) {
1270 - foreach ( $donation_data['fields'] as $field ) {
1324 + foreach ( $donation_data['fields'] as $slug => $field ) {
1271 1325 if ( ! is_array( $field ) ) {
1272 1326 continue;
1273 1327 }
1274 1328 // sanitize_text_field (not esc_html) for REST data: the values are
@@ -1274,8 +1328,11 @@
1274 1328 // sanitize_text_field (not esc_html) for REST data: the values are
1275 1329 // already sanitized at write time and React escapes on render, so
1276 1330 // esc_html here would double-encode (e.g. "Cats & Dogs" -> "Cats &amp; Dogs").
1277 1331 $submitted_fields[] = [
1332 + // The stored key. Labels are admin-editable and translatable;
1333 + // an add-on that presents a group its own way matches on this.
1334 + 'slug' => sanitize_text_field( Helper::get_string_value( $slug ) ),
1278 1335 'label' => sanitize_text_field( Helper::get_string_value( $field['label'] ?? '' ) ),
1279 1336 // Checkbox fields store a canonical untranslated token so the
1280 1337 // stored column stays locale-stable; it is translated here, on
1281 1338 // read, for the entry screen. Non-checkbox values pass through.
@@ -1305,9 +1362,18 @@
1305 1362 'refunded_amount' => Helper::get_float_value( $donation['refunded_amount'] ?? 0 ),
1306 1363 'currency' => esc_html( Helper::get_string_value( $donation['currency'] ?? 'USD' ) ),
1307 1364 'donation_type' => esc_html( Helper::get_string_value( $donation['donation_type'] ?? 'one-time' ) ),
1308 1365 'is_anonymous' => ! empty( $donation['is_anonymous'] ),
1309 - 'donor_comment' => wp_kses_post( Helper::get_string_value( $donation['donor_comment'] ?? '' ) ),
1366 + // Returned raw, unlike its neighbours. The only consumer is the React
1367 + // moderation panel, which renders it as a text child and so escapes it
1368 + // itself; and DonorCommentSection writes this value straight back on
1369 + // Save. Running it through wp_kses_post() here therefore did not
1370 + // protect anything — it parsed anything tag-shaped and the moderator
1371 + // persisted the parsed result, so "a < b and 3 > 2" was shown as
1372 + // "a <b> 2" and saved as "a 2". esc_html() would be just as wrong:
1373 + // the panel would display the entities rather than the donor's text.
1374 + 'donor_comment' => Helper::get_string_value( $donation['donor_comment'] ?? '' ),
1375 + 'donor_comment_status' => esc_html( Helper::get_string_value( $donation['donor_comment_status'] ?? 'approved' ) ),
1310 1376 'payment_status' => esc_html( Helper::get_string_value( $donation['payment_status'] ?? 'pending' ) ),
1311 1377 'payment_mode' => esc_html( Helper::get_string_value( $payment_mode ) ),
1312 1378 'gateway' => esc_html( Helper::get_string_value( $donation['gateway'] ?? '' ) ),
1313 1379 'transaction_id' => esc_html( Helper::get_string_value( $donation['transaction_id'] ?? '' ) ),