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 +179 -43 1.1.2 → 1.6.1 View file →
@@ -34,8 +34,11 @@
34 34 * @since 0.0.1
35 35 */
36 36 public function __construct() {
37 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' ] );
38 41 add_action( 'init', [ $this, 'register_form_settings_meta' ] );
39 42 add_action( 'init', [ $this, 'register_email_notifications_meta' ] );
40 43 }
41 44
@@ -45,31 +48,22 @@
45 48 * @return void
46 49 * @since 0.0.1
47 50 */
48 51 public function enqueue_editor_assets() {
49 - $screen = get_current_screen();
50 -
51 52 // Only load on donation form editor.
52 - if ( ! $screen || Donation_Form::POST_TYPE !== $screen->post_type ) {
53 + if ( ! $this->is_form_editor_screen() ) {
53 54 return;
54 55 }
55 56
56 - // Get the asset file for dependencies.
57 - $editor_asset_file = SUREDONATION_DIR . 'assets/build/editor/editor.asset.php';
58 - $editor_asset = file_exists( $editor_asset_file )
59 - ? include $editor_asset_file
60 - : [
61 - 'dependencies' => [
62 - 'wp-plugins',
63 - 'wp-editor',
64 - 'wp-components',
65 - 'wp-data',
66 - 'wp-element',
67 - 'wp-i18n',
68 - ],
69 - 'version' => SUREDONATION_VER,
70 - ];
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' );
71 63
64 + $editor_asset = $this->get_editor_asset();
65 +
72 66 // Editor plugin JS.
73 67 wp_enqueue_script(
74 68 'suredonation-form-editor',
75 69 SUREDONATION_URL . 'assets/build/editor/editor.js',
@@ -77,33 +71,55 @@
77 71 $editor_asset['version'],
78 72 true
79 73 );
80 74
81 - // Editor styles.
82 - wp_enqueue_style(
83 - 'suredonation-form-editor',
84 - SUREDONATION_URL . 'assets/build/editor/editor.css',
85 - [ 'wp-components' ],
86 - $editor_asset['version']
87 - );
75 + // Editor styles are enqueued from enqueue_editor_styles(), not here.
88 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 +
89 83 // Localized data.
90 84 $global_currency = Payment_Helper::get_global_setting( 'currency', 'USD' );
91 85 $global_currency = is_string( $global_currency ) ? $global_currency : 'USD';
92 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 +
93 118 wp_localize_script(
94 119 'suredonation-form-editor',
95 120 'suredonationFormEditor',
96 - [
97 - 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
98 - 'nonce' => wp_create_nonce( 'suredonation_editor_nonce' ),
99 - 'postType' => Donation_Form::POST_TYPE,
100 - 'smartTags' => Helper::get_smart_tags()['confirmation'],
101 - 'emailSmartTags' => Helper::get_smart_tags()['email_grouped'],
102 - 'defaultEmailNotifications' => $this->get_default_email_notifications(),
103 - 'currency' => $global_currency,
104 - 'currencySymbol' => Payment_Helper::get_currency_symbol( $global_currency ),
105 - ]
121 + $editor_data
106 122 );
107 123
108 124 // Set script translations.
109 125 wp_set_script_translations( 'suredonation-form-editor', 'suredonation' );
@@ -109,8 +125,102 @@
109 125 wp_set_script_translations( 'suredonation-form-editor', 'suredonation' );
110 126 }
111 127
112 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 + /**
113 223 * Register form settings meta fields.
114 224 *
115 225 * Stores form confirmation settings as a single JSON string.
116 226 *
@@ -183,8 +293,9 @@
183 293 // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found -- Readability.
184 294 $defaults = [
185 295 // --- Donor Emails ---
186 296 [
297 + 'key' => 'donation_receipt',
187 298 'id' => 1,
188 299 'status' => true,
189 300 'name' => __( 'Donation Receipt', 'suredonation' ),
190 301 'email_to' => '{donor_email}',
@@ -203,8 +314,9 @@
203 314 'reply_to' => '',
204 315 'trigger' => 'donation_completed',
205 316 ],
206 317 [
318 + 'key' => 'donation_processing',
207 319 'id' => 2,
208 320 'status' => true,
209 321 'name' => __( 'Donation Processing', 'suredonation' ),
210 322 'email_to' => '{donor_email}',
@@ -222,8 +334,9 @@
222 334 'reply_to' => '',
223 335 'trigger' => 'donation_processing',
224 336 ],
225 337 [
338 + 'key' => 'donation_failed',
226 339 'id' => 3,
227 340 'status' => true,
228 341 'name' => __( 'Donation Failed', 'suredonation' ),
229 342 'email_to' => '{donor_email}',
@@ -237,8 +350,9 @@
237 350 'reply_to' => '',
238 351 'trigger' => 'donation_failed',
239 352 ],
240 353 [
354 + 'key' => 'refund_processed',
241 355 'id' => 4,
242 356 'status' => true,
243 357 'name' => __( 'Refund Processed', 'suredonation' ),
244 358 'email_to' => '{donor_email}',
@@ -258,8 +372,9 @@
258 372 ],
259 373
260 374 // --- Admin Emails ---
261 375 [
376 + 'key' => 'donation_receipt_admin',
262 377 'id' => 9,
263 378 'status' => true,
264 379 'name' => __( 'New Donation (Admin)', 'suredonation' ),
265 380 'email_to' => '{admin_email}',
@@ -278,8 +393,9 @@
278 393 'reply_to' => '',
279 394 'trigger' => 'donation_completed',
280 395 ],
281 396 [
397 + 'key' => 'donation_failed_admin',
282 398 'id' => 10,
283 399 'status' => true,
284 400 'name' => __( 'Donation Failed (Admin)', 'suredonation' ),
285 401 'email_to' => '{admin_email}',
@@ -297,8 +413,9 @@
297 413 'reply_to' => '',
298 414 'trigger' => 'donation_failed',
299 415 ],
300 416 [
417 + 'key' => 'refund_processed_admin',
301 418 'id' => 11,
302 419 'status' => true,
303 420 'name' => __( 'Refund Processed (Admin)', 'suredonation' ),
304 421 'email_to' => '{admin_email}',
@@ -382,9 +499,30 @@
382 499 if ( ! is_array( $notification ) ) {
383 500 continue;
384 501 }
385 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.
386 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'] ) : '',
387 525 'id' => isset( $notification['id'] ) ? absint( $notification['id'] ) : 0,
388 526 'status' => isset( $notification['status'] ) ? (bool) $notification['status'] : true,
389 527 'name' => isset( $notification['name'] ) ? sanitize_text_field( $notification['name'] ) : '',
390 528 'email_to' => isset( $notification['email_to'] ) ? sanitize_text_field( $notification['email_to'] ) : '',
@@ -392,16 +530,13 @@
392 530 'email_body' => isset( $notification['email_body'] ) ? wp_kses_post( $notification['email_body'] ) : '',
393 531 'from_name' => isset( $notification['from_name'] ) ? sanitize_text_field( $notification['from_name'] ) : '',
394 532 'from_email' => isset( $notification['from_email'] ) ? sanitize_text_field( $notification['from_email'] ) : '',
395 533 'reply_to' => isset( $notification['reply_to'] ) ? sanitize_text_field( $notification['reply_to'] ) : '',
396 - 'trigger' => isset( $notification['trigger'] ) && in_array(
397 - $notification['trigger'],
398 - apply_filters(
399 - 'suredonation_email_notification_triggers',
400 - [ 'all', 'donation_completed', 'donation_processing', 'donation_failed', 'refund_processed' ]
401 - ),
402 - true
403 - ) ? $notification['trigger'] : 'all',
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 ) : '',
404 539 ];
405 540 }
406 541
407 542 $encoded = wp_json_encode( $sanitized );
@@ -406,8 +541,9 @@
406 541
407 542 $encoded = wp_json_encode( $sanitized );
408 543 return is_string( $encoded ) ? $encoded : '';
409 544 }
545 +
410 546
411 547 /**
412 548 * Sanitize form confirmation settings.
413 549 *