PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
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 / admin / campaign-builder / ajax-actions.php

ajax-actions.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13.1, at includes/admin/campaign-builder/ajax-actions.php

1,464 lines 59.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Exit if accessed directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * Ajax actions related to the campaign builder used in by admin.
10 *
11 * @package Charitable
12 * @since 1.8.0
13 * @version 1.8.9.1
14 * @version 1.8.13.1 Authorization fixes; see charitable_builder_require_editable_campaign().
15 */
16
17 /**
18 * Require that the current user may edit a specific campaign, not just campaigns in general.
19 *
20 * The builder's AJAX handlers verified a nonce and the BLANKET `edit_campaigns` capability, then
21 * acted on a post id taken straight from the request, with no `current_user_can( 'edit_post', $id )`
22 * and no post-type check. That is horizontal privilege escalation between campaign authors, and it
23 * also let campaign meta be written onto unrelated post types.
24 *
25 * A post id of 0 is legitimate and returns 0 rather than failing: a campaign being built for the
26 * first time has not been saved yet, so there is no post to authorize against.
27 *
28 * Sends a JSON error and exits when the id names a post the caller may not edit, so callers can
29 * simply invoke it and carry on.
30 *
31 * Ported from Pro's `a8f1d3d00a` (1.8.18), which generalised the pattern from the one handler that
32 * already did it correctly.
33 *
34 * @since 1.8.13.1
35 *
36 * @param mixed $campaign_id Post id from the request.
37 * @return int The validated campaign id, or 0 for an unsaved campaign.
38 */
39 function charitable_builder_require_editable_campaign( $campaign_id ) {
40
41 $campaign_id = absint( $campaign_id );
42
43 if ( ! $campaign_id ) {
44 return 0;
45 }
46
47 if ( 'campaign' !== get_post_type( $campaign_id ) || ! current_user_can( 'edit_post', $campaign_id ) ) {
48 wp_send_json_error( esc_html__( 'You are not allowed to edit this campaign.', 'charitable' ) );
49 }
50
51 return $campaign_id;
52 }
53
54 /**
55 * Get and return template data via ajax.
56 *
57 * @since 1.8.0
58 */
59 function charitable_get_campaign_builder_template_data() {
60
61 if ( empty( $_POST['id'] ) ) { // phpcs:ignore
62 wp_send_json_error( esc_html__( 'Something went wrong while performing this action.', 'charitable' ) );
63 }
64
65 $builder_template = new Charitable_Campaign_Builder_Templates();
66 $template_id = ! empty( $_POST['id'] ) ? esc_attr( $_POST['id'] ) : false; // phpcs:ignore
67
68 if ( ! $template_id ) {
69 wp_send_json_error( esc_html__( 'Something went wrong while performing this action.', 'charitable' ) );
70 }
71
72 // get the title if passed so we can create a temp $campaign_data array to custom populare a template down the road.
73 $campaign_title = ! empty( $_POST['title'] ) ? esc_html( $_POST['title'] ) : false; // phpcs:ignore
74 $campaign_data = array();
75 if ( $campaign_title ) {
76 $campaign_data['title'] = $campaign_title;
77 }
78
79 $no_cache = apply_filters( 'charitable_campaign_builder_ajax_template_no_cache', true );
80
81 $template_data = $builder_template->get_template_data( $template_id, $campaign_data, $no_cache );
82
83 if ( $template_data ) {
84
85 wp_send_json_success( $template_data );
86
87 } else {
88
89 wp_send_json_error( esc_html__( 'Something went wrong while attempting to get template information.', 'charitable' ) );
90 }
91 }
92 add_action( 'wp_ajax_charitable_get_campaign_builder_template_data', 'charitable_get_campaign_builder_template_data' );
93
94 /**
95 * Get and return template preview via ajax.
96 *
97 * @since 1.8.0
98 */
99 function charitable_get_campaign_builder_template_preview() {
100
101 if ( empty( $_POST['id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
102 wp_send_json_error( esc_html__( 'Something went wrong while performing this action.', 'charitable' ) );
103 }
104
105 $template_id = intval( $_POST['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
106
107 if ( 0 === $template_id ) {
108 wp_send_json_error( esc_html__( 'Something went wrong with getting the template while performing this action.', 'charitable' ) );
109 }
110
111 $builder_template = new Charitable_Campaign_Builder_Templates();
112 $template_preview = $builder_template->get_template_preview( $template_id );
113
114 if ( $template_preview ) {
115
116 $response = array( 'html' => $template_preview );
117
118 wp_send_json_success( $response );
119
120 } else {
121
122 wp_send_json_error( esc_html__( 'Something went wrong while attempting to get the template information.', 'charitable' ) );
123
124 }
125 }
126 add_action( 'wp_ajax_charitable_get_campaign_builder_template_preview', 'charitable_get_campaign_builder_template_preview' );
127
128
129 /**
130 * Save a campaign via ajax.
131 *
132 * @since 1.8.0
133 */
134 function charitable_save_campaign() {
135
136 // Run a security check.
137 if ( ! check_ajax_referer( 'charitable-builder', 'nonce', false ) ) {
138 wp_send_json_error( esc_html__( 'Your session expired. Please reload the builder.', 'charitable' ) );
139 }
140
141 // Check for permissions.
142 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
143 wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'charitable' ) );
144 }
145
146 // Check for form data.
147 if ( empty( $_POST['data'] ) ) {
148 wp_send_json_error( esc_html__( 'Something went wrong while performing this action.', 'charitable' ) );
149 }
150
151 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, decoded and sanitized in loop
152 // $campaign_post = json_decode( wp_unslash( $_POST['data'] ), ARRAY_A );
153 $campaign_post = array_column( json_decode( wp_unslash( $_POST['data'] ), ARRAY_A ), null, 'name' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, decoded and sanitized in loop
154
155 /*
156 * Check the caller may edit THIS campaign, not just campaigns in general (1.8.13.1).
157 *
158 * This runs as early as the id is known, and before any of the data below is consumed, so a
159 * caller who is not authorised cannot reach the write path at all. Further down, this handler
160 * reaches update_post_meta(), wp_update_post() and wp_set_post_terms() on the same id.
161 *
162 * Note the id lives in the decoded `data` payload as $campaign_post['id']['value'], NOT in
163 * $_POST['id'] the way Pro's copy of this handler receives it -- which is why Pro's one-line
164 * call could not be copied verbatim.
165 */
166 charitable_builder_require_editable_campaign(
167 isset( $campaign_post['id']['value'] ) ? $campaign_post['id']['value'] : 0
168 );
169 $is_preview = isset( $_POST['preview'] ) && 'true' === $_POST['preview'] ? true : false;
170 $data = [
171 'fields' => [],
172 ];
173
174 $campaign_settings_v2 = array();
175
176 // Note: We have to first get the layout from the template because the incoming data is only the fields the user has placed into the form.
177 // If we don't do this, if the user doesn't put anything into a particular column or row, that won't be included in the POST and we'll lose that data.
178
179 $campaign_settings_v2 = charitable_template_layout_to_campaign_layout( $campaign_settings_v2, $campaign_post );
180
181 // this tracks where tabs might be in the layout.
182 $campaign_tabs_location = array();
183
184 if ( $campaign_post ) {
185
186 foreach ( $campaign_post as $post_field_name => $post_field_data ) {
187
188 // This process has to check for various formats of fields that appear in the form on the screen hence the multiple if statements.
189
190 if ( strpos( $post_field_name, '__' ) !== false && strpos( $post_field_name, 'xxx' ) === false ) { // this covers fields that are like EXAMPLE__EXAMPLE__EXAMPLE.
191
192 $array_level = explode( '__', $post_field_name );
193 if ( count( $array_level ) === 4 ) {
194 $campaign_settings_v2[ $array_level[0] ][ $array_level[1] ][ $array_level[2] ][ $array_level[3] ] = $post_field_data['value'];
195 } elseif ( count( $array_level ) === 3 ) {
196 $campaign_settings_v2[ $array_level[0] ][ $array_level[1] ][ $array_level[2] ] = $post_field_data['value'];
197 } elseif ( count( $array_level ) === 2 ) {
198 $campaign_settings_v2[ $array_level[0] ][ $array_level[1] ] = $post_field_data['value'];
199 } elseif ( count( $array_level ) === 2 ) {
200 $campaign_settings_v2[ $array_level[0] ] = $post_field_data['value'];
201 }
202
203 } elseif ( strpos( $post_field_name, '[donation_amounts]' ) !== false && strpos( $post_field_name, '[campaign_suggested_donations_default]' ) !== false ) {
204
205 $campaign_settings_v2['settings']['donation-options']['suggested_donations_default'] = $post_field_data['value'];
206
207 } elseif ( strpos( $post_field_name, 'donation_amounts' ) !== false && strpos( $post_field_name, '_fields' ) !== false ) {
208
209 continue;
210
211 } elseif ( strpos( $post_field_name, '[donation_amounts]' ) !== false && strpos( $post_field_name, '[campaign_suggested_donations_default]' ) === false ) {
212
213 $post_field_name = str_replace( 'settings]', '', $post_field_name );
214 $post_field_name = str_replace( '[donation-options]', '', $post_field_name );
215 $post_field_name = str_replace( '[donation_amounts]', '', $post_field_name );
216
217 $output = charitable_convert_campaign_string_to_array( $post_field_name, $post_field_data['value'] );
218 $_tmp_array = $output[ array_key_first( $output ) ];
219
220 $first_key = esc_attr( array_key_first( $output ) );
221 $second_key = esc_attr( array_key_first( $_tmp_array ) );
222
223 // this would avoid field 0 which should be a hidden field for the purpose of adding dynamic rows via Javascript.
224 if ( 0 === intval( $first_key ) ) {
225 continue;
226 }
227
228 $value = ( 'amount' === esc_attr( $second_key ) ) ? charitable_sanitize_amount( $post_field_data['value'] ) : wp_strip_all_tags( $post_field_data['value'] );
229
230 // if ( intval( $first_key ) !== 0 ) { // this would avoid field 0 which should be a hidden field for the purpose of adding dynamic rows via Javascript.
231 $campaign_settings_v2['settings']['donation-options']['donation_amounts'][ $first_key ][ $second_key ] = $value;
232 // }
233
234 } elseif ( strpos( $post_field_name, '[recurring_donation_amounts]' ) !== false && strpos( $post_field_name, '[campaign_suggested_recurring_donations_default]' ) !== false ) {
235
236 $campaign_settings_v2['settings']['donation-options']['suggested_recurring_donations_default'] = $post_field_data['value'];
237
238 } elseif ( strpos( $post_field_name, '[recurring_donation_amounts]' ) !== false && strpos( $post_field_name, '[campaign_suggested_recurring_donations_default]' ) === false ) {
239
240 $post_field_name = str_replace( 'settings]', '', $post_field_name );
241 $post_field_name = str_replace( '[donation-options]', '', $post_field_name );
242 $post_field_name = str_replace( '[recurring_donation_amounts]', '', $post_field_name );
243
244 $output = charitable_convert_campaign_string_to_array( $post_field_name, $post_field_data['value'] );
245 $_tmp_array = $output[ array_key_first( $output ) ];
246
247 $first_key = esc_attr( array_key_first( $output ) );
248 $second_key = esc_attr( array_key_first( $_tmp_array ) );
249
250 // this would avoid field 0 which should be a hidden field for the purpose of adding dynamic rows via Javascript.
251 if ( 0 === intval( $first_key ) ) {
252 continue;
253 }
254
255 $value = ( 'amount' === esc_attr( $second_key ) ) ? charitable_sanitize_amount( $post_field_data['value'] ) : wp_strip_all_tags( $post_field_data['value'] );
256
257 // if ( intval( $first_key ) !== 0 ) { // this would avoid field 0 which should be a hidden field for the purpose of adding dynamic rows via Javascript.
258 $campaign_settings_v2['settings']['donation-options']['recurring_donation_amounts'][ $first_key ][ $second_key ] = $value;
259 // }
260
261 } elseif ( strpos( $post_field_name, '[recurring_donation_periods_to_display]' ) !== false ) {
262
263 $post_field_name = str_replace( 'settings[', '', $post_field_name );
264 $post_field_name = str_replace( 'donation-options]', '', $post_field_name );
265 $post_field_name = str_replace( '[recurring_donation_periods_to_display]', '', $post_field_name );
266
267 $output = charitable_convert_campaign_string_to_array_new( $post_field_name, $post_field_data['value'] );
268
269 $first_key = esc_attr( array_key_first( $output ) );
270 $value = wp_strip_all_tags( $post_field_data['value'] );
271
272 $campaign_settings_v2['settings']['donation-options']['recurring_donation_periods_to_display'][ $first_key ] = $value;
273
274 } elseif ( strpos( $post_field_name, '[row]' ) !== false && strpos( $post_field_name, '[column]' ) !== false && strpos( $post_field_name, '[row-type-tabs]' ) === false ) {
275
276 $post_field_name = str_replace( 'layout[row]', '', $post_field_name );
277 $post_field_name = str_replace( '[column]', '', $post_field_name );
278 $post_field_name = str_replace( '[section]', '', $post_field_name );
279 $post_field_name = str_replace( '[fields]', '', $post_field_name );
280
281 $output = charitable_convert_campaign_string_to_array_new( $post_field_name, $post_field_data['value'], 'photo' );
282
283 $row_type = array_key_first( $output );
284 $row_id = array_key_first( $output[ $row_type ] );
285 $row_css = array_key_first( $output[ $row_type ][ $row_id ] );
286 $column_id = array_key_first( $output[ $row_type ][ $row_id ][ $row_css ] );
287 $section_type = array_key_first( $output[ $row_type ][ $row_id ][ $row_css ][ $column_id ] );
288 $section_id = array_key_first( $output[ $row_type ][ $row_id ][ $row_css ][ $column_id ][ $section_type ] );
289
290 $row_type = str_replace( 'row-type-', '', $row_type );
291 $section_type = str_replace( 'section-type-', '', $section_type );
292
293 if ( 'row' === $row_type || 'header' === $row_type ) {
294
295 $campaign_settings_v2['layout']['rows'][ $row_id ]['type'] = ( false !== $row_type ) ? esc_attr( $row_type ) : 'row';
296
297 if ( 'tabs' === $section_type ) {
298
299 $tab_id = array_key_first( $output[ 'row-type-' . $row_type ][ $row_id ][ $row_css ][ $column_id ][ 'section-type-' . $section_type ][ $section_id ]['tabs'] );
300 $field_id_key = array_key_first( $output[ 'row-type-' . $row_type ][ $row_id ][ $row_css ][ $column_id ][ 'section-type-' . $section_type ][ $section_id ]['tabs'][ $tab_id ] );
301
302 $campaign_settings_v2['layout']['rows'][ $row_id ]['type'] = $row_type;
303 $campaign_settings_v2['layout']['rows'][ $row_id ]['css_class'] = esc_attr( $row_css );
304 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['type'] = 'tabs';
305 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['tabs'][ $tab_id ]['title'] = $campaign_post[ 'tabs__' . $tab_id . '__title' ]['value'];
306 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['tabs'][ $tab_id ]['type'] = 'fields';
307 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['tabs'][ $tab_id ]['slug'] = '';
308 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['tabs'][ $tab_id ]['fields'][] = $field_id_key;
309
310 $campaign_tabs_location = array(
311 'row_id' => $row_id,
312 'column_id' => $column_id,
313 'section_id' => $section_id,
314 );
315
316 } elseif ( 'fields' === $section_type ) {
317
318 $field_id_key = array_key_first( $output[ 'row-type-' . $row_type ][ $row_id ][ $row_css ][ $column_id ][ 'section-type-' . $section_type ][ $section_id ] );
319
320 $campaign_settings_v2['layout']['rows'][ $row_id ]['type'] = $row_type;
321 $campaign_settings_v2['layout']['rows'][ $row_id ]['css_class'] = esc_attr( $row_css );
322 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['type'] = 'fields';
323 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['fields'][] = $field_id_key;
324
325 }
326
327 $campaign_settings_v2['layout']['rows'][ $row_id ]['fields'][ $field_id_key ] = $post_field_data['value'];
328
329 }
330
331 } elseif ( strpos( $post_field_name, '[row]' ) !== false && strpos( $post_field_name, '[blocks]' ) !== false && strpos( $post_field_name, '[row-type-tabs]' ) !== false ) {
332
333 $post_field_name = str_replace( 'layout[row][row-type-tabs][999]', '', $post_field_name );
334 $post_field_name = str_replace( '[blocks]', '', $post_field_name );
335
336 $output = charitable_convert_campaign_string_to_array( $post_field_name, $post_field_data['value'] );
337
338 $_tmp_array = $output[ array_key_first( $output ) ];
339
340 $tab_key = array_key_first( $output );
341 $field_key = array_key_first( $_tmp_array );
342
343 $campaign_settings_v2['layout']['rows'][999]['type'] = 'tabs';
344 $campaign_settings_v2['layout']['rows'][999]['fields'][ $field_key ] = $post_field_data['value'];
345 $campaign_settings_v2['layout']['rows'][999]['tabs'][ $tab_key ][] = $field_key;
346
347 } elseif ( strpos( $post_field_name, '_fields' ) !== false ) {
348
349 $post_field_name = str_replace( '_fields', '', $post_field_name );
350
351 $output = charitable_convert_campaign_string_to_array( $post_field_name, $post_field_data['value'] );
352
353 $_tmp_array = $output[ array_key_first( $output ) ];
354 $_tmp_array_2 = is_array( $_tmp_array ) ? $_tmp_array[ array_key_first( $_tmp_array ) ] : false;
355
356 $first_key = array_key_first( $output );
357 $second_key = array_key_first( $_tmp_array );
358 $third_key = $_tmp_array_2 !== false && is_array( $_tmp_array_2 ) && '' !== array_key_first( $_tmp_array_2 ) ? array_key_first( $_tmp_array_2 ) : false;
359
360 // Santitize here.
361 // todo: this needs to be cleaned up.
362 if ( in_array( $second_key, array( 'css_class' ), true ) ) {
363 // Remove all characters from this string that would not make it a valid css value or attribute.
364 $post_field_data['value'] = str_replace( array( '"', "'", '`' ), '', $post_field_data['value'] );
365 $post_field_data['value'] = preg_replace( '/[^a-zA-Z0-9_,\/$:\-\.!\s;]/', '', $post_field_data['value'] );
366 $post_field_data['value'] = esc_attr( $post_field_data['value'] );
367 } elseif ( in_array( $second_key, array( 'background_image' ), true ) ) {
368 $post_field_data['value'] = esc_url_raw( $post_field_data['value'] );
369 } elseif ( in_array( $second_key, array( 'locked' ), true ) ) {
370 $post_field_data['value'] = ! empty( $post_field_data['value'] ) ? 1 : 0;
371 }
372
373 if ( null !== $third_key && false !== $third_key && '' !== trim( $third_key ) ) {
374 $campaign_settings_v2['fields'][ $first_key ][ $second_key ][ $third_key ] = $post_field_data['value'];
375 } else {
376 $campaign_settings_v2['fields'][ $first_key ][ $second_key ] = $post_field_data['value'];
377 }
378
379 } elseif ( strpos( $post_field_name, '][' ) !== false ) { // this covers EXAMPLE[EXAMPLE][EXAMPLE].
380
381 $output = array();
382 $pointer = &$output;
383 $input = $post_field_name;
384 $counter = ( substr_count( $input, '[' ) ) - 1;
385 while ( ( $index = strpos( $input, '[' ) ) !== false ) {
386 if ( $index != 0 ) {
387 $key = substr( $input, 0, $index );
388 $pointer[ $key ] = array();
389 $pointer = &$pointer[ $key ];
390 $input = substr( $input, $index );
391 continue;
392 }
393 $end_index = strpos( $input, ']' );
394 $array_key = substr( $input, $index + 1, $end_index - 1 );
395 if ( $counter <= 1 ) {
396 $pointer[ $array_key ] = array();
397 } elseif ( is_array( $pointer ) ) {
398 $pointer[ $array_key ] = isset( $post_field_data['value'] ) ? $post_field_data['value'] : false;
399 }
400 if ( is_array( $pointer ) ) {
401 $pointer = &$pointer[ $array_key ];
402 }
403 $input = substr( $input, $end_index + 1 );
404 ++$counter;
405 }
406
407 $campaign_settings_v2 = array_merge_recursive( $campaign_settings_v2, $output );
408
409 } elseif ( strpos( $post_field_name, 'xxx' ) === false ) {
410
411 $campaign_settings_v2[ $post_field_name ] = $post_field_data['value'];
412
413 }
414
415 }
416 }
417
418 if ( empty( $campaign_tabs_location ) ) {
419 // either there is NO tabs in this template, or there's at least one tab but has no fields in it. Let's got hunting for tabs.
420 foreach ( $campaign_settings_v2['layout']['rows'] as $row_id => $row ) {
421 foreach ( $row['columns'] as $column_id => $column ) {
422 foreach ( $column['sections'] as $section_id => $section ) {
423 if ( 'tabs' === $section['type'] ) {
424 $campaign_tabs_location = array(
425 'row_id' => $row_id,
426 'column_id' => $column_id,
427 'section_id' => $section_id,
428 );
429 break;
430 }
431 }
432 }
433 }
434 }
435
436 // We still check the tabs location because the above might not have found any tabs (likely because on purpose the template doesn't have any).
437 if ( ! empty( $campaign_tabs_location ) ) {
438 // get the list of current tabs in the layout from what we remembered/recorded from the location when we when through things above.
439 $campaign_tabs = $campaign_settings_v2['layout']['rows'][ $campaign_tabs_location['row_id'] ]['columns'][ $campaign_tabs_location['column_id'] ]['sections'][ $campaign_tabs_location['section_id'] ]['tabs'];
440 if ( ! empty( $campaign_tabs ) && ! empty( $campaign_settings_v2['tabs'] ) ) {
441 foreach ( $campaign_settings_v2['tabs'] as $settings_campaign_tab_id => $settings_campaign_tab ) {
442 // if there is no tab in the tabs field in a section, but there's a tab registered in the "settings" tab area, then add it to the layout (likely an empty tab?).
443 if ( ! isset( $campaign_tabs[ $settings_campaign_tab_id ] ) || empty( $campaign_tabs[ $settings_campaign_tab_id ] ) ) {
444 // tab section - ensure even empty tabs are included.
445 $campaign_settings_v2['layout']['rows'][ $campaign_tabs_location['row_id'] ]['columns'][ $campaign_tabs_location['column_id'] ]['sections'][ $campaign_tabs_location['section_id'] ]['tabs'][ $settings_campaign_tab_id ] = array(
446 'title' => $settings_campaign_tab['title'],
447 'type' => 'fields',
448 'slug' => '',
449 'fields' => array(),
450 );
451 }
452 }
453 }
454 }
455
456 // Remove any tabs in the layout tab section that aren't in the main tabs directory.
457 if ( ! empty( $campaign_tabs_location ) ) {
458 $campaign_tabs = $campaign_settings_v2['layout']['rows'][ $campaign_tabs_location['row_id'] ]['columns'][ $campaign_tabs_location['column_id'] ]['sections'][ $campaign_tabs_location['section_id'] ]['tabs'];
459 foreach ( $campaign_tabs as $section_tab_id => $campaign_tab ) {
460 // if there is a tab in the tabs field in a section, but there's no tab registered in the "settings" tab area, then remove it from the layout.
461 if ( ! isset( $campaign_settings_v2['tabs'][ $section_tab_id ] ) || empty( $campaign_settings_v2['tabs'][ $section_tab_id ] ) ) {
462 unset( $campaign_tabs[ $section_tab_id ] );
463 }
464 }
465 $campaign_settings_v2['layout']['rows'][ $campaign_tabs_location['row_id'] ]['columns'][ $campaign_tabs_location['column_id'] ]['sections'][ $campaign_tabs_location['section_id'] ]['tabs'] = $campaign_tabs;
466 }
467
468 // If there are tabs in the section/layout but the tab directory is empty, we need to clear out the section/layout tabs.
469 if ( ! empty( $campaign_tabs_location ) && empty( $campaign_settings_v2['tabs'] ) ) {
470 $campaign_settings_v2['layout']['rows'][ $campaign_tabs_location['row_id'] ]['columns'][ $campaign_tabs_location['column_id'] ]['sections'][ $campaign_tabs_location['section_id'] ]['tabs'] = array();
471 }
472
473 // tags.
474 $custom_tax_terms = array();
475 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, decoded and sanitized in loop
476 if ( ! empty( $_POST['data'] ) ) {
477 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, decoded and sanitized in loop
478 $_tmp_data = json_decode( wp_unslash( $_POST['data'] ) );
479 foreach ( $_tmp_data as $index => $object ) {
480 if ( $object->name === 'settings[general][tags]' ) {
481 $custom_tax_terms[] = intval( $object->value );
482 }
483 }
484 $campaign_settings_v2['settings']['general']['tags'] = $custom_tax_terms;
485 }
486
487 // categories.
488 $custom_cat_terms = array();
489 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, decoded and sanitized in loop
490 if ( ! empty( $_POST['data'] ) ) {
491 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, decoded and sanitized in loop
492 $_tmp_data = json_decode( wp_unslash( $_POST['data'] ) );
493 foreach ( $_tmp_data as $index => $object ) {
494 if ( strpos( $object->name, 'categories-' ) !== false ) {
495 $custom_cat_terms[] = intval( $object->value );
496 }
497 }
498 }
499
500 // Starting cleaning up.
501
502 // Remove any donation option that doesn't have an amount.
503 if ( isset( $campaign_settings_v2['settings']['donation-options']['donation_amounts'] ) ) {
504 foreach ( $campaign_settings_v2['settings']['donation-options']['donation_amounts'] as $key => $info ) {
505 if ( isset( $info['amount'] ) && 0 === intval( $info['amount'] ) ) {
506 unset( $campaign_settings_v2['settings']['donation-options']['donation_amounts'][ $key ] );
507 }
508 }
509 }
510
511 // Remove any fields that are associated with feedback forms.
512 foreach ( $campaign_settings_v2 as $key => $info ) {
513 if ( strpos( $key, '-feedback-form' ) !== false ) {
514 unset( $campaign_settings_v2[ $key ] );
515 }
516 }
517
518 // convert date formats.
519 if ( isset( $campaign_settings_v2['settings']['general']['end_date'] ) ) {
520 $campaign_settings_v2['settings']['general']['end_date'] = Charitable_Campaign::sanitize_campaign_end_date( $campaign_settings_v2['settings']['general']['end_date'] );
521 }
522
523 // Sanitize some misc fields.
524 $campaign_settings_v2['settings']['general']['goal'] = ! empty( $campaign_settings_v2['settings']['general']['goal'] ) ? Charitable_Currency::get_instance()->sanitize_monetary_amount( (string) $campaign_settings_v2['settings']['general']['goal'] ) : false;
525 $campaign_settings_v2['settings']['general']['goal'] = '0' == $campaign_settings_v2['settings']['general']['goal'] ? '' : $campaign_settings_v2['settings']['general']['goal'];
526
527 // if the money values are negative, make it positive.
528 if ( ! empty( $campaign_settings_v2['settings']['general']['goal'] ) && '-' === substr( $campaign_settings_v2['settings']['general']['goal'], 0, 1 ) ) {
529 $campaign_settings_v2['settings']['general']['goal'] = substr( $campaign_settings_v2['settings']['general']['goal'], 1 );
530 }
531 if ( ! empty( $campaign_settings_v2['settings']['donation-options']['minimum_donation_amount'] ) && '-' === substr( $campaign_settings_v2['settings']['donation-options']['minimum_donation_amount'], 0, 1 ) ) {
532 $campaign_settings_v2['settings']['donation-options']['minimum_donation_amount'] = substr( $campaign_settings_v2['settings']['donation-options']['minimum_donation_amount'], 1 );
533 }
534
535 $campaign_settings_v2['field_id'] = charitable_find_highest_field_key( $campaign_settings_v2 );
536
537 // Update OLD v1 campaign data.
538 if ( intval( $campaign_post['id']['value'] ) === 0 ) {
539 // new campaign.
540 $campaign_id = charitable_create_campaign(
541 array(
542 'title' => isset( $campaign_post['title']['value'] ) ? $campaign_post['title']['value'] : false,
543 'description' => isset( $campaign_post['setings_desc']['value'] ) ? $campaign_post['setings_desc']['value'] : false,
544 )
545 );
546 } else {
547 // existing campaign.
548 $campaign_id = charitable_create_campaign(
549 array(
550 'ID' => isset( $campaign_post['id']['value'] ) ? $campaign_post['id']['value'] : false,
551 'title' => isset( $campaign_post['title']['value'] ) ? $campaign_post['title']['value'] : false,
552 'description' => isset( $campaign_post['setings_desc'] ) ? $campaign_post['setings_desc']['value'] : false,
553 )
554 );
555 }
556
557 // Update campaign data for v1.
558 $processor = new Charitable_Campaign_Processor();
559 $processor->campaign_id = $campaign_id;
560
561 // Base meta values.
562 $base_meta_keys = array(
563 'goal' => 0,
564 'end_date' => 0,
565 'suggested_donations' => array(),
566 'suggested_donations_default' => 0,
567 'allow_custom_donations' => 0,
568 'minimum_donation_amount' => 0,
569 'donate_button_text' => '',
570 ); // taken from class-charitable-campaign-processor.php.
571 foreach ( $base_meta_keys as $base_meta_key => $base_meta_value ) {
572 $value = charitable_search_settings( $campaign_settings_v2['settings'], $base_meta_key );
573 if ( $value ) {
574 $processor->save_meta_field( $value, $base_meta_key );
575 } else {
576 $processor->save_meta_field( $base_meta_value, $base_meta_key );
577 }
578 }
579
580 if ( ! empty( $campaign_settings_v2['settings']['donation-options']['donation_amounts'] ) ) {
581 update_post_meta( $campaign_id, '_campaign_suggested_donations', ( $campaign_settings_v2['settings']['donation-options']['donation_amounts'] ) );
582 } else {
583 update_post_meta( $campaign_id, '_campaign_suggested_donations', ( array() ) );
584 }
585
586 if ( ! empty( $campaign_settings_v2['settings']['donation-options']['suggested_donations_default'] ) ) {
587 $suggest_donation_row_id = intval( $campaign_settings_v2['settings']['donation-options']['suggested_donations_default'] );
588 $array_to_serialize = array( html_entity_decode( charitable_format_money( $campaign_settings_v2['settings']['donation-options']['donation_amounts'][ $suggest_donation_row_id ]['amount'] ) ) );
589 update_post_meta( $campaign_id, '_campaign_suggested_donations_default', $array_to_serialize );
590 } else {
591 update_post_meta( $campaign_id, '_campaign_suggested_donations_default', ( serialize( array() ) ) );
592 }
593
594 /* descriptions */
595 if ( ! empty( $campaign_settings_v2['settings']['general']['description'] ) ) {
596 update_post_meta( $campaign_id, '_campaign_description', trim( $campaign_settings_v2['settings']['general']['description'] ) ); // todo: santitize this.
597 } else {
598 update_post_meta( $campaign_id, '_campaign_description', false );
599 }
600 if ( ! empty( $campaign_settings_v2['settings']['general']['extended_description'] ) ) {
601 $campaign_post_update = array(
602 'ID' => $campaign_id,
603 'post_content' => $campaign_settings_v2['settings']['general']['extended_description'],
604 );
605 } elseif ( ! empty( $campaign_settings_v2['settings']['general']['description'] ) ) {
606 $campaign_post_update = array(
607 'ID' => $campaign_id,
608 'post_content' => $campaign_settings_v2['settings']['general']['description'],
609 );
610 } else {
611 $campaign_post_update = array(
612 'ID' => $campaign_id,
613 'post_content' => '',
614 );
615 }
616
617 /* update campaign author which should be the campaign_creator_id, if it exists (otherwise leave it alone) */
618 if ( ! empty( $campaign_settings_v2['settings']['general']['campaign_creator_id'] ) ) {
619 $campaign_post_update['post_author'] = intval( $campaign_settings_v2['settings']['general']['campaign_creator_id'] );
620 }
621
622 /* tags */
623 wp_set_post_terms( $campaign_id, $custom_tax_terms, 'campaign_tag' );
624
625 /* categories */
626 wp_set_post_terms( $campaign_id, $custom_cat_terms, 'campaign_category' );
627
628 // Update the post status of the campaign.
629 $post_status = false;
630 // In the next statement, false means simply nothing was passed and if that happens we don't change anything.
631 $desired_post_status = isset( $_POST['status'] ) ? esc_attr( $_POST['status'] ) : false; // phpcs:ignore
632
633 if ( $desired_post_status ) {
634
635 switch ( $desired_post_status ) {
636 case 'draft':
637 case 'pending':
638 case 'publish':
639 case 'private':
640 case 'future':
641 $post_status = $desired_post_status;
642 break;
643
644 case 'switch-draft':
645 $post_status = 'draft';
646 break;
647
648 case 'switch-review':
649 $post_status = 'pending';
650 break;
651
652 default:
653 break;
654 }
655
656 if ( $post_status ) {
657 $campaign_post_update['post_status'] = apply_filters( 'charitable_campaign_builder_post_status_on_save', $post_status, $campaign_id );
658 }
659
660 $campaign_settings_v2['post_status'] = $post_status;
661 $campaign_settings_v2['post_status_label'] = ucfirst( $post_status );
662
663 }
664
665 // IF this isn't a preview request, update the post into the database.
666 if ( ! $is_preview ) :
667 wp_update_post( $campaign_post_update );
668 endif;
669
670 /* donation summary */
671 $summary_hide_keys = array(
672 'campaign_hide_amount_donated',
673 'campaign_hide_number_of_donors',
674 'campaign_hide_percent_raised',
675 'campaign_hide_time_remaining',
676 );
677
678 if ( ! empty( $campaign_settings_v2['settings']['campaign-summary'] ) ) {
679 foreach ( $summary_hide_keys as $meta_key ) {
680 if ( ! empty( $campaign_settings_v2['settings']['campaign-summary'][ $meta_key ] ) ) {
681 update_post_meta( $campaign_id, '_' . $meta_key, array( str_replace( 'campaign_', '', $meta_key ) ) );
682 } else {
683 update_post_meta( $campaign_id, '_' . $meta_key, '' );
684 }
685 }
686 } else {
687 foreach ( $summary_hide_keys as $meta_key ) {
688 update_post_meta( $campaign_id, '_' . $meta_key, '' );
689 }
690 }
691
692 if ( ! empty( $campaign_settings_v2['settings']['campaign-summary']['donation_button_text'] ) ) {
693 update_post_meta( $campaign_id, '_campaign_donate_button_text', trim( wp_strip_all_tags( $campaign_settings_v2['settings']['campaign-summary']['donation_button_text'] ) ) );
694 } else {
695 update_post_meta( $campaign_id, '_campaign_donate_button_text', false );
696 }
697
698 if ( $campaign_id && 0 === intval( $campaign_settings_v2['id'] ) ) {
699 $campaign_settings_v2['id'] = intval( $campaign_id );
700 }
701
702 // Adjust campaign creator ID if applicable.
703 if ( isset( $campaign_settings_v2['settings']['campaign-creator']['campaign_creator_id'] ) ) {
704 $creator_id = intval( $campaign_settings_v2['settings']['campaign-creator']['campaign_creator_id'] );
705 if ( 0 !== $creator_id ) { // user id must exist in the posted data.
706 $user = get_userdata( $creator_id ); // user must exist.
707 if ( $user !== false ) {
708 $arg = array(
709 'ID' => $campaign_id,
710 'post_author' => $creator_id,
711 );
712 wp_update_post( $arg );
713 }
714 }
715
716 }
717
718 if ( isset( $campaign_settings_v2['tabs'] ) && is_array( $campaign_settings_v2['tabs'] ) ) :
719 $campaign_settings_v2['tab_order'] = array_keys( $campaign_settings_v2['tabs'] );
720 endif;
721
722 if ( isset( $campaign_settings_v2['layout']['_header']['fields'] ) && is_array( $campaign_settings_v2['layout']['_header']['fields'] ) ) :
723 $campaign_settings_v2['layout']['_header']['order'] = array_keys( $campaign_settings_v2['layout']['_header']['fields'] );
724 endif;
725 if ( isset( $campaign_settings_v2['layout']['_tabs'] ) && is_array( $campaign_settings_v2['layout']['_tabs'] ) ) :
726 foreach ( $campaign_settings_v2['layout']['_tabs'] as $tab_id => $tab_info ) :
727 $campaign_settings_v2['layout']['_tabs'][ $tab_id ]['order'] = array_keys( $campaign_settings_v2['layout']['_tabs'][ $tab_id ]['fields'] );
728 endforeach;
729 endif;
730
731
732 // todo: remove this at some point - this was likely just because of testing.
733 unset( $campaign_settings_v2['settings]'] );
734
735 // Allow addons to update, sync, or override settings before they are saved.
736 $campaign_settings_v2 = apply_filters( 'charitable_campaign_builder_save_campaign_settings', $campaign_settings_v2, $campaign_id );
737
738 // Save the settings as post metadata.
739 if ( $is_preview ) {
740 // If this is a preview request, save the settings as a transient ONLY.
741 set_transient( 'charitable_campaign_preview_' . $campaign_id, $campaign_settings_v2, DAY_IN_SECONDS );
742 } else {
743 // IF this isn't a preview request, update the post into the database and update the transient (the preview at this point should match the real thing).
744 set_transient( 'charitable_campaign_preview_' . $campaign_id, $campaign_settings_v2, DAY_IN_SECONDS );
745 update_post_meta( $campaign_id, 'campaign_settings_v2', $campaign_settings_v2 );
746 }
747
748 /**
749 * Fires after updating campaign data.
750 *
751 * @since 1.8.0
752 *
753 * @param int $campaign_id campaign ID.
754 * @param array $data campaign data.
755 */
756 do_action( 'charitable_builder_save_campaign', $campaign_id, $campaign_settings_v2, $campaign_post, $is_preview );
757
758 if ( ! $campaign_id ) {
759 wp_send_json_error( esc_html__( 'Something went wrong while saving the campaign.', 'charitable' ) );
760 }
761
762 // Response data, start with the campaign settings.
763 $response_data = $campaign_settings_v2;
764 $response_data['redirect'] = admin_url( 'admin.php?page=charitable-overview' );
765 $response_data['campaign_id'] = $campaign_id;
766 $response_data['preview_url'] = charitable_get_campaign_preview_url( $campaign_id, true ); // get_preview_post_link( $campaign_id );
767 $response_data['permalink'] = get_permalink( $campaign_id );
768
769 /**
770 * Allows filtering ajax response data after form was saved.
771 *
772 * @since 1.8.0
773 *
774 * @param array $response_data The data to be sent in the response.
775 * @param int $campaign_id Campaign ID.
776 * @param array $data Campaign data.
777 */
778 $response_data = apply_filters(
779 'charitable_builder_save_campaign_response_data',
780 $response_data,
781 $campaign_id,
782 $data
783 );
784
785 wp_send_json_success( $response_data );
786 }
787 add_action( 'wp_ajax_charitable_save_campaign', 'charitable_save_campaign' );
788
789 /**
790 * Converts the template layout to campaign layout.
791 *
792 * @param array $campaign_settings_v2 The campaign settings.
793 * @param array $campaign_post The campaign post data.
794 *
795 * @return array The updated campaign settings.
796 */
797 function charitable_template_layout_to_campaign_layout( $campaign_settings_v2, $campaign_post ) {
798
799 $builder_template = new Charitable_Campaign_Builder_Templates();
800 $template_id = ! empty( $campaign_post['template_id']['value'] ) ? esc_attr( $campaign_post['template_id']['value'] ) : false;
801 $template_data = ( false !== $template_id ) ? $builder_template->get_template_data( $template_id ) : array();
802
803 $campaign_settings_v2['layout']['rows'] = array();
804
805 // Ensure fields array exists to prevent undefined key errors.
806 if ( ! isset( $campaign_settings_v2['fields'] ) ) {
807 $campaign_settings_v2['fields'] = array();
808 }
809
810 $row_id = 0;
811 $column_id = 0;
812 $section_id = 0;
813
814 // Ensure template_data layout is an array before iterating.
815 if ( ! is_array( $template_data ) || ! isset( $template_data['layout'] ) || ! is_array( $template_data['layout'] ) ) {
816 $template_data['layout'] = array();
817 }
818
819 foreach ( $template_data['layout'] as $row_id => $row ) :
820
821 $campaign_settings_v2['layout']['rows'][ $row_id ]['type'] = ! empty( $row['type'] ) ? $row['type'] : 'row';
822 $campaign_settings_v2['layout']['rows'][ $row_id ]['css_class'] = ! empty( $row['css_class'] ) ? $row['css_class'] : false;
823 $campaign_settings_v2['layout']['rows'][ $row_id ]['background_image'] = ! empty( $row['background_image'] ) ? esc_url_raw( $row['background_image'] ) : '';
824 $campaign_settings_v2['layout']['rows'][ $row_id ]['locked'] = ! empty( $row['locked'] ) ? 1 : 0;
825
826 foreach ( $row['columns'] as $column_key => $column ) :
827
828 foreach ( $column as $section_key => $section ) :
829
830 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'] = array();
831
832 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['type'] = esc_attr( $section['type'] );
833
834 switch ( $section['type'] ) {
835 case 'fields':
836 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['fields'] = array(); // [0] => 1 /// (array) $section['fields'];
837
838 break;
839 case 'tabs':
840 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ]['tabs'] = array(); // [0] => 1 /// (array) $section['fields'];
841
842 if ( ! empty( $section['tabs'] ) ) {
843 foreach ( $section['tabs'] as $tab_id => $tab_info ) :
844 $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ][ $section['type'] ][ $tab_id ] = array(
845 'title' => ! empty( $campaign_post[ 'tabs__' . $tab_id . '__title' ]['value'] ) ? sanitize_text_field( $campaign_post[ 'tabs__' . $tab_id . '__title' ]['value'] ) : $tab_info['title'],
846 'type' => $tab_info['type'],
847 'slug' => sanitize_title( $tab_info['slug'] ),
848 'fields' => array(),
849 );
850 endforeach;
851 }
852
853 break;
854 default:
855 // $campaign_settings_v2['layout']['rows'][ $row_id ]['columns'][ $column_id ]['sections'][ $section_id ][ $section['type'] ] = $section[ $section['type'] ];
856 break;
857 }
858
859 ++$column_id;
860 ++$section_id;
861
862 endforeach;
863
864 // $column_id++;
865
866 endforeach;
867
868 $campaign_settings_v2['layout']['rows'][ $row_id ]['fields'] = array();
869
870 ++$row_id;
871
872 endforeach;
873
874 /*
875 * Copy template field data to campaign settings to preserve complex data
876 * structures. This is especially important for fields with nested data
877 * like list fields.
878 *
879 * Ported from Charitable Pro (1.8.13): without this second pass,
880 * $campaign_settings_v2['fields'] is left empty, so a freshly created
881 * visual campaign has row/column/section skeletons that reference field
882 * ids with no field DATA (headline, content, etc.) behind them at all —
883 * content.php has nothing to render for any of them.
884 */
885 if ( ! empty( $template_data['layout'] ) && is_array( $template_data['layout'] ) ) {
886 if ( charitable_is_debug() ) {
887 error_log( 'Charitable: Copying template field data to campaign settings' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
888 }
889
890 $field_id = 0;
891
892 foreach ( $template_data['layout'] as $row ) {
893 if ( ! empty( $row['columns'] ) && is_array( $row['columns'] ) ) {
894 foreach ( $row['columns'] as $column ) {
895 if ( ! empty( $column ) && is_array( $column ) ) {
896 foreach ( $column as $section ) {
897 if ( ! empty( $section['type'] ) ) {
898 if ( 'fields' === $section['type'] && ! empty( $section['fields'] ) ) {
899 // Process regular fields.
900 foreach ( $section['fields'] as $field_data ) {
901 if ( is_array( $field_data ) && ! empty( $field_data['type'] ) ) {
902 // Copy the field data to preserve complex structures.
903 $campaign_settings_v2['fields'][ $field_id ] = $field_data;
904
905 if ( 'list' === $field_data['type'] && ! empty( $field_data['list_items'] ) && charitable_is_debug() ) {
906 error_log( 'Charitable: Copied list field ' . $field_id . ' from template with list_items: ' . print_r( $field_data['list_items'], true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
907 }
908 }
909
910 ++$field_id;
911 }
912 } elseif ( 'tabs' === $section['type'] && ! empty( $section['tabs'] ) ) {
913 // Process tab fields.
914 foreach ( $section['tabs'] as $tab ) {
915 if ( ! empty( $tab['fields'] ) && is_array( $tab['fields'] ) ) {
916 foreach ( $tab['fields'] as $field_data ) {
917 if ( is_array( $field_data ) && ! empty( $field_data['type'] ) ) {
918 // Copy the field data to preserve complex structures.
919 $campaign_settings_v2['fields'][ $field_id ] = $field_data;
920
921 if ( 'list' === $field_data['type'] && ! empty( $field_data['list_items'] ) && charitable_is_debug() ) {
922 error_log( 'Charitable: Copied list field ' . $field_id . ' from template with list_items: ' . print_r( $field_data['list_items'], true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
923 }
924 }
925
926 ++$field_id;
927 }
928 }
929 }
930 }
931 }
932 }
933 }
934 }
935 }
936 }
937
938 if ( charitable_is_debug() ) {
939 $fields_count = isset( $campaign_settings_v2['fields'] ) && is_array( $campaign_settings_v2['fields'] ) ? count( $campaign_settings_v2['fields'] ) : 0;
940 error_log( 'Charitable: Template field data copying complete. Total fields: ' . $fields_count ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
941 }
942 }
943
944 return $campaign_settings_v2;
945 }
946
947 /**
948 * Searches for a key in a multidimensional array.
949 *
950 * @param array $array The array to search.
951 * @param mixed $key The key to search for.
952 * @return mixed|null The value of the key if found, null otherwise.
953 */
954 function charitable_search_settings( $array, $key ) {
955 // If the key exists in the current level of the array, return its value.
956 if ( array_key_exists( $key, $array ) ) {
957 return $array[ $key ];
958 }
959
960 // Iterate over each element in the array.
961 foreach ( $array as $value ) {
962 // If the element is an array, recursively search it for the key.
963 if ( is_array( $value ) ) {
964 $result = charitable_search_settings( $value, $key );
965
966 // If the key is found in the nested array, return its value.
967 if ( $result !== null ) {
968 return $result;
969 }
970 }
971 }
972
973 // The key was not found in the array.
974 return null;
975 }
976
977
978 /**
979 * Find the highest field ID field so we can keep the max number accurate.
980 *
981 * @since 1.8.0
982 *
983 * @param array $array Settings data.
984 */
985 function charitable_find_highest_field_key( $array ) {
986 $highest_key = null;
987
988 // Iterate through each element in the array.
989 foreach ( $array as $key => $value ) {
990 // Check if the current element is an array.
991 if ( is_array( $value ) ) {
992 // Recursively call the function to search through nested arrays.
993 $field_key = charitable_find_highest_field_key( $value );
994
995 // Check if the key is numeric and greater than the current highest key.
996 if ( is_numeric( $field_key ) && ( $highest_key === null || $field_key > $highest_key ) ) {
997 $highest_key = $field_key;
998 }
999 } else {
1000 // Check if the key is numeric and greater than the current highest key.
1001 if ( is_numeric( $key ) && ( $highest_key === null || $key > $highest_key ) ) {
1002 $highest_key = $key;
1003 }
1004 }
1005 }
1006
1007 return $highest_key;
1008 }
1009
1010 /**
1011 * A very particular util array for the builder.
1012 *
1013 * @since 1.8.0
1014 *
1015 * @param string $string The string from the settings data.
1016 * @param string $value Value.
1017 * @param string $end_key End key.
1018 */
1019 function charitable_convert_campaign_string_to_array( $string, $value, $end_key = 'amount' ) {
1020
1021 $keys = explode( '][', trim( $string, '[]' ) ); // split the string into an array of keys.
1022 $array = [];
1023 $current = &$array;
1024 foreach ( $keys as $key ) {
1025 $current[ $key ] = [];
1026 if ( $key === $end_key ) {
1027 $current[ $key ] = $value; // $value;
1028 }
1029 $current = &$current[ $key ];
1030 }
1031
1032 return $array;
1033 }
1034
1035 /**
1036 * Convert a campaign string to an array.
1037 *
1038 * @param string $input The input string to convert.
1039 * @param mixed $value The default value to use for each key.
1040 * @param string $end_key The key to use for the final value.
1041 * @return array The resulting array.
1042 */
1043 function charitable_convert_campaign_string_to_array_new( $input, $value, $end_key = 'amount' ) { // phpcs:ignore
1044
1045 $output = array();
1046 $pointer = &$output;
1047
1048 while ( ( $index = strpos( $input, '[' ) ) !== false ) {
1049 if ( $index != 0 ) {
1050 $key = substr( $input, 0, $index );
1051 $pointer[ $key ] = array();
1052 $pointer = &$pointer[ $key ];
1053 $input = substr( $input, $index );
1054 continue;
1055 }
1056 $end_index = strpos( $input, ']' );
1057 $array_key = substr( $input, $index + 1, $end_index - 1 );
1058 $pointer[ $array_key ] = array();
1059 $pointer = &$pointer[ $array_key ];
1060 $input = substr( $input, $end_index + 1 );
1061 }
1062
1063 return( $output );
1064 }
1065
1066 /*
1067 * Three orphaned AJAX handlers were REMOVED here in 1.8.13.1:
1068 * charitable_new_campaign(), charitable_update_campaign_template() and
1069 * charitable_builder_increase_next_field_id(), with their wp_ajax_ registrations.
1070 *
1071 * All three called charitable()->get( 'builder_templates' ) or charitable()->get( 'form' ).
1072 * The Charitable class has no get() method -- only get_registered_object() -- and no
1073 * __call(), and 'builder_templates' was never registered anywhere, so every one of the six
1074 * calls was a fatal `Call to undefined method Charitable::get()` if the handler was ever
1075 * reached. Nothing referenced them: no JS in the plugin fires any of the three actions, and
1076 * the builder creates and saves through charitable_save_campaign, which validates templates
1077 * via `new Charitable_Campaign_Builder_Templates()` -- the pattern the other call sites in
1078 * this file already use. So they were dead paths rather than a live break, but reachable by
1079 * a hand-crafted authenticated POST carrying a valid charitable-builder nonce, which turned
1080 * them into a trivially-triggered 500 for that request.
1081 *
1082 * Pro removed the same three in 8b71ac4e7c and 6a3e53298c.
1083 */
1084 /**
1085 * Campaign Builder Dynamic Choices option toggle.
1086 *
1087 * This can be triggered with select/radio/checkbox fields.
1088 *
1089 * @since 1.8.0
1090 */
1091 function charitable_builder_dynamic_choices() {
1092
1093 // Run a security check.
1094 check_ajax_referer( 'charitable-builder', 'nonce' );
1095
1096 // Check for permissions.
1097 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
1098 wp_send_json_error();
1099 }
1100
1101 // Check for valid/required items.
1102 if ( ! isset( $_POST['field_id'] ) || empty( $_POST['type'] ) || ! in_array( $_POST['type'], [ 'post_type', 'taxonomy' ], true ) ) {
1103 wp_send_json_error();
1104 }
1105
1106 $type = sanitize_key( $_POST['type'] );
1107 $id = absint( $_POST['field_id'] );
1108
1109 // Fetch the option row HTML to be returned to the builder.
1110 $field = new Charitable_Field_Select( false );
1111 $field_args = [
1112 'id' => $id,
1113 'dynamic_choices' => $type,
1114 ];
1115 $option_row = $field->field_option( 'dynamic_choices_source', $field_args, [], false );
1116
1117 wp_send_json_success(
1118 [
1119 'markup' => $option_row,
1120 ]
1121 );
1122 }
1123
1124 add_action( 'wp_ajax_charitable_builder_dynamic_choices', 'charitable_builder_dynamic_choices' );
1125
1126 /**
1127 * Content preview via ajax.
1128 *
1129 * @since 1.8.0
1130 */
1131 function charitable_builder_tab_content_preview() {
1132 // phpcs:disable WordPress.Security.NonceVerification.Missing
1133 $type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : '';
1134 // phpcs:enable WordPress.Security.NonceVerification.Missing
1135 $output = charitable_builder_tab_content_preview_by_type( $type );
1136
1137 wp_send_json_success(
1138 [
1139 'output' => $output,
1140 ]
1141 );
1142 }
1143
1144 add_action( 'wp_ajax_charitable_tab_content_preview', 'charitable_builder_tab_content_preview' );
1145
1146 /**
1147 * Content preview via ajax.
1148 *
1149 * @since 1.8.0
1150 */
1151 function charitable_builder_field_content_preview() {
1152
1153 /*
1154 * This handler had NO nonce and NO capability check, while being registered as wp_ajax_ --
1155 * so any logged-in user, including a Subscriber, could reach it (1.8.13.1).
1156 *
1157 * It renders an arbitrary builder field from raw $_POST and echoes the result, and several
1158 * field previews reach do_shortcode(), so it was an authenticated shortcode-execution and
1159 * arbitrary-post-meta-read primitive.
1160 *
1161 * The nonce was ALREADY being sent by every JS caller (charitable_builder.nonce, from
1162 * wp_create_nonce( 'charitable-builder' )) and simply never checked, so validating it
1163 * breaks nothing -- the same was verified live in Pro.
1164 */
1165 check_ajax_referer( 'charitable-builder', 'nonce' );
1166
1167 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
1168 wp_send_json_error();
1169 }
1170
1171 // phpcs:disable WordPress.Security.NonceVerification.Missing
1172 if ( ! empty( $_POST['field_type'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below
1173
1174 $type = 'Charitable_Field_' . str_replace( ' ', '_', ( ucwords( str_replace( '-', ' ', sanitize_key( wp_unslash( $_POST['field_type'] ) ) ) ) ) );
1175
1176 /*
1177 * class_exists() alone let any class whose name happens to match the Charitable_Field_*
1178 * pattern be instantiated and have field_preview() called on it. Requiring a
1179 * Charitable_Builder_Field subclass keeps this to classes that actually implement the
1180 * preview contract.
1181 */
1182 if ( class_exists( $type ) && is_subclass_of( $type, 'Charitable_Builder_Field' ) ) :
1183
1184 $class = new $type();
1185 $field_id = isset( $_POST['field_id'] ) ? intval( wp_unslash( $_POST['field_id'] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1186
1187 /*
1188 * campaign_settings_v2 was read from whatever post id arrived, with no check that
1189 * the caller may read that post -- disclosing the builder configuration of any
1190 * private or draft campaign, or of any other post type carrying that meta key.
1191 *
1192 * 0 is legitimate: a campaign being built for the first time has not been saved
1193 * yet, and previews still need to render.
1194 */
1195 $campaign_id = isset( $_POST['campaign_id'] ) ? absint( wp_unslash( $_POST['campaign_id'] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1196
1197 if ( $campaign_id && ! current_user_can( 'edit_post', $campaign_id ) ) {
1198 wp_send_json_error();
1199 }
1200
1201 $campaign_data = $campaign_id ? get_post_meta( $campaign_id, 'campaign_settings_v2', true ) : array();
1202 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array data, sanitized in field_preview method
1203 $field_data = $_POST;
1204
1205 ob_start();
1206
1207 echo $class->field_preview( $field_data, $campaign_data, $field_id, false, true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1208
1209 $output = ob_get_clean();
1210
1211 wp_send_json_success(
1212 [
1213 'output' => $output,
1214 ]
1215 );
1216
1217 endif;
1218 // phpcs:enable WordPress.Security.NonceVerification.Missing
1219 }
1220
1221 wp_send_json_error();
1222 }
1223
1224 add_action( 'wp_ajax_charitable_builder_field_content_preview', 'charitable_builder_field_content_preview' );
1225
1226 /**
1227 * Perform test connection to verify that the current web host can successfully
1228 * make outbound SSL connections.
1229 *
1230 * @since 1.8.0
1231 */
1232 function charitable_verify_ssl_ajax() {
1233
1234 // Run a security check.
1235 check_ajax_referer( 'charitable-admin', 'nonce' );
1236
1237 // Check for permissions.
1238 if ( ! charitable_current_user_can() ) {
1239 wp_send_json_error(
1240 [
1241 'msg' => esc_html__( 'You do not have permission to perform this operation.', 'charitable' ),
1242 ]
1243 );
1244 }
1245
1246 $response = wp_remote_post( 'https://wpcharitable.com/connection-test.php' );
1247
1248 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
1249 wp_send_json_success(
1250 [
1251 'msg' => esc_html__( 'Success! Your server can make SSL connections.', 'charitable' ),
1252 ]
1253 );
1254 }
1255
1256 wp_send_json_error(
1257 [
1258 'msg' => esc_html__( 'There was an error and the connection failed. Please contact your web host with the technical details below.', 'charitable' ),
1259 'debug' => '<pre>' . print_r( map_deep( $response, 'wp_strip_all_tags' ), true ) . '</pre>', // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1260 ]
1261 );
1262 }
1263 // add_action( 'wp_ajax_charitable_verify_ssl', 'charitable_verify_ssl_ajax' ); // Disabled - conflicts with handler in charitable-core-admin-functions.php
1264
1265
1266 /**
1267 * Update form template.
1268 *
1269 * @since 1.8.0
1270 */
1271 function charitable_builder_ajax_update_campaign_creator() {
1272
1273 // Run a security check.
1274 check_ajax_referer( 'charitable-builder', 'nonce' );
1275
1276 /*
1277 * Answers the `// todo: check creds?` that used to sit further down this function
1278 * (1.8.13.1). The nonce alone proved the request came from our own admin UI, not that the
1279 * caller may act on campaigns or read other users' details -- this returned display name,
1280 * avatar, author archive and a user-edit link for ANY user id, which is user enumeration.
1281 *
1282 * Changing a campaign's creator is an edit_campaigns operation, and where a saved campaign
1283 * is named the caller must be able to edit that specific campaign.
1284 */
1285 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
1286 wp_send_json_error(
1287 [
1288 'error_type' => 'cant_update',
1289 'message' => esc_html__( 'Permission denied.', 'charitable' ),
1290 ]
1291 );
1292 }
1293
1294 $campaign_id = ! empty( $_POST['campaign_id'] ) ? absint( $_POST['campaign_id'] ) : false;
1295 $new_creator_id = ! empty( $_POST['creator_id'] ) ? absint( $_POST['creator_id'] ) : false;
1296
1297 if ( $campaign_id && ! current_user_can( 'edit_post', $campaign_id ) ) {
1298 wp_send_json_error(
1299 [
1300 'error_type' => 'cant_update',
1301 'message' => esc_html__( 'Permission denied.', 'charitable' ),
1302 ]
1303 );
1304 }
1305
1306 if ( false === $new_creator_id ) {
1307 wp_send_json_error(
1308 [
1309 'error_type' => 'invalid_campaign_id',
1310 'message' => esc_html__( 'No creator ID provided.', 'charitable' ),
1311 ]
1312 );
1313 }
1314
1315 // check to see if the user exists.
1316 $user = get_userdata( $new_creator_id );
1317 if ( $user === false ) {
1318 wp_send_json_error(
1319 [
1320 'error_type' => 'invalid_campaign_id',
1321 'message' => esc_html__( 'Invalid creator ID provided.', 'charitable' ),
1322 ]
1323 );
1324 }
1325
1326 if ( $user ) {
1327
1328 wp_send_json_success(
1329 [
1330 'id' => $campaign_id,
1331 'new_creator_id' => $new_creator_id,
1332 'avatar_url' => esc_url( get_avatar_url( $new_creator_id ) ),
1333 'creator_name' => $user->display_name . ' (USER ID ' . $new_creator_id . ')',
1334 'public_profile_link' => get_author_posts_url( $new_creator_id ),
1335 'edit_profile_link' => admin_url( 'user-edit.php?user_id=' . $new_creator_id ),
1336 ]
1337 );
1338 }
1339
1340 wp_send_json_error(
1341 [
1342 'error_type' => 'cant_update',
1343 'message' => esc_html__( 'Error updating campaign creator.', 'charitable' ),
1344 ]
1345 );
1346 }
1347 add_action( 'wp_ajax_charitable_update_campaign_creator', 'charitable_builder_ajax_update_campaign_creator' );
1348
1349 /**
1350 * Update form template.
1351 *
1352 * @since 1.8.0
1353 */
1354 function charitable_builder_ajax_update_campaign_status_link() {
1355
1356 // Run a security check.
1357 check_ajax_referer( 'charitable-builder', 'nonce' );
1358
1359 // Check for campaign ID.
1360 if ( empty( $_POST['campaign_id'] ) || 0 === intval( $_POST['campaign_id'] ) ) {
1361 wp_send_json_error(
1362 [
1363 'error_type' => 'invalid_campaign_id',
1364 'message' => esc_html__( 'No campaign ID provided.', 'charitable' ),
1365 ]
1366 );
1367 }
1368
1369 $campaign_id = absint( $_POST['campaign_id'] );
1370
1371 /*
1372 * The nonce proved the request came from our admin UI; it did not prove the caller may read
1373 * this campaign (1.8.13.1). Without this, status and permalink were returned for any
1374 * campaign id, including drafts and privately-published ones.
1375 */
1376 if ( ! current_user_can( 'edit_post', $campaign_id ) ) {
1377 wp_send_json_error(
1378 [
1379 'error_type' => 'cant_update',
1380 'message' => esc_html__( 'Permission denied.', 'charitable' ),
1381 ]
1382 );
1383 }
1384
1385 // get status.
1386 $campaign = $campaign_id ? charitable_get_campaign( $campaign_id ) : false;
1387 $status = $campaign ? $campaign->get_status() : false;
1388 if ( 'finished' === $status && $campaign->has_goal() ) {
1389 $status = $campaign->has_achieved_goal() ? 'successful' : 'unsuccessful';
1390 }
1391
1392 /*
1393 * campaign status. esc_url() on the permalink: this is interpolated straight into an href
1394 * and the value passes through the page_link/post_link filters, so it is not guaranteed
1395 * safe markup (1.8.13.1).
1396 */
1397 $html = '<span class="charitable-view-campaign-external-link"><a href="' . esc_url( get_permalink( $campaign_id ) ) . '" target="_blank" title="' . esc_html__( 'View Campaign Page', 'charitable' ) . '"><!--<i class="fa fa-eye"></i>--></a></span>';
1398
1399 wp_send_json_success(
1400 [
1401 'id' => $campaign_id,
1402 'html' => $html,
1403 ]
1404 );
1405 }
1406 add_action( 'wp_ajax_charitable_update_campaign_status_link', 'charitable_builder_ajax_update_campaign_status_link' );
1407
1408 /**
1409 * Receive and process feedback form content.
1410 *
1411 * @since 1.8.0
1412 */
1413 function charitable_campaign_builder_send_feedback_ajax() {
1414
1415 /*
1416 * Had no nonce and no capability check, so any logged-in user could drive unlimited
1417 * outbound requests to wpcharitable.com from the site (1.8.13.1). The JS caller already
1418 * sends charitable_builder.nonce, so checking it costs nothing.
1419 */
1420 check_ajax_referer( 'charitable-builder', 'nonce' );
1421
1422 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
1423 wp_send_json_error();
1424 }
1425
1426 // phpcs:disable WordPress.Security.NonceVerification.Missing
1427 /* Data to send in our API request */
1428 $feedback_params = array(
1429 'feedback_action' => 'template_feedback',
1430 'type' => ! empty( $_POST['data']['type'] ) ? sanitize_text_field( wp_unslash( $_POST['data']['type'] ) ) : false, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1431 'name' => ! empty( $_POST['data']['name'] ) ? sanitize_text_field( wp_unslash( $_POST['data']['name'] ) ) : false, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1432 'email' => ! empty( $_POST['data']['email'] ) ? sanitize_email( wp_unslash( $_POST['data']['email'] ) ) : false, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1433 'feedback' => ! empty( $_POST['data']['feedback'] ) ? sanitize_text_field( wp_unslash( $_POST['data']['feedback'] ) ) : false, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1434 );
1435 // phpcs:enable WordPress.Security.NonceVerification.Missing
1436
1437 /* Call the custom API */
1438 $response = wp_remote_post(
1439 'https://wpcharitable.com',
1440 array(
1441 /*
1442 * sslverify was false, which silently accepted any certificate for this request
1443 * (1.8.13.1). There is no reason to skip verification for our own HTTPS endpoint,
1444 * and doing so makes the request interceptable.
1445 */
1446 'timeout' => 15,
1447 'body' => $feedback_params,
1448 )
1449 );
1450
1451 if ( is_wp_error( $response ) ) {
1452 wp_send_json_error(
1453 );
1454 } else {
1455 wp_send_json_success(
1456 [
1457 'sent' => true,
1458 ]
1459 );
1460 }
1461 }
1462 add_action( 'wp_ajax_charitable_campaign_builder_send_feedback_ajax', 'charitable_campaign_builder_send_feedback_ajax' );
1463
1464