PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.0
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
suredonation / inc / blocks / campaign-social-sharing / block.php

block.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.6.0, at inc/blocks/campaign-social-sharing/block.php

264 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * PHP render for the Campaign Social Sharing block.
4 *
5 * @package SureDonation
6 * @since 1.2.0
7 */
8
9 namespace SureDonation\Inc\Blocks\Campaign_Social_Sharing;
10
11 use SureDonation\Inc\Blocks\Base;
12 use SureDonation\Inc\Campaigns\Campaign_Page;
13 use SureDonation\Inc\Helper;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Campaign Social Sharing block.
21 *
22 * @since 1.2.0
23 */
24 class Block extends Base {
25 /**
26 * Whether the Mastodon click handler has already been printed on this page.
27 *
28 * @var bool
29 * @since 1.2.0
30 */
31 private static $mastodon_script_printed = false;
32
33 /**
34 * Render the block.
35 *
36 * @param array<string, mixed> $attributes Block attributes.
37 * @param string $content Block content.
38 * @return string
39 * @since 1.2.0
40 */
41 public function render( $attributes, $content = '' ) {
42 unset( $content );
43
44 $campaign_id = Campaign_Page::resolve_campaign_id( $attributes );
45 if ( ! $campaign_id ) {
46 return '';
47 }
48
49 wp_enqueue_style( 'suredonation-campaign-blocks' );
50
51 $networks = self::get_networks();
52 $enabled = isset( $attributes['networks'] ) && is_array( $attributes['networks'] ) ? $attributes['networks'] : [];
53 $enabled = array_values( array_intersect( array_keys( $networks ), array_map( 'strval', $enabled ) ) );
54 if ( empty( $enabled ) ) {
55 return '';
56 }
57
58 // No block.json default (static JSON isn't localizable) — absent attribute
59 // means "never edited", which falls back to the translated default; an
60 // explicitly cleared headline is stored as '' and renders no headline.
61 $headline = isset( $attributes['headline'] ) ? Helper::get_string_value( $attributes['headline'] ) : __( 'Share:', 'suredonation' );
62 $new_tab = ! isset( $attributes['openInNewTab'] ) || ! empty( $attributes['openInNewTab'] );
63 $align = isset( $attributes['contentAlign'] ) ? Helper::get_string_value( $attributes['contentAlign'] ) : 'left';
64 $align = in_array( $align, [ 'left', 'center', 'right' ], true ) ? $align : 'left';
65 $target_attrs = $new_tab ? ' target="_blank" rel="noopener noreferrer"' : '';
66
67 // Share tokens: the campaign title and its permalink, url-encoded.
68 $text = rawurlencode( wp_strip_all_tags( (string) get_the_title( $campaign_id ) ) );
69 $url = rawurlencode( (string) get_permalink( $campaign_id ) );
70
71 $wrapper = get_block_wrapper_attributes(
72 [
73 'class' => 'suredonation-campaign-social suredonation-campaign-social--align-' . sanitize_html_class( $align ),
74 ]
75 );
76
77 ob_start();
78 ?>
79 <div <?php echo $wrapper; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() returns escaped markup. ?>>
80 <?php if ( '' !== trim( $headline ) ) { ?>
81 <h5 class="suredonation-campaign-social__headline"><?php echo esc_html( $headline ); ?></h5>
82 <?php } ?>
83 <div class="suredonation-campaign-social__list">
84 <?php
85 foreach ( $enabled as $key ) {
86 $network = $networks[ $key ];
87
88 if ( 'mastodon' === $key ) {
89 // Mastodon has no central share endpoint; the instance is asked
90 // for (and remembered) by the click handler below.
91 printf(
92 '<a class="suredonation-campaign-social__link suredonation-campaign-social__link--mastodon" href="#" data-share-text="%1$s" data-share-url="%2$s" data-new-tab="%3$s" aria-label="%4$s">%5$s</a>',
93 esc_attr( $text ),
94 esc_attr( $url ),
95 esc_attr( $new_tab ? '1' : '0' ),
96 esc_attr(
97 sprintf(
98 /* translators: %s: social network name. */
99 __( 'Share on %s', 'suredonation' ),
100 $network['label']
101 )
102 ),
103 self::get_icon( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static, developer-defined inline SVG markup.
104 );
105 continue;
106 }
107
108 $share_url = strtr(
109 $network['share_url'],
110 [
111 '{text}' => $text,
112 '{url}' => $url,
113 ]
114 );
115
116 printf(
117 '<a class="suredonation-campaign-social__link suredonation-campaign-social__link--%1$s" href="%2$s"%3$s aria-label="%4$s">%5$s</a>',
118 esc_attr( $key ),
119 esc_url( $share_url ),
120 $target_attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static, developer-defined attribute string.
121 esc_attr(
122 sprintf(
123 /* translators: %s: social network name. */
124 __( 'Share on %s', 'suredonation' ),
125 $network['label']
126 )
127 ),
128 self::get_icon( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static, developer-defined inline SVG markup.
129 );
130 }
131 ?>
132 </div>
133 <?php self::maybe_print_mastodon_script( $enabled ); ?>
134 </div>
135 <?php
136 $output = ob_get_clean();
137
138 return false !== $output ? $output : '';
139 }
140
141 /**
142 * The supported social networks, in display order.
143 *
144 * Keys, labels and share-intent URL patterns mirror WP Charitable's social
145 * sharing field. `{text}` = url-encoded campaign title, `{url}` = url-encoded
146 * permalink. Mastodon has no central endpoint (handled client-side).
147 *
148 * @since 1.2.0
149 * @return array<string, array{label:string, share_url:string}>
150 */
151 private static function get_networks() {
152 return [
153 'twitter' => [
154 'label' => __( 'Twitter / X', 'suredonation' ),
155 'share_url' => 'https://x.com/intent/post?text={text}&url={url}',
156 ],
157 'facebook' => [
158 'label' => __( 'Facebook', 'suredonation' ),
159 'share_url' => 'https://www.facebook.com/sharer/sharer.php?u={url}',
160 ],
161 'linkedin' => [
162 'label' => __( 'LinkedIn', 'suredonation' ),
163 'share_url' => 'https://www.linkedin.com/sharing/share-offsite/?url={url}',
164 ],
165 'pinterest' => [
166 'label' => __( 'Pinterest', 'suredonation' ),
167 'share_url' => 'https://www.pinterest.com/pin/create/button/?url={url}&description={text}',
168 ],
169 'mastodon' => [
170 'label' => __( 'Mastodon', 'suredonation' ),
171 'share_url' => '',
172 ],
173 // Threads/Bluesky take the title and URL in a single `text` field.
174 // esc_url() strips encoded CR/LF (%0D/%0A), so a space separates them
175 // (Charitable uses a blank line, which esc_url would drop entirely).
176 'threads' => [
177 'label' => __( 'Threads', 'suredonation' ),
178 'share_url' => 'https://www.threads.net/intent/post?text={text}%20{url}',
179 ],
180 'bluesky' => [
181 'label' => __( 'Bluesky', 'suredonation' ),
182 'share_url' => 'https://bsky.app/intent/compose?text={text}%20{url}',
183 ],
184 ];
185 }
186
187 /**
188 * Inline brand-colored SVG for a network key.
189 *
190 * Each icon carries its own brand color(s) baked into the SVG (no CSS tinting),
191 * so it renders identically in the editor and on the front end.
192 *
193 * @since 1.2.0
194 * @param string $key Network key.
195 * @return string SVG markup (static, developer-defined).
196 */
197 private static function get_icon( $key ) {
198 $icons = [
199 'twitter' => '<path fill="#000000" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>',
200 'facebook' => '<path fill="#1877F2" d="M12 0C5.373 0 0 5.373 0 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078V12h3.047V9.356c0-3.007 1.792-4.669 4.533-4.669 1.313 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874V12h3.328l-.532 3.469h-2.796v8.385C19.612 22.954 24 17.99 24 12 24 5.373 18.627 0 12 0z"/><path fill="#ffffff" d="M16.671 15.469 17.203 12h-3.328V9.75c0-.949.465-1.874 1.956-1.874h1.513V4.923s-1.373-.235-2.686-.235c-2.741 0-4.533 1.662-4.533 4.669V12H7.078v3.469h3.047v8.385a12.06 12.06 0 0 0 3.75 0v-8.385h2.796z"/>',
201 'linkedin' => '<path fill="#0A66C2" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>',
202 'pinterest' => '<path fill="#BD081C" d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.402.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.357-.629-2.749-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12.017 24c6.624 0 11.99-5.367 11.99-11.988C24.007 5.367 18.641.001 12.017.001z"/>',
203 'mastodon' => '<path fill="#6364FF" d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/>',
204 'threads' => '<path fill="#000000" d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z"/>',
205 'bluesky' => '<path fill="#0285FF" d="M5.203 3.036C7.955 5.132 10.916 9.38 12 11.66c1.084-2.28 4.045-6.528 6.797-8.624C20.783 1.524 24 .363 24 4.03c0 .733-.42 6.156-.667 7.037-.856 3.06-3.978 3.84-6.755 3.368 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.153-.448-.153-.326 0-.122-.049.021-.153.326-.567 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.299-2.777.472-5.899-.308-6.755-3.368C.42 10.185 0 4.763 0 4.03 0 .363 3.217 1.524 5.203 3.036z"/>',
206 ];
207
208 if ( ! isset( $icons[ $key ] ) ) {
209 return '';
210 }
211
212 return '<svg class="suredonation-campaign-social__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">' . $icons[ $key ] . '</svg>';
213 }
214
215 /**
216 * Print the Mastodon click handler once per page (only when Mastodon is used).
217 *
218 * Mirrors WP Charitable: on click, read the saved instance from localStorage or
219 * prompt for it, remember it, then open `https://{instance}/share?text=…\n{url}`.
220 *
221 * @since 1.2.0
222 * @param array<int, string> $enabled Enabled network keys.
223 * @return void
224 */
225 private static function maybe_print_mastodon_script( $enabled ) {
226 if ( self::$mastodon_script_printed || ! in_array( 'mastodon', $enabled, true ) ) {
227 return;
228 }
229 self::$mastodon_script_printed = true;
230
231 // Nowdoc keeps the JS literal (no PHP interpolation of the regexes/'\n');
232 // the translatable prompt is injected via a placeholder instead.
233 $js = <<<'JS'
234 document.addEventListener('click',function(e){
235 var link=e.target.closest('.suredonation-campaign-social__link--mastodon');
236 if(!link){return;}
237 e.preventDefault();
238 var key='suredonation-mastodon-instance';
239 var instance=window.localStorage.getItem(key);
240 if(!instance){
241 instance=window.prompt('%PROMPT%');
242 if(!instance){return;}
243 instance=instance.replace(/^https?:\/\//,'').replace(/\/+$/,'');
244 window.localStorage.setItem(key,instance);
245 }
246 var text=decodeURIComponent(link.getAttribute('data-share-text')||'');
247 var url=decodeURIComponent(link.getAttribute('data-share-url')||'');
248 var share='https://'+instance+'/share?text='+encodeURIComponent(text+'\n'+url);
249 window.open(share, link.getAttribute('data-new-tab')==='1'?'_blank':'_self','noopener');
250 });
251 JS;
252
253 $js = str_replace(
254 '%PROMPT%',
255 esc_js( __( 'Enter your Mastodon instance (e.g. mastodon.social):', 'suredonation' ) ),
256 $js
257 );
258
259 // wp_print_inline_script_tag() lets WordPress attach CSP nonces/attributes
260 // a hand-printed <script> tag would miss (same as inc/assets/register.php).
261 wp_print_inline_script_tag( $js, [ 'id' => 'suredonation-mastodon-share' ] );
262 }
263 }
264