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/form-editor/assets.php +490 -80 0.0.1 → 1.6.1 View file →
@@ -9,8 +9,10 @@
9 9 */
10 10
11 11 namespace SureDonation\Inc\FormEditor;
12 12
13 +use SureDonation\Inc\Helper;
14 +use SureDonation\Inc\Payments\Payment_Helper;
13 15 use SureDonation\Inc\Traits\Get_Instance;
14 16 use SureDonation\Inc\Post_Types\Donation_Form;
15 17
16 18 // Exit if accessed directly.
@@ -32,9 +34,13 @@
32 34 * @since 0.0.1
33 35 */
34 36 public function __construct() {
35 37 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
38 + // The stylesheet is registered separately, on the hook WordPress replays
39 + // inside the editor canvas iframe. See enqueue_editor_styles().
40 + add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_styles' ] );
36 41 add_action( 'init', [ $this, 'register_form_settings_meta' ] );
42 + add_action( 'init', [ $this, 'register_email_notifications_meta' ] );
37 43 }
38 44
39 45 /**
40 46 * Enqueue editor assets for the donation form editor.
@@ -42,31 +48,22 @@
42 48 * @return void
43 49 * @since 0.0.1
44 50 */
45 51 public function enqueue_editor_assets() {
46 - $screen = get_current_screen();
47 -
48 52 // Only load on donation form editor.
49 - if ( ! $screen || Donation_Form::POST_TYPE !== $screen->post_type ) {
53 + if ( ! $this->is_form_editor_screen() ) {
50 54 return;
51 55 }
52 56
53 - // Get the asset file for dependencies.
54 - $editor_asset_file = SUREDONATION_DIR . 'assets/build/editor/editor.asset.php';
55 - $editor_asset = file_exists( $editor_asset_file )
56 - ? include $editor_asset_file
57 - : [
58 - 'dependencies' => [
59 - 'wp-plugins',
60 - 'wp-editor',
61 - 'wp-components',
62 - 'wp-data',
63 - 'wp-element',
64 - 'wp-i18n',
65 - ],
66 - 'version' => SUREDONATION_VER,
67 - ];
57 + // Core's bundled CodeMirror (CSS mode) backs the Custom CSS tab in the form
58 + // settings dialog. Returns false when the user turned syntax highlighting
59 + // off in their profile; the tab falls back to a plain textarea then.
60 + wp_enqueue_code_editor( [ 'type' => 'text/css' ] );
61 + wp_enqueue_script( 'wp-theme-plugin-editor' );
62 + wp_enqueue_style( 'wp-codemirror' );
68 63
64 + $editor_asset = $this->get_editor_asset();
65 +
69 66 // Editor plugin JS.
70 67 wp_enqueue_script(
71 68 'suredonation-form-editor',
72 69 SUREDONATION_URL . 'assets/build/editor/editor.js',
@@ -74,25 +71,55 @@
74 71 $editor_asset['version'],
75 72 true
76 73 );
77 74
78 - // Editor styles.
79 - wp_enqueue_style(
80 - 'suredonation-form-editor',
81 - SUREDONATION_URL . 'assets/build/editor/editor.css',
82 - [ 'wp-components' ],
83 - $editor_asset['version']
84 - );
75 + // Editor styles are enqueued from enqueue_editor_styles(), not here.
85 76
77 + // The OttoKit embed script (defines window.SureTriggers) is NOT enqueued
78 + // here — it is remote executable JS and would load on every form-editor
79 + // session even when OttoKit is absent. It is lazy-injected from the
80 + // OttoKit tab only when the builder opens (see OttoKitSettings.js), using
81 + // the URL passed below.
82 +
86 83 // Localized data.
84 + $global_currency = Payment_Helper::get_global_setting( 'currency', 'USD' );
85 + $global_currency = is_string( $global_currency ) ? $global_currency : 'USD';
86 +
87 + $editor_data = [
88 + 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
89 + 'nonce' => wp_create_nonce( 'suredonation_editor_nonce' ),
90 + 'postType' => Donation_Form::POST_TYPE,
91 + 'smartTags' => Helper::get_smart_tags()['confirmation'],
92 + 'emailSmartTags' => Helper::get_smart_tags()['email_grouped'],
93 + 'defaultEmailNotifications' => $this->get_default_email_notifications(),
94 + // The recurring payment type is a Pro control, but the block attribute
95 + // it sets is registered in free and persists in post content. Without
96 + // this, deactivating Pro leaves the recurring notifications on screen
97 + // and editable while nothing can send them.
98 + 'isProActive' => defined( 'SUREDONATION_PRO_VER' ),
99 + 'currency' => $global_currency,
100 + 'currencySymbol' => Payment_Helper::get_currency_symbol( $global_currency ),
101 + // OttoKit integration: embed config + lazy-loaded builder script.
102 + 'suretriggersNonce' => wp_create_nonce( 'suredonation_suretriggers_nonce' ),
103 + 'embedScriptUrl' => SUREDONATION_SURETRIGGERS_INTEGRATION_BASE_URL . 'js/v2/embed.js',
104 + 'integrations' => [
105 + 'sure_triggers' => Helper::get_ottokit_integration(),
106 + ],
107 + ];
108 +
109 + // Plugin install/activate nonces are a plugin-management capability;
110 + // only expose them to users who can actually install plugins. The AJAX
111 + // handlers enforce this server-side too — this keeps the nonces out of
112 + // the page source for sub-admins who reach the editor via post.php.
113 + if ( current_user_can( 'install_plugins' ) ) {
114 + $editor_data['pluginInstallerNonce'] = wp_create_nonce( 'updates' );
115 + $editor_data['pluginManagerNonce'] = wp_create_nonce( 'suredonation_plugin_manager' );
116 + }
117 +
87 118 wp_localize_script(
88 119 'suredonation-form-editor',
89 120 'suredonationFormEditor',
90 - [
91 - 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
92 - 'nonce' => wp_create_nonce( 'suredonation_editor_nonce' ),
93 - 'postType' => Donation_Form::POST_TYPE,
94 - ]
121 + $editor_data
95 122 );
96 123
97 124 // Set script translations.
98 125 wp_set_script_translations( 'suredonation-form-editor', 'suredonation' );
@@ -98,87 +125,470 @@
98 125 wp_set_script_translations( 'suredonation-form-editor', 'suredonation' );
99 126 }
100 127
101 128 /**
129 + * Enqueue the form editor stylesheet.
130 + *
131 + * Split out from enqueue_editor_assets() because the two need different
132 + * hooks. `enqueue_block_editor_assets` only reaches the admin document, but
133 + * most of this stylesheet targets `.editor-styles-wrapper` — the block
134 + * canvas, which is an iframe. WordPress used to paper over that with a
135 + * compatibility pass that clones any outer stylesheet mentioning
136 + * `.editor-styles-wrapper` into the canvas, logging "<handle> was added to
137 + * the iframe incorrectly" for each one (see the block editor's `Iframe`
138 + * component). `enqueue_block_assets` is the supported hook instead:
139 + * core replays it inside _wp_get_iframed_editor_assets() to build the
140 + * canvas document, so the styles land there directly and the compatibility
141 + * pass skips the handle instead of cloning it.
142 + *
143 + * The hook fires for the admin document as well, so a single enqueue here
144 + * covers the editor chrome too and the handle stays `suredonation-form-editor`
145 + * — the id core matches on when deciding whether a clone is still needed.
146 + *
147 + * One consequence of the move: `enqueue_block_assets` reaches the admin
148 + * document only through wp_common_block_scripts_and_styles(), which bails when
149 + * `should_load_block_editor_scripts_and_styles` is filtered false in wp-admin.
150 + * Anything doing that also strips `wp-block-library` and visibly breaks core's
151 + * own editor, so it is not a case worth defending against here.
152 + *
153 + * @return void
154 + * @since 1.5.1
155 + */
156 + public function enqueue_editor_styles() {
157 + // `enqueue_block_assets` also fires on the front end, where there is no
158 + // editor to style.
159 + if ( ! is_admin() || ! $this->is_form_editor_screen() ) {
160 + return;
161 + }
162 +
163 + /*
164 + * No `wp-components` dependency. It is already in both documents ahead of
165 + * this sheet without being asked for: the admin page loads it as editor
166 + * chrome, and _wp_get_iframed_editor_assets() enqueues `wp-edit-blocks`
167 + * — whose dependencies include `wp-components` — before it fires
168 + * `enqueue_block_assets`. Declaring it would neither change the cascade nor
169 + * keep anything out of the canvas.
170 + */
171 + wp_enqueue_style(
172 + 'suredonation-form-editor',
173 + SUREDONATION_URL . 'assets/build/editor/editor.css',
174 + [],
175 + $this->get_editor_asset()['version']
176 + );
177 + }
178 +
179 + /**
180 + * Whether the current admin screen is the donation form editor.
181 + *
182 + * @return bool
183 + * @since 1.5.1
184 + */
185 + private function is_form_editor_screen() {
186 + // get_current_screen() lives in an admin include, so it is missing on the
187 + // front end — where enqueue_block_assets also fires. Checked here rather
188 + // than at each call site so the helper is safe for any caller.
189 + if ( ! function_exists( 'get_current_screen' ) ) {
190 + return false;
191 + }
192 +
193 + $screen = get_current_screen();
194 +
195 + return $screen instanceof \WP_Screen && Donation_Form::POST_TYPE === $screen->post_type;
196 + }
197 +
198 + /**
199 + * Build metadata (dependencies and version) for the editor bundle.
200 + *
201 + * @return array{dependencies: array<int, string>, version: string}
202 + * @since 1.5.1
203 + */
204 + private function get_editor_asset() {
205 + $editor_asset_file = SUREDONATION_DIR . 'assets/build/editor/editor.asset.php';
206 +
207 + return file_exists( $editor_asset_file )
208 + ? include $editor_asset_file
209 + : [
210 + 'dependencies' => [
211 + 'wp-plugins',
212 + 'wp-editor',
213 + 'wp-components',
214 + 'wp-data',
215 + 'wp-element',
216 + 'wp-i18n',
217 + ],
218 + 'version' => SUREDONATION_VER,
219 + ];
220 + }
221 +
222 + /**
102 223 * Register form settings meta fields.
103 224 *
104 - * These meta fields store form-level settings like submit button text,
105 - * success message, and confirmation type.
225 + * Stores form confirmation settings as a single JSON string.
106 226 *
107 227 * @return void
108 - * @since 0.0.1
228 + * @since 1.0.0
109 229 */
110 230 public function register_form_settings_meta() {
111 231 $post_type = Donation_Form::POST_TYPE;
112 232
113 - // Submit button text.
114 - register_post_meta(
115 - $post_type,
116 - '_suredonation_form_submitButtonText',
233 + // Default confirmation message HTML (receipt layout with smart tags).
234 + $default_message = Helper::get_default_confirmation_message();
235 +
236 + $default_confirmation = wp_json_encode(
117 237 [
118 - 'type' => 'string',
119 - 'description' => __( 'Submit button text.', 'suredonation' ),
120 - 'single' => true,
121 - 'default' => 'Donate',
122 - 'show_in_rest' => true,
123 - 'sanitize_callback' => 'sanitize_text_field',
124 - 'auth_callback' => function () {
125 - return current_user_can( 'edit_posts' );
126 - },
238 + 'confirmation_type' => 'same page',
239 + 'message' => $default_message,
240 + 'submission_action' => 'hide form',
241 + 'custom_url' => '',
242 + 'page_url' => '',
127 243 ]
128 244 );
129 245
130 - // Success message.
246 + // Form confirmation settings (JSON string).
131 247 register_post_meta(
132 248 $post_type,
133 - '_suredonation_form_successMessage',
249 + '_suredonation_form_confirmation',
134 250 [
135 251 'type' => 'string',
136 - 'description' => __( 'Success message shown after donation.', 'suredonation' ),
252 + 'description' => __( 'Form confirmation settings.', 'suredonation' ),
137 253 'single' => true,
138 - 'default' => 'Thank you for your donation!',
139 - 'show_in_rest' => true,
140 - 'sanitize_callback' => 'sanitize_textarea_field',
141 - 'auth_callback' => function () {
142 - return current_user_can( 'edit_posts' );
254 + 'default' => $default_confirmation,
255 + 'show_in_rest' => [
256 + 'schema' => [
257 + 'type' => 'string',
258 + 'context' => [ 'edit' ],
259 + ],
260 + ],
261 + 'sanitize_callback' => [ $this, 'sanitize_confirmation_settings' ],
262 + 'auth_callback' => static function () {
263 + return current_user_can( 'manage_options' );
143 264 },
144 265 ]
145 266 );
267 + }
146 268
147 - // Confirmation type.
148 - register_post_meta(
149 - $post_type,
150 - '_suredonation_form_confirmationType',
269 + /**
270 + * Email notification meta key.
271 + *
272 + * No migration from global `suredonation_options['email_notifications']` is needed:
273 + * the plugin is pre-release (v0.0.1) with no production installs carrying customized
274 + * global settings. New forms are seeded with defaults via the editor JS useEffect.
275 + *
276 + * @since 1.0.0
277 + */
278 + public const EMAIL_NOTIFICATIONS_META_KEY = '_suredonation_form_email_notifications';
279 +
280 + /**
281 + * Get default email notifications for new forms.
282 + *
283 + * Provides the initial set of notifications (donation receipt + admin) that
284 + * are seeded when a form has no email notifications configured.
285 + *
286 + * @return array<int, array<string, mixed>> Default notifications array.
287 + * @since 1.0.0
288 + */
289 + public function get_default_email_notifications() {
290 + $sig = '<p>' . esc_html__( 'We truly appreciate your support', 'suredonation' ) . '</p>'
291 + . '<p>— {site_title}</p>';
292 +
293 + // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found -- Readability.
294 + $defaults = [
295 + // --- Donor Emails ---
151 296 [
152 - 'type' => 'string',
153 - 'description' => __( 'What happens after successful donation.', 'suredonation' ),
154 - 'single' => true,
155 - 'default' => 'message',
156 - 'show_in_rest' => true,
157 - 'sanitize_callback' => function ( $value ) {
158 - $allowed = [ 'message', 'redirect' ];
159 - return in_array( $value, $allowed, true ) ? $value : 'message';
160 - },
161 - 'auth_callback' => function () {
162 - return current_user_can( 'edit_posts' );
163 - },
164 - ]
165 - );
297 + 'key' => 'donation_receipt',
298 + 'id' => 1,
299 + 'status' => true,
300 + 'name' => __( 'Donation Receipt', 'suredonation' ),
301 + 'email_to' => '{donor_email}',
302 + 'subject' => __( 'Thank you for your donation to {campaign_name}', 'suredonation' ),
303 + 'email_body' => '<p>' . esc_html__( 'Hi {donor_name},', 'suredonation' ) . '</p>'
304 + . '<p>' . esc_html__( 'Thank you for supporting {campaign_name}. Your contribution means a lot.', 'suredonation' ) . '</p>'
305 + . '<p><strong>' . esc_html__( 'Donation Details:', 'suredonation' ) . '</strong></p>'
306 + . '<ul>'
307 + . '<li>' . esc_html__( 'Amount:', 'suredonation' ) . ' {amount}</li>'
308 + . '<li>' . esc_html__( 'Date:', 'suredonation' ) . ' {donation_date}</li>'
309 + . '<li>' . esc_html__( 'Transaction ID:', 'suredonation' ) . ' {transaction_id}</li>'
310 + . '</ul>'
311 + . $sig,
312 + 'from_name' => '{site_title}',
313 + 'from_email' => '{admin_email}',
314 + 'reply_to' => '',
315 + 'trigger' => 'donation_completed',
316 + ],
317 + [
318 + 'key' => 'donation_processing',
319 + 'id' => 2,
320 + 'status' => true,
321 + 'name' => __( 'Donation Processing', 'suredonation' ),
322 + 'email_to' => '{donor_email}',
323 + 'subject' => __( 'Your donation is being processed', 'suredonation' ),
324 + 'email_body' => '<p>' . esc_html__( 'Hi {donor_name},', 'suredonation' ) . '</p>'
325 + . '<p>' . esc_html__( 'Your donation to {campaign_name} is currently being processed.', 'suredonation' ) . '</p>'
326 + . '<ul>'
327 + . '<li>' . esc_html__( 'Amount:', 'suredonation' ) . ' {amount}</li>'
328 + . '<li>' . esc_html__( 'Date:', 'suredonation' ) . ' {donation_date}</li>'
329 + . '</ul>'
330 + . '<p>' . esc_html__( "We'll notify you once it's confirmed.", 'suredonation' ) . '</p>'
331 + . '<p>— {site_title}</p>',
332 + 'from_name' => '{site_title}',
333 + 'from_email' => '{admin_email}',
334 + 'reply_to' => '',
335 + 'trigger' => 'donation_processing',
336 + ],
337 + [
338 + 'key' => 'donation_failed',
339 + 'id' => 3,
340 + 'status' => true,
341 + 'name' => __( 'Donation Failed', 'suredonation' ),
342 + 'email_to' => '{donor_email}',
343 + 'subject' => __( "We couldn't process your donation", 'suredonation' ),
344 + 'email_body' => '<p>' . esc_html__( 'Hi {donor_name},', 'suredonation' ) . '</p>'
345 + . '<p>' . esc_html__( 'Unfortunately, your donation to {campaign_name} could not be completed.', 'suredonation' ) . '</p>'
346 + . '<p>' . esc_html__( 'If you need help, feel free to contact us.', 'suredonation' ) . '</p>'
347 + . '<p>— {site_title}</p>',
348 + 'from_name' => '{site_title}',
349 + 'from_email' => '{admin_email}',
350 + 'reply_to' => '',
351 + 'trigger' => 'donation_failed',
352 + ],
353 + [
354 + 'key' => 'refund_processed',
355 + 'id' => 4,
356 + 'status' => true,
357 + 'name' => __( 'Refund Processed', 'suredonation' ),
358 + 'email_to' => '{donor_email}',
359 + 'subject' => __( 'Your donation has been refunded', 'suredonation' ),
360 + 'email_body' => '<p>' . esc_html__( 'Hi {donor_name},', 'suredonation' ) . '</p>'
361 + . '<p>' . esc_html__( 'Your donation has been refunded.', 'suredonation' ) . '</p>'
362 + . '<ul>'
363 + . '<li>' . esc_html__( 'Campaign:', 'suredonation' ) . ' {campaign_name}</li>'
364 + . '<li>' . esc_html__( 'Amount:', 'suredonation' ) . ' {refund_amount}</li>'
365 + . '</ul>'
366 + . '<p>' . esc_html__( 'If you have any questions, feel free to reach out.', 'suredonation' ) . '</p>'
367 + . '<p>— {site_title}</p>',
368 + 'from_name' => '{site_title}',
369 + 'from_email' => '{admin_email}',
370 + 'reply_to' => '',
371 + 'trigger' => 'refund_processed',
372 + ],
166 373
167 - // Redirect URL.
374 + // --- Admin Emails ---
375 + [
376 + 'key' => 'donation_receipt_admin',
377 + 'id' => 9,
378 + 'status' => true,
379 + 'name' => __( 'New Donation (Admin)', 'suredonation' ),
380 + 'email_to' => '{admin_email}',
381 + 'subject' => __( 'New donation received', 'suredonation' ),
382 + 'email_body' => '<p>' . esc_html__( 'A new donation has been received.', 'suredonation' ) . '</p>'
383 + . '<ul>'
384 + . '<li>' . esc_html__( 'Donor:', 'suredonation' ) . ' {donor_name}</li>'
385 + . '<li>' . esc_html__( 'Email:', 'suredonation' ) . ' {donor_email}</li>'
386 + . '<li>' . esc_html__( 'Campaign:', 'suredonation' ) . ' {campaign_name}</li>'
387 + . '<li>' . esc_html__( 'Amount:', 'suredonation' ) . ' {amount}</li>'
388 + . '<li>' . esc_html__( 'Date:', 'suredonation' ) . ' {donation_date}</li>'
389 + . '</ul>'
390 + . '<p>' . esc_html__( 'View details:', 'suredonation' ) . '<br />{admin_url}</p>',
391 + 'from_name' => '{site_title}',
392 + 'from_email' => '{admin_email}',
393 + 'reply_to' => '',
394 + 'trigger' => 'donation_completed',
395 + ],
396 + [
397 + 'key' => 'donation_failed_admin',
398 + 'id' => 10,
399 + 'status' => true,
400 + 'name' => __( 'Donation Failed (Admin)', 'suredonation' ),
401 + 'email_to' => '{admin_email}',
402 + 'subject' => __( 'Donation failed', 'suredonation' ),
403 + 'email_body' => '<p>' . esc_html__( 'A donation attempt has failed.', 'suredonation' ) . '</p>'
404 + . '<ul>'
405 + . '<li>' . esc_html__( 'Donor:', 'suredonation' ) . ' {donor_name}</li>'
406 + . '<li>' . esc_html__( 'Campaign:', 'suredonation' ) . ' {campaign_name}</li>'
407 + . '<li>' . esc_html__( 'Amount:', 'suredonation' ) . ' {amount}</li>'
408 + . '<li>' . esc_html__( 'Date:', 'suredonation' ) . ' {donation_date}</li>'
409 + . '</ul>'
410 + . '<p>' . esc_html__( 'Check details:', 'suredonation' ) . '<br />{admin_url}</p>',
411 + 'from_name' => '{site_title}',
412 + 'from_email' => '{admin_email}',
413 + 'reply_to' => '',
414 + 'trigger' => 'donation_failed',
415 + ],
416 + [
417 + 'key' => 'refund_processed_admin',
418 + 'id' => 11,
419 + 'status' => true,
420 + 'name' => __( 'Refund Processed (Admin)', 'suredonation' ),
421 + 'email_to' => '{admin_email}',
422 + 'subject' => __( 'Refund issued', 'suredonation' ),
423 + 'email_body' => '<p>' . esc_html__( 'A refund has been processed.', 'suredonation' ) . '</p>'
424 + . '<ul>'
425 + . '<li>' . esc_html__( 'Donor:', 'suredonation' ) . ' {donor_name}</li>'
426 + . '<li>' . esc_html__( 'Campaign:', 'suredonation' ) . ' {campaign_name}</li>'
427 + . '<li>' . esc_html__( 'Amount:', 'suredonation' ) . ' {refund_amount}</li>'
428 + . '</ul>'
429 + . '<p>' . esc_html__( 'View details:', 'suredonation' ) . '<br />{admin_url}</p>',
430 + 'from_name' => '{site_title}',
431 + 'from_email' => '{admin_email}',
432 + 'reply_to' => '',
433 + 'trigger' => 'refund_processed',
434 + ],
435 + ];
436 + // phpcs:enable Generic.Strings.UnnecessaryStringConcat.Found
437 +
438 + /**
439 + * Filter default email notifications.
440 + * Pro uses this to add subscription email notification templates.
441 + *
442 + * @param array<int, array<string, mixed>> $defaults Default notification configs.
443 + * @since 1.0.0
444 + */
445 + return apply_filters( 'suredonation_default_email_notifications', $defaults );
446 + }
447 +
448 + /**
449 + * Register email notification meta for donation forms.
450 + *
451 + * Stores per-form email notification settings as a JSON string.
452 + *
453 + * @return void
454 + * @since 1.0.0
455 + */
456 + public function register_email_notifications_meta() {
168 457 register_post_meta(
169 - $post_type,
170 - '_suredonation_form_redirectUrl',
458 + Donation_Form::POST_TYPE,
459 + self::EMAIL_NOTIFICATIONS_META_KEY,
171 460 [
172 461 'type' => 'string',
173 - 'description' => __( 'URL to redirect after donation.', 'suredonation' ),
462 + 'description' => __( 'Form email notification settings.', 'suredonation' ),
174 463 'single' => true,
175 464 'default' => '',
176 - 'show_in_rest' => true,
177 - 'sanitize_callback' => 'esc_url_raw',
178 - 'auth_callback' => function () {
179 - return current_user_can( 'edit_posts' );
465 + 'show_in_rest' => [
466 + 'schema' => [
467 + 'type' => 'string',
468 + 'context' => [ 'edit' ],
469 + ],
470 + ],
471 + 'sanitize_callback' => [ $this, 'sanitize_email_notifications' ],
472 + 'auth_callback' => static function () {
473 + return current_user_can( 'manage_options' );
180 474 },
181 475 ]
182 476 );
477 + }
478 +
479 + /**
480 + * Sanitize email notification settings.
481 + *
482 + * @param string $value JSON string of email notification settings.
483 + * @return string Sanitized JSON string.
484 + * @since 1.0.0
485 + */
486 + public function sanitize_email_notifications( $value ) {
487 + if ( empty( $value ) || ! is_string( $value ) ) {
488 + return '';
489 + }
490 +
491 + $data = json_decode( $value, true );
492 +
493 + if ( ! is_array( $data ) ) {
494 + return '';
495 + }
496 +
497 + $sanitized = [];
498 + foreach ( $data as $notification ) {
499 + if ( ! is_array( $notification ) ) {
500 + continue;
501 + }
502 +
503 + $trigger = isset( $notification['trigger'] ) && is_string( $notification['trigger'] ) ? $notification['trigger'] : '';
504 +
505 + // Preserve the trigger verbatim rather than validating against the
506 + // registered list. 'all' means "send on every event", so coercing an
507 + // unrecognised trigger to it silently rewires that notification to
508 + // fire on every donation event. Saving a form while Pro is inactive
509 + // did exactly that to the recurring templates: the trigger was not
510 + // registered, so a customised "Subscription Created" became a message
511 + // sent on completed, failed, processing and refunded donations, and
512 + // the editor stopped recognising it as recurring and appended a
513 + // duplicate set.
514 + //
515 + // An unregistered trigger is already inert: dispatch is an equality
516 + // match against an event name, and the code that fires the recurring
517 + // events does not load while Pro is inactive. Preserving the value
518 + // costs nothing and lets the notification resume working, with its
519 + // customisations, as soon as Pro is active again.
520 + $sanitized[] = [
521 + // Stable machine identity. `id` is reassigned when a set is
522 + // re-seeded and `name` is user-editable and translated, so neither
523 + // survives as a way to recognise a notification later.
524 + 'key' => isset( $notification['key'] ) && is_string( $notification['key'] ) ? sanitize_key( $notification['key'] ) : '',
525 + 'id' => isset( $notification['id'] ) ? absint( $notification['id'] ) : 0,
526 + 'status' => isset( $notification['status'] ) ? (bool) $notification['status'] : true,
527 + 'name' => isset( $notification['name'] ) ? sanitize_text_field( $notification['name'] ) : '',
528 + 'email_to' => isset( $notification['email_to'] ) ? sanitize_text_field( $notification['email_to'] ) : '',
529 + 'subject' => isset( $notification['subject'] ) ? sanitize_text_field( $notification['subject'] ) : '',
530 + 'email_body' => isset( $notification['email_body'] ) ? wp_kses_post( $notification['email_body'] ) : '',
531 + 'from_name' => isset( $notification['from_name'] ) ? sanitize_text_field( $notification['from_name'] ) : '',
532 + 'from_email' => isset( $notification['from_email'] ) ? sanitize_text_field( $notification['from_email'] ) : '',
533 + 'reply_to' => isset( $notification['reply_to'] ) ? sanitize_text_field( $notification['reply_to'] ) : '',
534 + // A missing trigger must stay inert. Dispatch skips an empty
535 + // trigger but treats 'all' as "fire on every event", so falling
536 + // back to 'all' routes the unknown case to the most permissive
537 + // outcome — the opposite of what a missing value should mean.
538 + 'trigger' => '' !== $trigger ? sanitize_key( $trigger ) : '',
539 + ];
540 + }
541 +
542 + $encoded = wp_json_encode( $sanitized );
543 + return is_string( $encoded ) ? $encoded : '';
544 + }
545 +
546 +
547 + /**
548 + * Sanitize form confirmation settings.
549 + *
550 + * @param string $value JSON string of confirmation settings.
551 + * @return string Sanitized JSON string.
552 + * @since 1.0.0
553 + */
554 + public function sanitize_confirmation_settings( $value ) {
555 + $fallback_data = [
556 + 'confirmation_type' => 'same page',
557 + 'message' => '',
558 + 'submission_action' => 'hide form',
559 + 'custom_url' => '',
560 + 'page_url' => '',
561 + ];
562 + $fallback = (string) wp_json_encode( $fallback_data );
563 +
564 + if ( empty( $value ) || ! is_string( $value ) ) {
565 + return $fallback;
566 + }
567 +
568 + $data = json_decode( $value, true );
569 +
570 + if ( ! is_array( $data ) ) {
571 + return $fallback;
572 + }
573 +
574 + $allowed_types = [ 'same page', 'different page', 'custom url' ];
575 + $allowed_actions = [ 'hide form', 'reset form' ];
576 +
577 + $message = isset( $data['message'] ) ? wp_kses_post( $data['message'] ) : '';
578 +
579 + $sanitized = [
580 + 'confirmation_type' => isset( $data['confirmation_type'] ) && in_array( $data['confirmation_type'], $allowed_types, true )
581 + ? $data['confirmation_type']
582 + : 'same page',
583 + 'message' => $message,
584 + 'submission_action' => isset( $data['submission_action'] ) && in_array( $data['submission_action'], $allowed_actions, true )
585 + ? $data['submission_action']
586 + : 'hide form',
587 + 'custom_url' => isset( $data['custom_url'] ) ? esc_url_raw( $data['custom_url'] ) : '',
588 + 'page_url' => isset( $data['page_url'] ) ? esc_url_raw( $data['page_url'] ) : '',
589 + ];
590 +
591 + $encoded = wp_json_encode( $sanitized );
592 + return is_string( $encoded ) ? $encoded : $fallback;
183 593 }
184 594 }