Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 188
0.00% covered (danger)
0.00%
0 / 6
CRAP
n/a
0 / 0
Automattic\Jetpack\Extensions\Sharing_Button_Block\register_block
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Sharing_Button_Block\render_block
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
132
Automattic\Jetpack\Extensions\Sharing_Button_Block\get_services
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Sharing_Button_Block\sharing_process_requests
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
Automattic\Jetpack\Extensions\Sharing_Button_Block\add_block_to_single_posts_template
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
240
Automattic\Jetpack\Extensions\Sharing_Button_Block\add_default_services_to_block
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Sharing Buttons Block.
4 *
5 * @since 13.1
6 *
7 * @package automattic/jetpack
8 */
9
10namespace Automattic\Jetpack\Extensions\Sharing_Button_Block;
11
12use Automattic\Jetpack\Blocks;
13use Automattic\Jetpack\Modules;
14use Automattic\Jetpack\Status\Host;
15use Jetpack_Gutenberg;
16use WP_Block_Template;
17
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22require_once __DIR__ . '/class-sharing-source-block.php';
23require_once __DIR__ . '/components/social-icons.php';
24
25const PARENT_BLOCK_NAME = 'jetpack/sharing-buttons';
26const INNER_BLOCK_NAME  = 'jetpack/sharing-button';
27
28/**
29 * Registers the block for use in Gutenberg
30 * This is done via an action so that we can disable
31 * registration if we need to.
32 */
33function register_block() {
34    Blocks::jetpack_register_block(
35        __DIR__,
36        array( 'render_callback' => __NAMESPACE__ . '\render_block' )
37    );
38
39    /*
40     * Automatically add the sharing block to the end of single posts.
41     */
42    add_filter( 'hooked_block_types', __NAMESPACE__ . '\add_block_to_single_posts_template', 10, 4 );
43    add_filter( 'hooked_block_' . PARENT_BLOCK_NAME, __NAMESPACE__ . '\add_default_services_to_block', 10, 5 );
44}
45add_action( 'init', __NAMESPACE__ . '\register_block' );
46
47/**
48 * Sharing Buttons block registration/dependency declaration.
49 *
50 * @param array  $attr    Array containing the Sharing Buttons block attributes.
51 * @param string $content String containing the Sharing Buttons block content.
52 * @param object $block   Object containing block data.
53 *
54 * @return string
55 */
56function render_block( $attr, $content, $block ) {
57    $services = get_services();
58    if ( ! isset( $attr['service'] ) || ! array_key_exists( $attr['service'], $services ) ) {
59        return $content;
60    }
61
62    $service_name = $attr['service'];
63    $title        = $attr['label'] ?? $service_name;
64    $icon         = get_social_logo( $service_name );
65    $style_type   = $block->context['styleType'] ?? 'icon-text';
66    $post_id      = $block->context['postId'] ?? 0;
67    $data_shared  = sprintf(
68        'sharing-%1$s-%2$d',
69        $service_name,
70        $post_id
71    );
72
73    $service      = new $services[ $service_name ]( $service_name, array() );
74    $link_props   = $service->get_link(
75        $post_id,
76        'share=' . esc_attr( $service_name ) . '&nb=1',
77        esc_attr( $data_shared )
78    );
79    $link_url     = $link_props['url'];
80    $link_classes = sprintf(
81        'jetpack-sharing-button__button style-%1$s share-%2$s',
82        $style_type,
83        $service_name
84    );
85
86    $accessible_name = sprintf(
87        /* translators: %s refers to a string representation of sharing service, e.g. Facebook */
88        esc_html__( 'Share on %s', 'jetpack' ),
89        esc_html( $title )
90    );
91
92    $accessible_name .= __( ' (Opens in new window)', 'jetpack' );
93
94    $block_class_name = 'jetpack-sharing-button__list-item';
95
96    if ( $service_name === 'share' ) {
97        $block_class_name .= ' tooltip';
98        /* translators: aria label for SMS sharing button */
99        $accessible_name = esc_attr__( 'Share using Native tools', 'jetpack' );
100        $link_props      = $service->get_link( $post_id );
101    }
102
103    $link_url     = $link_props['url'];
104    $link_classes = sprintf(
105        'jetpack-sharing-button__button style-%1$s share-%2$s',
106        $style_type,
107        $service_name
108    );
109
110    $styles = array();
111    if (
112        array_key_exists( 'iconColorValue', $block->context )
113        && ! empty( $block->context['iconColorValue'] )
114    ) {
115        $styles['color'] = $block->context['iconColorValue'];
116    }
117    if (
118        array_key_exists( 'iconBackgroundColorValue', $block->context )
119        && ! empty( $block->context['iconBackgroundColorValue'] )
120    ) {
121        $styles['background-color'] = $block->context['iconBackgroundColorValue'];
122    }
123    $link_styles = '';
124    foreach ( $styles as $property => $value ) {
125        $link_styles .= $property . ':' . $value . ';';
126    }
127
128    Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
129
130    $component  = '<li class="' . esc_attr( $block_class_name ) . '">';
131    $component .= sprintf(
132        '<a href="%1$s" target="_blank" rel="nofollow noopener noreferrer" class="%2$s" style="%3$s" data-service="%4$s" data-shared="%5$s" aria-labelledby="%5$s">',
133        esc_url( $link_url ),
134        esc_attr( $link_classes ),
135        esc_attr( $link_styles ),
136        esc_attr( $service_name ),
137        esc_attr( $data_shared )
138    );
139
140    // Add hidden span with accessible name
141    $component .= sprintf(
142        '<span id="%s" hidden>%s</span>',
143        esc_attr( $data_shared ),
144        esc_html( $accessible_name )
145    );
146
147    $component .= $style_type !== 'text' ? $icon : '';
148    $component .= '<span class="jetpack-sharing-button__service-label" aria-hidden="true">' . esc_html( $title ) . '</span>';
149    if ( $service_name === 'share' ) {
150        $component .= '<span class="tooltiptext" aria-live="assertive">' . esc_html__( 'Copied to clipboard', 'jetpack' ) . '</span>';
151    }
152    $component .= '</a>';
153    $component .= '</li>';
154
155    return $component;
156}
157
158/**
159 * Get services for the Sharing Buttons block.
160 *
161 * @return array Array of services.
162 */
163function get_services() {
164    $services = array(
165        'print'     => Share_Print_Block::class,
166        'mail'      => Share_Email_Block::class,
167        'facebook'  => Share_Facebook_Block::class,
168        'linkedin'  => Share_LinkedIn_Block::class,
169        'reddit'    => Share_Reddit_Block::class,
170        'twitter'   => Share_Twitter_Block::class,
171        'tumblr'    => Share_Tumblr_Block::class,
172        'pinterest' => Share_Pinterest_Block::class,
173        'telegram'  => Share_Telegram_Block::class,
174        'threads'   => Share_Threads_Block::class,
175        'whatsapp'  => Jetpack_Share_WhatsApp_Block::class,
176        'mastodon'  => Share_Mastodon_Block::class,
177        'nextdoor'  => Share_Nextdoor_Block::class,
178        'bluesky'   => Share_Bluesky_Block::class,
179        'x'         => Share_X_Block::class,
180        'share'     => Share_Native_Block::class,
181    );
182
183    return $services;
184}
185
186/**
187 * Launch sharing requests on page load when a specific query string is used.
188 *
189 * @return void
190 */
191function sharing_process_requests() {
192    global $post;
193
194    // Only process if: single post and share={service} defined
195    if ( ( is_page() || is_single() ) && isset( $_GET['share'] ) && is_string( $_GET['share'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
196        $services     = get_services();
197        $service_name = sanitize_text_field( wp_unslash( $_GET['share'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
198
199        // Only allow services that have been defined in get_services().
200        if ( ! array_key_exists( $service_name, $services ) ) {
201            return;
202        }
203
204        $service = new $services[ ( $service_name ) ]( $service_name, array() );
205        $service->process_request( $post, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
206    }
207}
208
209// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking for the data being present.
210if ( isset( $_GET['share'] ) ) {
211    add_action( 'template_redirect', __NAMESPACE__ . '\sharing_process_requests', 9 );
212}
213
214/**
215 * Automatically add the Sharing Buttons block to the end of the Single Posts template.
216 *
217 * @since 13.2
218 *
219 * @param array                   $hooked_block_types The list of hooked block types.
220 * @param string                  $relative_position  The relative position of the hooked blocks. Can be one of 'before', 'after', 'first_child', or 'last_child'.
221 * @param string                  $anchor_block_type  The anchor block type.
222 * @param WP_Block_Template|array $context            The block template, template part, or pattern that the anchor block belongs to.
223 *
224 * @return array
225 */
226function add_block_to_single_posts_template( $hooked_block_types, $relative_position, $anchor_block_type, $context ) {
227    // Add the block at the end of the post content.
228    if (
229        'after' !== $relative_position
230        || 'core/post-content' !== $anchor_block_type
231    ) {
232        return $hooked_block_types;
233    }
234
235    // Only automate the addition of the block in block-based themes.
236    if ( ! wp_is_block_theme() ) {
237        return $hooked_block_types;
238    }
239
240    // Proceed if the user has toggled the auto-addition in Jetpack settings.
241    if ( ! get_option( 'jetpack_sharing_buttons_auto_add' ) ) {
242        return $hooked_block_types;
243    }
244
245    /*
246     * The Sharing module must be disabled.
247     * We do not want to automatically insert sharing buttons twice.
248     * On WordPress.com Simple the module is always active so we must check differently.
249     * There, we check if buttons are enabled on single posts and pages.
250     */
251    if ( ( new Host() )->is_wpcom_simple() ) {
252        if ( ! class_exists( 'Sharing_Service' ) ) {
253            include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php';
254        }
255
256        $sharer = new \Sharing_Service();
257        $global = $sharer->get_global_options();
258        if (
259            ! $global['show']
260            || in_array( 'post', $global['show'], true )
261            || in_array( 'page', $global['show'], true )
262        ) {
263            return $hooked_block_types;
264        }
265    } elseif ( ( new Modules() )->is_active( 'sharedaddy' ) ) {
266        return $hooked_block_types;
267    }
268
269    // Only hook into page and single post templates.
270    if (
271        ! $context instanceof WP_Block_Template
272        || ! property_exists( $context, 'slug' )
273        || empty( $context->slug )
274        || ! preg_match( '/^(page|single)/', $context->slug )
275    ) {
276        return $hooked_block_types;
277    }
278
279    $hooked_block_types[] = PARENT_BLOCK_NAME;
280    return $hooked_block_types;
281}
282
283/**
284 * Add default services to the block we add to the post content by default.
285 *
286 * @since 13.2
287 *
288 * @param array                   $parsed_hooked_block The parsed block array for the given hooked block type.
289 * @param string                  $hooked_block_type   The hooked block type name.
290 * @param string                  $relative_position   The relative position of the hooked block.
291 * @param array                   $parsed_anchor_block The anchor block, in parsed block array format.
292 * @param WP_Block_Template|array $context             The block template, template part, or pattern that the anchor block.
293 *
294 * @return array
295 */
296function add_default_services_to_block( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
297    // Is the hooked block adjacent to the anchor block?
298    if ( 'after' !== $relative_position ) {
299        return $parsed_hooked_block;
300    }
301
302    // Use the icon style by default.
303    $parsed_hooked_block['attrs']['styleType'] = 'icon';
304
305    // Add default services (inner blocks) to the block.
306    $parsed_hooked_block['innerBlocks'] = array(
307        array(
308            'blockName'    => INNER_BLOCK_NAME,
309            'innerContent' => array(),
310            'attrs'        => array(
311                'service' => 'facebook',
312                'label'   => esc_html__( 'Facebook', 'jetpack' ),
313            ),
314        ),
315        array(
316            'blockName'    => INNER_BLOCK_NAME,
317            'innerContent' => array(),
318            'attrs'        => array(
319                'service' => 'x',
320                'label'   => esc_html__( 'X', 'jetpack' ),
321            ),
322        ),
323        array(
324            'blockName'    => INNER_BLOCK_NAME,
325            'innerContent' => array(),
326            'attrs'        => array(
327                'service' => 'mastodon',
328                'label'   => esc_html__( 'Mastodon', 'jetpack' ),
329            ),
330        ),
331    );
332
333    // Wrap inner blocks in our sharing buttons markup.
334    $parsed_hooked_block['innerContent'] = array(
335        '<ul class="wp-block-jetpack-sharing-buttons has-normal-icon-size jetpack-sharing-buttons__services-list" id="jetpack-sharing-serivces-list">',
336        null,
337        null,
338        null,
339        '</ul>',
340    );
341
342    // Wrap the whole thing in a group block.
343    return array(
344        'blockName'    => 'core/group',
345        'attrs'        => array(
346            // Does the anchor block have a layout attribute? If so, use it in the group to maintain the same alignment.
347            'layout' => $parsed_anchor_block['attrs']['layout'] ?? 'null',
348        ),
349        'innerBlocks'  => array( $parsed_hooked_block ),
350        'innerContent' => array(
351            '<div class="wp-block-group">',
352            null,
353            '</div>',
354        ),
355    );
356}