Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.14% covered (success)
97.14%
68 / 70
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sharing_Buttons_Markup
97.14% covered (success)
97.14%
68 / 70
66.67% covered (warning)
66.67%
4 / 6
27
0.00% covered (danger)
0.00%
0 / 1
 fill
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
5
 services
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
7
 remember_legacy_services
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 block_services
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 legacy_button_style
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 labels
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Builds the Sharing Buttons block that gets hooked into templates.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes\Hooked_Blocks;
11
12use Automattic\Jetpack\Sharing_Likes\Block_Names;
13
14/**
15 * A `jetpack/sharing-buttons` block with its buttons, carried over from the
16 * site's legacy sharing services where the block offers them.
17 *
18 * @since 0.4.0
19 */
20final class Sharing_Buttons_Markup {
21
22    /**
23     * The parent block's markup around its buttons, as `sharing-buttons/save.jsx` saves it.
24     * The misspelt id is that save output; changing it would invalidate every saved block.
25     */
26    private const LIST_OPEN  = '<ul class="wp-block-jetpack-sharing-buttons has-normal-icon-size jetpack-sharing-buttons__services-list" id="jetpack-sharing-serivces-list">';
27    private const LIST_CLOSE = '</ul>';
28
29    /**
30     * The legacy services Simple had when it removed them all to switch to the block.
31     */
32    public const SAVED_SERVICES_OPTION = 'jetpack_sharing_buttons_legacy_services';
33
34    /**
35     * Buttons when the site has no legacy service the block offers.
36     */
37    private const DEFAULT_SERVICES = array( 'facebook', 'x', 'mastodon' );
38
39    /**
40     * Legacy service IDs the block knows under another name.
41     */
42    private const LEGACY_ALIASES = array(
43        'email'            => 'mail',
44        'jetpack-whatsapp' => 'whatsapp',
45        'twitter'          => 'x',
46    );
47
48    /**
49     * Legacy button styles the block styles too. It accepts `official` but has no styles for it.
50     */
51    private const STYLE_TYPES = array( 'icon-text', 'icon', 'text' );
52
53    /**
54     * Fill a hooked `jetpack/sharing-buttons` block with its buttons and markup.
55     *
56     * @param array $block The hooked block, in parsed block format.
57     * @return array
58     */
59    public static function fill( array $block ): array {
60        $labels   = self::labels();
61        $children = array();
62
63        foreach ( self::services() as $service ) {
64            $children[] = array(
65                'blockName'    => Block_Names::SHARING_BUTTON,
66                'attrs'        => array(
67                    'service' => $service,
68                    'label'   => $labels[ $service ],
69                ),
70                'innerBlocks'  => array(),
71                'innerHTML'    => '',
72                'innerContent' => array(),
73            );
74        }
75
76        $block['attrs'] = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
77
78        $style = self::legacy_button_style();
79        if ( '' !== $style ) {
80            $block['attrs']['styleType'] = $style;
81        }
82
83        $block['innerBlocks']  = $children;
84        $block['innerHTML']    = self::LIST_OPEN . self::LIST_CLOSE;
85        $block['innerContent'] = array_merge(
86            array( self::LIST_OPEN ),
87            array_fill( 0, count( $children ), null ),
88            array( self::LIST_CLOSE )
89        );
90
91        return $block;
92    }
93
94    /**
95     * The block's services for the legacy buttons the site shows, or showed before Simple removed
96     * them all; those behind "More" when none are visible; or the defaults.
97     *
98     * Reads the option rather than `Sharing_Service`, which may not be loaded, and saves defaults on read.
99     *
100     * @return string[]
101     */
102    public static function services(): array {
103        foreach ( array( get_option( 'sharing-services' ), get_option( self::SAVED_SERVICES_OPTION ) ) as $stored ) {
104            foreach ( array( 'visible', 'hidden' ) as $list ) {
105                $services = is_array( $stored ) && isset( $stored[ $list ] ) && is_array( $stored[ $list ] ) ? self::block_services( $stored[ $list ] ) : array();
106
107                if ( $services ) {
108                    return $services;
109                }
110            }
111        }
112
113        return self::DEFAULT_SERVICES;
114    }
115
116    /**
117     * Keep the legacy services for the block before they are all removed. The one writer for the copy.
118     *
119     * A site that never saved any keeps no copy, so the block gets its own defaults.
120     */
121    public static function remember_legacy_services(): void {
122        $stored = get_option( 'sharing-services' );
123
124        if ( ! is_array( $stored ) || ( empty( $stored['visible'] ) && empty( $stored['hidden'] ) ) ) {
125            return;
126        }
127
128        update_option(
129            self::SAVED_SERVICES_OPTION,
130            array(
131                'visible' => is_array( $stored['visible'] ?? null ) ? $stored['visible'] : array(),
132                'hidden'  => is_array( $stored['hidden'] ?? null ) ? $stored['hidden'] : array(),
133            ),
134            false
135        );
136    }
137
138    /**
139     * The block's services for some legacy service IDs, dropping those it does not offer.
140     *
141     * @param array $legacy_ids Legacy service IDs.
142     * @return string[]
143     */
144    private static function block_services( array $legacy_ids ): array {
145        $labels   = self::labels();
146        $services = array();
147
148        foreach ( $legacy_ids as $legacy_id ) {
149            if ( ! is_string( $legacy_id ) ) {
150                continue;
151            }
152
153            $service = self::LEGACY_ALIASES[ $legacy_id ] ?? $legacy_id;
154
155            if ( isset( $labels[ $service ] ) && ! in_array( $service, $services, true ) ) {
156                $services[] = $service;
157            }
158        }
159
160        return $services;
161    }
162
163    /**
164     * The legacy button style, when the block accepts it.
165     */
166    private static function legacy_button_style(): string {
167        $options = get_option( 'sharing-options' );
168        $style   = is_array( $options ) ? ( $options['global']['button_style'] ?? '' ) : '';
169
170        return in_array( $style, self::STYLE_TYPES, true ) ? $style : '';
171    }
172
173    /**
174     * Button labels for the services carried over, as the block's variations set them.
175     *
176     * The block prints the label, falling back to the raw service slug.
177     *
178     * @return array<string, string>
179     */
180    private static function labels(): array {
181        return array(
182            'bluesky'   => 'Bluesky',
183            // translators: option to print the content - a verb labelling a button.
184            'print'     => __( 'Print', 'jetpack-sharing-likes' ),
185            'facebook'  => 'Facebook',
186            'linkedin'  => 'LinkedIn',
187            // translators: option to share the content by email - a verb labelling a button.
188            'mail'      => __( 'Mail', 'jetpack-sharing-likes' ),
189            'mastodon'  => 'Mastodon',
190            'pinterest' => 'Pinterest',
191            'reddit'    => 'Reddit',
192            'telegram'  => 'Telegram',
193            'threads'   => 'Threads',
194            'tumblr'    => 'Tumblr',
195            'whatsapp'  => 'WhatsApp',
196            'x'         => 'X',
197            'nextdoor'  => 'Nextdoor',
198        );
199    }
200}