PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / extensions / blocks / donations / donations.php

donations.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at extensions/blocks/donations/donations.php

1,062 lines 39.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donations Block.
4 *
5 * @since 8.x
6 *
7 * @package automattic/jetpack
8 */
9
10 namespace Automattic\Jetpack\Extensions\Donations;
11
12 use Automattic\Jetpack\Blocks;
13 use Automattic\Jetpack\Status\Host;
14 use Automattic\Jetpack\Status\Request;
15 use Automattic\Jetpack\Tracking;
16 use Jetpack;
17 use Jetpack_Gutenberg;
18 use WP_Post;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit( 0 );
22 }
23
24 /**
25 * Registers the block for use in Gutenberg
26 * This is done via an action so that we can disable
27 * registration if we need to.
28 */
29 function register_block() {
30
31 require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
32 if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) {
33 Blocks::jetpack_register_block(
34 __DIR__,
35 array(
36 'render_callback' => __NAMESPACE__ . '\render_block',
37 'render_email_callback' => __NAMESPACE__ . '\render_email',
38 'plan_check' => true,
39 )
40 );
41 }
42 }
43 add_action( 'init', __NAMESPACE__ . '\register_block' );
44
45 /**
46 * Donations block dynamic rendering.
47 *
48 * @param array $attr Array containing the Donations block attributes.
49 * @param string $content String containing the Donations block content.
50 *
51 * @return string
52 */
53 function render_block( $attr, $content ) {
54 // Keep content as-is if rendered in other contexts than frontend (i.e. feed, emails, API, etc.).
55 if ( ! Request::is_frontend() ) {
56 $parsed = parse_blocks( $content );
57 if ( ! empty( $parsed[0] ) ) {
58 // Inject the link of the current post from the server side as the fallback link to make sure the donations block
59 // points to the correct post when it's inserted from the synced pattern (aka “My Pattern”).
60 $post_link = get_permalink();
61 $parsed[0]['attrs']['fallbackLinkUrl'] = $post_link;
62 $content = \render_block( $parsed[0] );
63 if ( preg_match( '/<a\s+class="jetpack-donations-fallback-link"\s+href="([^"]*)"/', $content, $matches ) ) {
64 $content = str_replace( $matches[1], $post_link, $content );
65 }
66 }
67
68 return $content;
69 }
70
71 require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
72
73 // If stripe isn't connected don't show anything to potential donors - they can't actually make a donation.
74 if ( ! \Jetpack_Memberships::has_connected_account() ) {
75 return '';
76 }
77
78 Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
79
80 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-currencies.php';
81
82 $default_texts = get_default_texts();
83
84 // `array_merge` lets the user-supplied attributes override only the keys
85 // they actually set. Undefined keys (new blocks that have never been
86 // edited) fall back to the defaults in the first array. User-cleared
87 // keys (empty strings explicitly saved) win over defaults, so
88 // "blank stays blank" and "never set" gets the default.
89 // Treat `show !== false` as on, so legacy blocks (where `show` was never
90 // set on oneTimeDonation) still render the one-time interval by default.
91 $donations = array();
92 if ( false !== ( $attr['oneTimeDonation']['show'] ?? true ) ) {
93 $donations['one-time'] = array_merge(
94 array(
95 'planId' => null,
96 'title' => __( 'One-Time', 'jetpack' ),
97 'class' => 'donations__one-time-item',
98 'heading' => $default_texts['oneTimeDonation']['heading'],
99 'buttonText' => $default_texts['oneTimeDonation']['buttonText'],
100 ),
101 $attr['oneTimeDonation']
102 );
103 }
104 if ( $attr['monthlyDonation']['show'] ) {
105 $donations['1 month'] = array_merge(
106 array(
107 'planId' => null,
108 'title' => __( 'Monthly', 'jetpack' ),
109 'class' => 'donations__monthly-item',
110 'heading' => $default_texts['monthlyDonation']['heading'],
111 'buttonText' => $default_texts['monthlyDonation']['buttonText'],
112 ),
113 $attr['monthlyDonation']
114 );
115 }
116 if ( $attr['annualDonation']['show'] ) {
117 $donations['1 year'] = array_merge(
118 array(
119 'planId' => null,
120 'title' => __( 'Yearly', 'jetpack' ),
121 'class' => 'donations__annual-item',
122 'heading' => $default_texts['annualDonation']['heading'],
123 'buttonText' => $default_texts['annualDonation']['buttonText'],
124 ),
125 $attr['annualDonation']
126 );
127 }
128
129 $choose_amount_text = $attr['chooseAmountText'] ?? $default_texts['chooseAmountText'];
130 $custom_amount_text = $attr['customAmountText'] ?? $default_texts['customAmountText'];
131 $currency = $attr['currency'];
132
133 // Drop intervals whose plan no longer resolves so we can compute the active tab
134 // against the actually-rendered set, not the configured set.
135 $valid_donations = array();
136 foreach ( $donations as $interval => $donation ) {
137 $plan = get_post( (int) $donation['planId'] );
138 if ( $plan && ! is_wp_error( $plan ) ) {
139 $valid_donations[ $interval ] = $donation;
140 }
141 }
142 $donations = $valid_donations;
143 $rendered_intervals = array_keys( $donations );
144
145 // Effective default = configured defaultInterval if it survived plan validation,
146 // otherwise the first rendered interval (one-time → monthly → annual).
147 $default_interval = $attr['defaultInterval'] ?? null;
148 if ( ! in_array( $default_interval, $rendered_intervals, true ) ) {
149 $default_interval = $rendered_intervals[0] ?? null;
150 }
151 $tab_content_class_map = array(
152 'one-time' => 'is-one-time',
153 '1 month' => 'is-monthly',
154 '1 year' => 'is-annual',
155 );
156 $tab_content_class = $default_interval ? $tab_content_class_map[ $default_interval ] : '';
157
158 $nav = '';
159 $headings = '';
160 $amounts = '';
161 $extra_text = '';
162 $buttons = '';
163 foreach ( $donations as $interval => $donation ) {
164 $plan_id = (int) $donation['planId'];
165
166 if ( count( $donations ) > 1 ) {
167 if ( ! $nav ) {
168 $nav .= '<div class="donations__nav">';
169 }
170 $is_active_class = $interval === $default_interval ? ' is-active' : '';
171 $nav .= sprintf(
172 '<div role="button" tabindex="0" class="donations__nav-item%3$s" data-interval="%1$s">%2$s</div>',
173 esc_attr( $interval ),
174 esc_html( $donation['title'] ),
175 esc_attr( $is_active_class )
176 );
177 }
178 $heading_text = wp_kses_post( $donation['heading'] ?? '' );
179 if ( '' !== trim( $heading_text ) ) {
180 $headings .= sprintf(
181 '<h4 class="%1$s">%2$s</h4>',
182 esc_attr( $donation['class'] ),
183 $heading_text
184 );
185 }
186 $default_index_attr = '';
187 if ( isset( $donation['defaultAmountIndex'] ) && is_numeric( $donation['defaultAmountIndex'] ) ) {
188 $default_index_attr = sprintf( ' data-default-index="%d"', (int) $donation['defaultAmountIndex'] );
189 }
190 $amounts .= sprintf(
191 '<div class="donations__amounts %s"%s>',
192 esc_attr( $donation['class'] ),
193 $default_index_attr
194 );
195 foreach ( $donation['amounts'] as $amount ) {
196 $amounts .= sprintf(
197 '<div class="donations__amount" data-amount="%1$s">%2$s</div>',
198 esc_attr( $amount ),
199 esc_html( \Jetpack_Currencies::format_price( $amount, $currency ) )
200 );
201 }
202 $amounts .= '</div>';
203 $extra_text_html = wp_kses_post( $donation['extraText'] ?? $default_texts['extraText'] );
204 if ( '' !== trim( $extra_text_html ) ) {
205 $extra_text .= sprintf(
206 '<p class="%1$s">%2$s</p>',
207 esc_attr( $donation['class'] ),
208 $extra_text_html
209 );
210 }
211 $buttons .= sprintf(
212 '<div class="wp-block-button donations__donate-button-wrapper %1$s"><a class="wp-block-button__link wp-element-button donations__donate-button %1$s" href="%2$s">%3$s</a></div>',
213 esc_attr( $donation['class'] ),
214 esc_url( \Jetpack_Memberships::get_instance()->get_subscription_url( $plan_id ) ),
215 wp_kses_post( $donation['buttonText'] )
216 );
217 }
218 if ( $nav ) {
219 $nav .= '</div>';
220 }
221
222 $custom_amount = '';
223 if ( $attr['showCustomAmount'] ) {
224 $custom_amount_html = wp_kses_post( $custom_amount_text );
225 if ( '' !== trim( $custom_amount_html ) ) {
226 $custom_amount .= sprintf( '<p>%s</p>', $custom_amount_html );
227 }
228 $default_custom_amount = $attr['customAmountPlaceholder']
229 ?? ( \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1 ) * 100;
230 $custom_amount .= sprintf(
231 '<div class="donations__amount donations__custom-amount">
232 %1$s
233 <div class="donations__amount-value" data-currency="%2$s" data-empty-text="%3$s"></div>
234 </div>',
235 esc_html( \Jetpack_Currencies::CURRENCIES[ $currency ]['symbol'] ?? '¤' ),
236 esc_attr( $currency ),
237 esc_attr( \Jetpack_Currencies::format_price( $default_custom_amount, $currency, false ) )
238 );
239 }
240
241 $display_mode = $attr['displayMode'] ?? 'inline';
242 $instance_id = wp_unique_id( 'jp-donations-' );
243 $instance_classes = $instance_id;
244 if ( isset( $attr['tabsAppearance'] ) && 'buttons' === $attr['tabsAppearance'] ) {
245 $instance_classes .= ' is-style-buttons';
246 }
247 if ( 'modal' === $display_mode ) {
248 $instance_classes .= ' is-display-modal';
249 if ( ! empty( $attr['triggerSticky'] ) ) {
250 $instance_classes .= ' is-sticky';
251 }
252 }
253 $wrapper_attr_array = array( 'class' => $instance_classes );
254 if ( $default_interval ) {
255 $wrapper_attr_array['data-default-interval'] = $default_interval;
256 }
257 $wrapper_attr_array = array_merge( $wrapper_attr_array, build_security_data_attrs( $attr, $currency ) );
258 $wrapper_attrs = get_block_wrapper_attributes( $wrapper_attr_array );
259 $custom_styles = build_custom_styles( $attr, '.' . $instance_id );
260
261 $choose_amount_html = wp_kses_post( $choose_amount_text );
262 $choose_amount_block = '' !== trim( $choose_amount_html ) ? '<p>' . $choose_amount_html . '</p>' : '';
263
264 if ( 'modal' === $display_mode ) {
265 $trigger_text = $attr['triggerButtonText'] ?? $default_texts['triggerButtonText'];
266 $trigger_icon_key = $attr['triggerIcon'] ?? 'heart';
267 $trigger_svg = get_trigger_icon_svg( $trigger_icon_key );
268 $modal_id = $instance_id . '-modal';
269
270 return sprintf(
271 '
272 <div %1$s>%9$s
273 <button
274 class="donations__trigger-button wp-block-button__link wp-element-button"
275 aria-haspopup="dialog"
276 aria-controls="%11$s"
277 >%12$s%10$s</button>
278 <div
279 id="%11$s"
280 class="donations__modal-overlay"
281 role="dialog"
282 aria-modal="true"
283 aria-label="%10$s"
284 hidden
285 >
286 <div class="donations__modal-dialog">
287 <button class="donations__modal-close" aria-label="%13$s">&#x2715;</button>
288 <div class="donations__modal-content">
289 <div class="donations__container">
290 %2$s
291 <div class="donations__content">
292 <div class="donations__tab %14$s">
293 %3$s
294 %4$s
295 %5$s
296 %6$s
297 <div class="donations__range-error"></div>
298 <hr class="donations__separator">
299 %7$s
300 %8$s
301 </div>
302 </div>
303 </div>
304 </div>
305 </div>
306 </div>
307 </div>
308 ',
309 $wrapper_attrs,
310 $nav,
311 $headings,
312 $choose_amount_block,
313 $amounts,
314 $custom_amount,
315 $extra_text,
316 $buttons,
317 $custom_styles ? '<style>' . $custom_styles . '</style>' : '',
318 esc_html( $trigger_text ),
319 esc_attr( $modal_id ),
320 $trigger_svg,
321 esc_attr__( 'Close', 'jetpack' ),
322 esc_attr( $tab_content_class )
323 );
324 }
325
326 return sprintf(
327 '
328 <div %1$s>%9$s
329 <div class="donations__container">
330 %2$s
331 <div class="donations__content">
332 <div class="donations__tab %10$s">
333 %3$s
334 %4$s
335 %5$s
336 %6$s
337 <div class="donations__range-error"></div>
338 <hr class="donations__separator">
339 %7$s
340 %8$s
341 </div>
342 </div>
343 </div>
344 </div>
345 ',
346 $wrapper_attrs,
347 $nav,
348 $headings,
349 $choose_amount_block,
350 $amounts,
351 $custom_amount,
352 $extra_text,
353 $buttons,
354 $custom_styles ? '<style>' . $custom_styles . '</style>' : '',
355 esc_attr( $tab_content_class )
356 );
357 }
358
359 /**
360 * WooCommerce Email Editor render callback for the Donations block.
361 *
362 * Builds a dedicated email version of the block from its attributes (rather than
363 * post-processing the interactive frontend markup) so customizations — per
364 * interval heading, supporting text and button label — are preserved. Each
365 * interval is rendered as an email-safe table section with a CTA button drawn by
366 * the Button block's own email renderer.
367 *
368 * @param string $block_content The rendered block content (unused; we rebuild from attributes).
369 * @param array $parsed_block The parsed block data.
370 * @param object $rendering_context The email rendering context.
371 *
372 * @return string
373 */
374 function render_email( $block_content, array $parsed_block, $rendering_context ) {
375 if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] )
376 || ! function_exists( '\Automattic\Jetpack\Extensions\Button\render_email' )
377 || ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' ) ) {
378 return '';
379 }
380
381 $attr = $parsed_block['attrs'];
382 $default_texts = get_default_texts();
383
384 $url = get_permalink();
385 if ( ! is_string( $url ) || '' === $url ) {
386 $url = is_string( $attr['fallbackLinkUrl'] ?? null ) && '' !== $attr['fallbackLinkUrl'] ? $attr['fallbackLinkUrl'] : '#';
387 }
388
389 $alignment = in_array( $attr['contentAlignment'] ?? '', array( 'left', 'center', 'right' ), true ) ? $attr['contentAlignment'] : 'left';
390 $table_style = sprintf( 'width:100%%;max-width:%dpx;border-collapse:collapse;', get_email_target_width( $rendering_context ) );
391
392 $sections = '';
393 foreach ( array(
394 'oneTimeDonation' => true,
395 'monthlyDonation' => false,
396 'annualDonation' => false,
397 ) as $key => $default_show ) {
398 $interval = is_array( $attr[ $key ] ?? null ) ? $attr[ $key ] : array();
399 if ( false === ( $interval['show'] ?? $default_show ) ) {
400 continue;
401 }
402
403 $heading = wp_kses_post( $interval['heading'] ?? $default_texts[ $key ]['heading'] );
404 $extra = wp_kses_post( $interval['extraText'] ?? $default_texts['extraText'] );
405
406 $content = '';
407 if ( '' !== trim( wp_strip_all_tags( $heading ) ) ) {
408 $content .= sprintf(
409 '<p style="margin:0 0 4px;font-size:18px;font-weight:700;line-height:1.3;text-align:%s;">%s</p>',
410 esc_attr( $alignment ),
411 $heading
412 );
413 }
414 if ( '' !== trim( wp_strip_all_tags( $extra ) ) ) {
415 $content .= sprintf(
416 '<p style="margin:0 0 14px;font-size:15px;line-height:1.5;text-align:%s;">%s</p>',
417 esc_attr( $alignment ),
418 $extra
419 );
420 }
421 $content .= render_email_donate_button( $interval['buttonText'] ?? $default_texts[ $key ]['buttonText'], $url, $attr, $rendering_context );
422
423 $is_first = '' === $sections;
424 $cell_style = sprintf(
425 'text-align:%1$s;padding:%2$s;%3$s',
426 esc_attr( $alignment ),
427 $is_first ? '0 0 24px' : '24px 0',
428 $is_first ? '' : 'border-top:1px solid #e0e0e0;'
429 );
430 $sections .= \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper(
431 $content,
432 array( 'style' => $table_style ),
433 array( 'style' => $cell_style )
434 );
435 }
436
437 return $sections;
438 }
439
440 /**
441 * Render a single donation CTA as an email-safe button via the Button block's
442 * email renderer, carrying over the donation block's button styling.
443 *
444 * @param string $text Button label.
445 * @param string $url Button destination.
446 * @param array $attr Donations block attributes.
447 * @param object $rendering_context The email rendering context.
448 * @return string
449 */
450 function render_email_donate_button( $text, $url, $attr, $rendering_context ) {
451 $text = trim( wp_strip_all_tags( (string) $text ) );
452
453 $button_attrs = array(
454 'text' => '' !== $text ? $text : __( 'Donate', 'jetpack' ),
455 'url' => $url,
456 );
457
458 if ( ! empty( $attr['buttonFontSize'] ) && is_string( $attr['buttonFontSize'] ) ) {
459 $button_attrs['customFontSize'] = $attr['buttonFontSize'];
460 }
461 if ( isset( $attr['buttonBorderRadius'] ) && is_string( $attr['buttonBorderRadius'] ) ) {
462 $radius = (int) $attr['buttonBorderRadius'];
463 if ( $radius > 0 ) {
464 $button_attrs['borderRadius'] = $radius;
465 }
466 }
467
468 return \Automattic\Jetpack\Extensions\Button\render_email(
469 '',
470 array( 'attrs' => $button_attrs ),
471 $rendering_context
472 );
473 }
474
475 /**
476 * Resolve the target content width (in px) from the email rendering context,
477 * falling back to a sensible default.
478 *
479 * @param object $rendering_context The email rendering context.
480 * @return int Width in pixels.
481 */
482 function get_email_target_width( $rendering_context ) {
483 $target_width = 600;
484 if ( is_object( $rendering_context ) && method_exists( $rendering_context, 'get_layout_width_without_padding' ) ) {
485 $width = $rendering_context->get_layout_width_without_padding();
486 if ( is_string( $width ) && preg_match( '/(\d+)px/', $width, $matches ) ) {
487 $parsed = (int) $matches[1];
488 if ( $parsed > 0 ) {
489 $target_width = $parsed;
490 }
491 }
492 }
493 return $target_width;
494 }
495
496 /**
497 * Build data-attributes array for security (min/max amount) constraints.
498 *
499 * Extracted so it can be tested independently of the full render pipeline.
500 *
501 * @since 15.9
502 *
503 * @param array $attr Block attributes.
504 * @param string $currency Currency code (e.g. 'USD').
505 * @return array Associative array of data-attribute name => value.
506 */
507 function build_security_data_attrs( $attr, $currency ) {
508 $attrs = array();
509 $min_amount = isset( $attr['minimumAmount'] ) ? (float) $attr['minimumAmount'] : null;
510 $max_amount = isset( $attr['maximumAmount'] ) ? (float) $attr['maximumAmount'] : null;
511 if ( null !== $min_amount ) {
512 $attrs['data-min-amount'] = $min_amount;
513 $attrs['data-min-error'] = sprintf(
514 /* translators: %s: minimum donation amount formatted with currency symbol */
515 __( 'The minimum donation amount is %s.', 'jetpack' ),
516 \Jetpack_Currencies::format_price( (string) $min_amount, $currency )
517 );
518 }
519 if ( null !== $max_amount ) {
520 $attrs['data-max-amount'] = $max_amount;
521 $attrs['data-max-error'] = sprintf(
522 /* translators: %s: maximum donation amount formatted with currency symbol */
523 __( 'The maximum donation amount is %s.', 'jetpack' ),
524 \Jetpack_Currencies::format_price( (string) $max_amount, $currency )
525 );
526 }
527 $stripe_min = \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1;
528 $attrs['data-stripe-min-error'] = sprintf(
529 /* translators: %s: payment processor minimum donation amount formatted with currency symbol */
530 _x( 'The minimum donation amount is %s.', 'payment processor minimum', 'jetpack' ),
531 \Jetpack_Currencies::format_price( (string) $stripe_min, $currency )
532 );
533 return $attrs;
534 }
535
536 /**
537 * Build a CSS string scoping per-state and tab-level style rules to a single
538 * block instance.
539 *
540 * @param array $attr Block attributes.
541 * @param string $scope CSS class selector (with leading dot) unique to this instance.
542 * @return string CSS rules joined into one string, or '' when no overrides are set.
543 */
544 function build_custom_styles( $attr, $scope ) {
545 $tab_padding = isset( $attr['tabPadding'] ) && is_array( $attr['tabPadding'] ) ? $attr['tabPadding'] : array();
546 $button_padding = isset( $attr['buttonPadding'] ) && is_array( $attr['buttonPadding'] ) ? $attr['buttonPadding'] : array();
547
548 $groups = array(
549 array(
550 'selector' => $scope . ' .donations__nav-item',
551 'properties' => array(
552 'font-size' => $attr['tabFontSize'] ?? '',
553 'padding-top' => $tab_padding['top'] ?? '',
554 'padding-right' => $tab_padding['right'] ?? '',
555 'padding-bottom' => $tab_padding['bottom'] ?? '',
556 'padding-left' => $tab_padding['left'] ?? '',
557 ),
558 ),
559 array(
560 'selector' => $scope . ' .donations__nav-item.is-active',
561 'properties' => array(
562 'background' => $attr['activeTabBackgroundColor'] ?? '',
563 'color' => $attr['activeTabTextColor'] ?? '',
564 ),
565 ),
566 array(
567 'selector' => $scope . ' .donations__nav-item:not(.is-active)',
568 'properties' => array(
569 'background' => $attr['inactiveTabBackgroundColor'] ?? '',
570 'color' => $attr['inactiveTabTextColor'] ?? '',
571 ),
572 ),
573 array(
574 'selector' => $scope . ' .donations__amount.is-selected',
575 'properties' => array(
576 'background-color' => $attr['selectedAmountBackgroundColor'] ?? '',
577 'color' => $attr['selectedAmountTextColor'] ?? '',
578 ),
579 ),
580 array(
581 'selector' => $scope . ' .donations__amount.is-selected',
582 'properties' => array(
583 // Override only the outer ring color; the inner 1px white separator stays put.
584 'box-shadow' => isset( $attr['selectedAmountOutlineColor'] ) && '' !== $attr['selectedAmountOutlineColor']
585 ? '0 0 0 1px #fff,0 0 0 3px ' . $attr['selectedAmountOutlineColor']
586 : '',
587 ),
588 ),
589 array(
590 'selector' => $scope . ' .donations__donate-button',
591 'properties' => array(
592 'font-size' => $attr['buttonFontSize'] ?? '',
593 'padding-top' => $button_padding['top'] ?? '',
594 'padding-right' => $button_padding['right'] ?? '',
595 'padding-bottom' => $button_padding['bottom'] ?? '',
596 'padding-left' => $button_padding['left'] ?? '',
597 ),
598 ),
599 );
600
601 $rules = array();
602
603 $content_alignment = $attr['contentAlignment'] ?? '';
604 if ( in_array( $content_alignment, array( 'left', 'center', 'right' ), true ) ) {
605 $rules[] = $scope . ' .donations__content{text-align:' . $content_alignment . '}';
606 $justify_map = array(
607 'left' => 'flex-start',
608 'center' => 'center',
609 'right' => 'flex-end',
610 );
611 $rules[] = $scope . ' .donations__amounts{justify-content:' . $justify_map[ $content_alignment ] . '}';
612 }
613
614 foreach ( $groups as $group ) {
615 $decls = array();
616 foreach ( $group['properties'] as $property => $value ) {
617 $safe = sanitize_css_value( $value );
618 if ( '' !== $safe ) {
619 $decls[] = $property . ':' . $safe;
620 }
621 }
622 if ( $decls ) {
623 $rules[] = $group['selector'] . '{' . implode( ';', $decls ) . '}';
624 }
625 }
626
627 // User-set tab border color: applies to the default-style nav bottom
628 // divider, the per-tab dividers, and the buttons-style pill borders.
629 $tab_border_safe = sanitize_css_value( $attr['tabBorderColor'] ?? '' );
630 if ( '' !== $tab_border_safe ) {
631 $rules[] = $scope . ' .donations__nav,' . $scope . ' .donations__nav-item,' . $scope . ' .donations__nav-item.is-active{border-color:' . $tab_border_safe . '}';
632 }
633
634 $button_radius_decls = build_radius_decls( $attr['buttonBorderRadius'] ?? null );
635 if ( $button_radius_decls ) {
636 $rules[] = $scope . ' .donations__donate-button{' . implode( ';', $button_radius_decls ) . '}';
637 }
638
639 // User-set amount tile font size, border (BorderBoxControl shape) and
640 // border radius (BorderRadiusControl shape). Applies to all amount tiles
641 // (preset + custom); selected-state colors above only kick in when an
642 // amount has the is-selected class.
643 $amount_decls = array();
644 $amount_font = sanitize_css_value( $attr['amountFontSize'] ?? '' );
645 if ( '' !== $amount_font ) {
646 $amount_decls[] = 'font-size:' . $amount_font;
647 }
648 $amount_decls = array_merge( $amount_decls, build_border_decls( $attr['amountBorder'] ?? null ) );
649 $amount_decls = array_merge( $amount_decls, build_radius_decls( $attr['amountBorderRadius'] ?? null ) );
650 if ( $amount_decls ) {
651 $rules[] = $scope . ' .donations__amount{' . implode( ';', $amount_decls ) . '}';
652 }
653
654 $button_alignment = $attr['buttonAlignment'] ?? '';
655 if ( in_array( $button_alignment, array( 'left', 'center', 'right' ), true ) ) {
656 $rules[] = $scope . ' .donations__donate-button-wrapper{text-align:' . $button_alignment . '}';
657 } elseif ( 'full' === $button_alignment ) {
658 $rules[] = $scope . ' .donations__donate-button-wrapper{display:block;width:100%}'
659 . $scope . ' .donations__donate-button{display:block;width:100%;box-sizing:border-box;text-align:center}';
660 }
661
662 // Pop-up display mode: wire up trigger-button alignment via text-align on
663 // the wrapper so the inline-flex button responds to the alignment toolbar.
664 if ( 'modal' === ( $attr['displayMode'] ?? 'inline' ) ) {
665 if ( in_array( $content_alignment, array( 'left', 'center', 'right' ), true ) ) {
666 $rules[] = $scope . '{text-align:' . $content_alignment . '}';
667 }
668 }
669
670 // Wrapper border (In-page mode only). Compound selector (.wp-block-jetpack-donations.jp-donations-N)
671 // has specificity 0,2,0 and wins over the default single-class rule (0,1,0) in common.scss.
672 if ( 'modal' !== ( $attr['displayMode'] ?? 'inline' ) ) {
673 $wrapper_border_decls = array_merge(
674 build_border_decls( $attr['blockBorder'] ?? null ),
675 build_radius_decls( $attr['blockBorderRadius'] ?? null )
676 );
677 if ( $wrapper_border_decls ) {
678 $rules[] = '.wp-block-jetpack-donations' . $scope . '{' . implode( ';', $wrapper_border_decls ) . '}';
679 }
680 }
681
682 return implode( '', $rules );
683 }
684
685 /**
686 * Convert a uniform-or-split BorderBoxControl value into individual CSS declarations.
687 * Uniform shape: { color, style, width }. Split shape: { top: {...}, right: ..., etc. }.
688 *
689 * @param mixed $border BorderBoxControl value (or null).
690 * @return array List of CSS declaration strings (e.g. "border-color:#abc"), already sanitized.
691 */
692 function build_border_decls( $border ) {
693 if ( ! is_array( $border ) ) {
694 return array();
695 }
696 $decls = array();
697 $sides = array( 'top', 'right', 'bottom', 'left' );
698 $is_split = false;
699 foreach ( $sides as $side ) {
700 if ( isset( $border[ $side ] ) ) {
701 $is_split = true;
702 break;
703 }
704 }
705 if ( $is_split ) {
706 foreach ( $sides as $side ) {
707 $sb = $border[ $side ] ?? null;
708 if ( ! is_array( $sb ) ) {
709 continue;
710 }
711 foreach ( array( 'color', 'style', 'width' ) as $prop ) {
712 $safe = sanitize_css_value( $sb[ $prop ] ?? '' );
713 if ( '' !== $safe ) {
714 $decls[] = 'border-' . $side . '-' . $prop . ':' . $safe;
715 }
716 }
717 }
718 } else {
719 foreach ( array( 'color', 'style', 'width' ) as $prop ) {
720 $safe = sanitize_css_value( $border[ $prop ] ?? '' );
721 if ( '' !== $safe ) {
722 $decls[] = 'border-' . $prop . ':' . $safe;
723 }
724 }
725 }
726 return $decls;
727 }
728
729 /**
730 * Convert a uniform-or-per-corner BorderRadiusControl value into CSS declarations.
731 * Uniform shape: a string like "8px". Per-corner shape:
732 * { topLeft, topRight, bottomRight, bottomLeft } each with string values.
733 *
734 * @param mixed $radius BorderRadiusControl value (or null).
735 * @return array List of CSS declaration strings.
736 */
737 function build_radius_decls( $radius ) {
738 if ( is_string( $radius ) && '' !== $radius ) {
739 $safe = sanitize_css_value( $radius );
740 return '' !== $safe ? array( 'border-radius:' . $safe ) : array();
741 }
742 if ( ! is_array( $radius ) ) {
743 return array();
744 }
745 $corners = array(
746 'topLeft' => 'border-top-left-radius',
747 'topRight' => 'border-top-right-radius',
748 'bottomRight' => 'border-bottom-right-radius',
749 'bottomLeft' => 'border-bottom-left-radius',
750 );
751 $decls = array();
752 foreach ( $corners as $key => $css_prop ) {
753 $safe = sanitize_css_value( $radius[ $key ] ?? '' );
754 if ( '' !== $safe ) {
755 $decls[] = $css_prop . ':' . $safe;
756 }
757 }
758 return $decls;
759 }
760
761 /**
762 * Sanitize a user-supplied CSS value (color, length, etc.) for safe inclusion
763 * in a <style> element. Strips characters that could break out of the style
764 * context (<, >, {, }, ;, quotes, backslash) and caps length, while leaving
765 * valid hex / rgb() / hsl() / var() / named-color / px / rem / em values intact.
766 *
767 * @param mixed $value Raw attribute value.
768 * @return string Sanitized value, or '' if rejected.
769 */
770 function sanitize_css_value( $value ) {
771 if ( ! is_string( $value ) || '' === $value ) {
772 return '';
773 }
774 $value = trim( $value );
775 if ( strlen( $value ) > 100 ) {
776 return '';
777 }
778 if ( preg_match( '/[<>{};\\\\\'"]/', $value ) ) {
779 return '';
780 }
781 return $value;
782 }
783
784 /**
785 * Get the default texts for the block.
786 *
787 * @return array
788 */
789 function get_default_texts() {
790 return array(
791 'chooseAmountText' => __( 'Choose an amount', 'jetpack' ),
792 'customAmountText' => __( 'Or enter a custom amount', 'jetpack' ),
793 'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ),
794 'triggerButtonText' => __( 'Donate', 'jetpack' ),
795 'oneTimeDonation' => array(
796 'heading' => __( 'Make a one-time donation', 'jetpack' ),
797 'buttonText' => __( 'Donate', 'jetpack' ),
798 ),
799 'monthlyDonation' => array(
800 'heading' => __( 'Make a monthly donation', 'jetpack' ),
801 'buttonText' => __( 'Donate monthly', 'jetpack' ),
802 ),
803 'annualDonation' => array(
804 'heading' => __( 'Make a yearly donation', 'jetpack' ),
805 'buttonText' => __( 'Donate yearly', 'jetpack' ),
806 ),
807 );
808 }
809
810 /**
811 * Return inline SVG markup for a trigger button icon.
812 *
813 * Path data sourced from icons.js ICON_SVG_PATHS — keep in sync.
814 *
815 * @param string $icon_key Icon key (e.g. 'coffee', 'heart', 'gift').
816 * @return string SVG HTML string, or '' when key is 'none' or unknown.
817 */
818 function get_trigger_icon_svg( $icon_key ) {
819 $paths = array(
820 'heart' => array( 'd' => 'M16.5 4.5c2.206 0 4 1.794 4 4 0 4.67-5.543 8.94-8.5 11.023C9.043 17.44 3.5 13.17 3.5 8.5c0-2.206 1.794-4 4-4 1.298 0 2.522.638 3.273 1.706L12 7.953l1.227-1.746c.75-1.07 1.975-1.707 3.273-1.707m0-1.5c-1.862 0-3.505.928-4.5 2.344C11.005 3.928 9.362 3 7.5 3 4.462 3 2 5.462 2 8.5c0 5.72 6.5 10.438 10 12.85 3.5-2.412 10-7.13 10-12.85C22 5.462 19.538 3 16.5 3z' ),
821 'gift' => array( 'd' => 'M15.333 4C16.6677 4 17.75 5.0823 17.75 6.41699V6.75C17.75 7.20058 17.6394 7.62468 17.4473 8H18.5C19.2767 8 19.9154 8.59028 19.9922 9.34668L20 9.5V18.5C20 19.3284 19.3284 20 18.5 20H5.5C4.72334 20 4.08461 19.4097 4.00781 18.6533L4 18.5V9.5L4.00781 9.34668C4.07949 8.64069 4.64069 8.07949 5.34668 8.00781L5.5 8H6.55273C6.36065 7.62468 6.25 7.20058 6.25 6.75V6.41699C6.25 5.0823 7.3323 4 8.66699 4C10.0436 4.00011 11.2604 4.68183 12 5.72559C12.7396 4.68183 13.9564 4.00011 15.333 4ZM5.5 18.5H11.25V9.5H5.5V18.5ZM12.75 18.5H18.5V9.5H12.75V18.5ZM8.66699 5.5C8.16073 5.5 7.75 5.91073 7.75 6.41699V6.75C7.75 7.44036 8.30964 8 9 8H11.2461C11.2021 6.61198 10.0657 5.50017 8.66699 5.5ZM15.333 5.5C13.9343 5.50017 12.7979 6.61198 12.7539 8H15C15.6904 8 16.25 7.44036 16.25 6.75V6.41699C16.25 5.91073 15.8393 5.5 15.333 5.5Z' ),
822 'star' => array( 'd' => 'M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z' ),
823 'thumbs-up' => array( 'd' => 'm3 12 1 8h1.5l-1-8H3Zm15.8-2h-4.4l.8-3.6c.3-1.3-.7-2.4-1.9-2.4h-.2c-.6 0-1.2.3-1.6.8l-5 6.6c-.3.4-.4.8-.4 1.2v.2l.7 5.4v.2c.2.9 1 1.5 1.9 1.5h8.2c.9 0 1.7-.6 1.9-1.4l1.8-6c.4-1.3-.6-2.6-1.9-2.6Zm.5 2.1-1.8 6c0 .2-.3.4-.5.4H8.8c-.3 0-.5-.2-.5-.4l-.7-5.4v-.4l5-6.6c0-.1.2-.2.4-.2h.2c.3 0 .6.3.5.6l-.8 3.6c-.1.4 0 .9.3 1.3s.7.6 1.2.6h4.4c.3 0 .6.3.5.6Z' ),
824 'smiley' => array( 'd' => 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5 14.67 11 15.5 11zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z' ),
825 'coffee' => array(
826 'd' => 'M6.5 5h11v3.25h1a2 2 0 010 4H17.5v1.25a3 3 0 01-3 3H9.5a3 3 0 01-3-3V5zm1.5 1.5v7.25a1.5 1.5 0 001.5 1.5h5a1.5 1.5 0 001.5-1.5V6.5h-8zm10 2.5H17.5v2h.5a1 1 0 000-2zM5 20h14V18.5H5z',
827 'fill-rule' => 'evenodd',
828 ),
829 'tip-jar' => array( 'd' => 'M9 3h6c0-1.1-.9-2-2-2H11C9.9 1 9 1.9 9 3zm7 0H8c-.55 0-1 .45-1 1v1.5c0 .55.45 1 1 1h8c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zm-1 3.5H9V19c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2V6.5zm-3 1.5h2v1.5l-1 1.5-1-1.5V8z' ),
830 'hand-heart' => array( 'd' => 'M15.5 2.1c-1.1 0-2 .6-2.5 1.4-.5-.9-1.4-1.4-2.5-1.4C8.8 2.1 7.5 3.4 7.5 5c0 2.5 4.5 5.9 5.5 6.6 1-.7 5.5-4.1 5.5-6.6 0-1.6-1.3-2.9-3-2.9zM9 14H7l-2 7h14l-2-7h-2l-1 3H10l-1-3z' ),
831 'people' => array(
832 'd' => 'M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z',
833 'fill-rule' => 'evenodd',
834 ),
835 );
836
837 if ( ! isset( $paths[ $icon_key ] ) || 'none' === $icon_key ) {
838 return '';
839 }
840
841 $icon = $paths[ $icon_key ];
842 $extra_attrs = isset( $icon['fill-rule'] ) ? ' fill-rule="' . esc_attr( $icon['fill-rule'] ) . '"' : '';
843
844 return sprintf(
845 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false" class="donations__trigger-icon"><path d="%s"%s/></svg>',
846 esc_attr( $icon['d'] ),
847 $extra_attrs
848 );
849 }
850
851 /**
852 * Make default texts available to the editor.
853 */
854 function load_editor_scripts() {
855 // Only relevant to the editor right now.
856 if ( ! is_admin() ) {
857 return;
858 }
859
860 $data = array(
861 'defaultTexts' => get_default_texts(),
862 );
863
864 wp_add_inline_script(
865 'jetpack-blocks-editor',
866 'var Jetpack_DonationsBlock = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
867 'before'
868 );
869 }
870 add_action( 'enqueue_block_assets', __NAMESPACE__ . '\load_editor_scripts', 11 );
871
872 /**
873 * Determine if AMP should be disabled on posts having Donations blocks.
874 *
875 * @param bool $skip Skipped.
876 * @param int $post_id Post ID.
877 * @param WP_Post $post Post.
878 *
879 * @return bool Whether to skip the post from AMP.
880 */
881 function amp_skip_post( $skip, $post_id, $post ) {
882 // When AMP is on standard mode, there are no non-AMP posts to link to where the donation can be completed, so let's
883 // prevent the post from being available in AMP.
884 if ( function_exists( 'amp_is_canonical' ) && \amp_is_canonical() && has_block( Blocks::get_block_name( __DIR__ ), $post->post_content ) ) {
885 return true;
886 }
887 return $skip;
888 }
889 add_filter( 'amp_skip_post', __NAMESPACE__ . '\amp_skip_post', 10, 3 );
890
891 /**
892 * Record a Tracks event when a post containing the Donations block transitions
893 * to "publish" status. Fires once per donations block in the post, with the
894 * block's configuration snapshot as event properties.
895 *
896 * Admin-driven event only (no donor data). Does not fire on regular updates of
897 * an already-published post, on autosaves, or on revisions.
898 *
899 * @since 15.9
900 *
901 * @param string $new_status New post status.
902 * @param string $old_status Previous post status.
903 * @param WP_Post $post Post being transitioned.
904 * @return void
905 */
906 function record_block_published_event( $new_status, $old_status, $post ) {
907 if ( 'publish' !== $new_status || 'publish' === $old_status ) {
908 return;
909 }
910 if ( ! $post instanceof WP_Post ) {
911 return;
912 }
913 if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
914 return;
915 }
916 $block_name = Blocks::get_block_name( __DIR__ );
917 if ( ! has_block( $block_name, $post ) ) {
918 return;
919 }
920
921 $donation_blocks = collect_donation_blocks( parse_blocks( $post->post_content ), $block_name );
922 if ( empty( $donation_blocks ) ) {
923 return;
924 }
925
926 require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
927 $stripe_connected = \Jetpack_Memberships::has_connected_account();
928 $block_count = count( $donation_blocks );
929
930 foreach ( $donation_blocks as $index => $block ) {
931 $props = build_block_published_event_props( $block, $post, $index, $block_count, $stripe_connected );
932 record_published_event( 'donations_block_published', $props );
933 }
934 }
935 add_action( 'transition_post_status', __NAMESPACE__ . '\record_block_published_event', 10, 3 );
936
937 /**
938 * Walk a parsed block tree and return a flat list of Donations blocks (including
939 * nested ones inside columns / groups / patterns).
940 *
941 * @since 15.9
942 *
943 * @param array $blocks Parsed blocks from parse_blocks().
944 * @param string $block_name Donations block name (e.g. 'jetpack/donations').
945 * @return array List of parsed blocks matching $block_name.
946 */
947 function collect_donation_blocks( $blocks, $block_name ) {
948 $matches = array();
949 foreach ( $blocks as $block ) {
950 if ( isset( $block['blockName'] ) && $block_name === $block['blockName'] ) {
951 $matches[] = $block;
952 }
953 if ( ! empty( $block['innerBlocks'] ) ) {
954 $matches = array_merge( $matches, collect_donation_blocks( $block['innerBlocks'], $block_name ) );
955 }
956 }
957 return $matches;
958 }
959
960 /**
961 * Build the property bag for a jetpack_donations_block_published Tracks event.
962 *
963 * Extracted so it can be tested independently of the WordPress hook plumbing.
964 *
965 * @since 15.9
966 *
967 * @param array $block Parsed block (with 'attrs' key).
968 * @param WP_Post $post Post being published.
969 * @param int $index 0-based position of this block within the post's donation blocks.
970 * @param int $block_count Total count of donations blocks in the post.
971 * @param bool $stripe_connected Whether the site currently has a connected Stripe account.
972 * @return array Tracks event properties.
973 */
974 function build_block_published_event_props( $block, $post, $index, $block_count, $stripe_connected ) {
975 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
976
977 $one_time_show = ! isset( $attrs['oneTimeDonation']['show'] ) || false !== $attrs['oneTimeDonation']['show'];
978 $monthly_show = ! empty( $attrs['monthlyDonation']['show'] );
979 $yearly_show = ! empty( $attrs['annualDonation']['show'] );
980
981 $default_interval = $attrs['defaultInterval'] ?? 'one-time';
982 $interval_to_freq = array(
983 'one-time' => 'one_time',
984 '1 month' => 'monthly',
985 '1 year' => 'yearly',
986 );
987 $default_frequency = $interval_to_freq[ $default_interval ] ?? $default_interval;
988 $enabled_count = (int) $one_time_show + (int) $monthly_show + (int) $yearly_show;
989 $tabs_appearance = $attrs['tabsAppearance'] ?? 'tabs';
990
991 return array(
992 'feature' => 'donations',
993 'surface' => 'server',
994 'stripe_connected' => (bool) $stripe_connected,
995 'post_id' => (int) $post->ID,
996 'post_type' => (string) $post->post_type,
997 'block_index_in_post' => (int) $index,
998 'block_count_in_post' => (int) $block_count,
999 'currency' => isset( $attrs['currency'] ) ? (string) $attrs['currency'] : null,
1000 'default_frequency' => $default_frequency,
1001 'enabled_frequencies_count' => $enabled_count,
1002 'show_one_time' => $one_time_show,
1003 'show_monthly' => $monthly_show,
1004 'show_yearly' => $yearly_show,
1005 'show_custom_amount' => ! empty( $attrs['showCustomAmount'] ),
1006 'has_min_amount' => isset( $attrs['minimumAmount'] ),
1007 'has_max_amount' => isset( $attrs['maximumAmount'] ),
1008 'has_custom_styles' => has_custom_styles( $attrs ),
1009 'tabs_appearance' => (string) $tabs_appearance,
1010 );
1011 }
1012
1013 /**
1014 * Detect whether the block carries any of the customizable style attributes
1015 * we ship for the RSM project (so we can measure adoption of the new options).
1016 *
1017 * @since 15.9
1018 *
1019 * @param array $attrs Block attributes.
1020 * @return bool Whether any style override is set.
1021 */
1022 function has_custom_styles( $attrs ) {
1023 $style_keys = array(
1024 'tabFontSize',
1025 'tabPadding',
1026 'activeTabBackgroundColor',
1027 'activeTabTextColor',
1028 'buttonPadding',
1029 );
1030 foreach ( $style_keys as $key ) {
1031 if ( isset( $attrs[ $key ] ) && '' !== $attrs[ $key ] && array() !== $attrs[ $key ] ) {
1032 return true;
1033 }
1034 }
1035 return false;
1036 }
1037
1038 /**
1039 * Send a Donations Tracks event using whichever pipeline is available for the
1040 * current environment (WPCOM Simple sites vs. WoA / connected Jetpack sites).
1041 *
1042 * Mirrors the dual-path pattern used by the Map block. No-op on environments
1043 * that have neither path available, so this stays safe on unconnected sites.
1044 *
1045 * @since 15.9
1046 *
1047 * @param string $event_name Event name WITHOUT the `jetpack_` prefix (the Tracking class adds it).
1048 * @param array $props Tracks event properties.
1049 * @return void
1050 */
1051 function record_published_event( $event_name, $props ) {
1052 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1053 require_lib( 'tracks/client' );
1054 tracks_record_event( wp_get_current_user(), 'jetpack_' . $event_name, $props );
1055 return;
1056 }
1057 if ( ( new Host() )->is_woa_site() && Jetpack::is_connection_ready() ) {
1058 $tracking = new Tracking();
1059 $tracking->record_user_event( $event_name, $props );
1060 }
1061 }
1062