Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.00% covered (danger)
40.00%
42 / 105
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Placement_Section
40.00% covered (danger)
40.00%
42 / 105
40.00% covered (danger)
40.00%
4 / 10
137.26
0.00% covered (danger)
0.00%
0 / 1
 render_summary
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 choices
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 heading
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 selected_post_types
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_saved
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 normalize_show
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 default_post_types
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 label_for
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
1<?php
2/**
3 * The shared placement section of Settings > Sharing.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes\Settings;
11
12/**
13 * Renders "Show buttons on", which governs where sharing buttons, Like buttons,
14 * and Jetpack's Comment Likes appear. Its own section because it belongs to no feature alone.
15 */
16final class Placement_Section {
17
18    /**
19     * Anchor the feature sections link to.
20     */
21    public const ANCHOR = 'jetpack-sharing-placement';
22
23    /**
24     * Identifies the Sharing buttons section to `render_summary()`.
25     */
26    public const FEATURE_SHARING = 'sharing';
27
28    /**
29     * Identifies the Like buttons section to `render_summary()`.
30     */
31    public const FEATURE_LIKES = 'likes';
32
33    /**
34     * Identifies the Comment Likes section to `render_summary()`.
35     */
36    public const FEATURE_COMMENT_LIKES = 'comment-likes';
37
38    /**
39     * Where a feature's buttons currently appear, stated inside that feature's
40     * own section.
41     *
42     * This setting governs several features but lives in none, so each section
43     * says what it means for that feature and links here to change it. It also
44     * surfaces a placement that hides the buttons entirely, which is otherwise
45     * only visible on this section further down the page.
46     *
47     * Each feature gets a complete sentence: interpolating the feature name into
48     * a shared one does not translate.
49     *
50     * @param string $feature One of the FEATURE_* constants.
51     */
52    public static function render_summary( string $feature ): void {
53        $labels = array_map( array( __CLASS__, 'label_for' ), self::selected_post_types() );
54
55        if ( $labels === array() ) {
56            $nowhere = array(
57                self::FEATURE_SHARING       => __( 'Sharing buttons are currently not shown anywhere.', 'jetpack-sharing-likes' ),
58                self::FEATURE_LIKES         => __( 'Like buttons are currently not shown anywhere.', 'jetpack-sharing-likes' ),
59                self::FEATURE_COMMENT_LIKES => __( 'Comment Likes are currently not shown anywhere.', 'jetpack-sharing-likes' ),
60            );
61            $summary = $nowhere[ $feature ] ?? $nowhere[ self::FEATURE_SHARING ];
62        } else {
63            $somewhere = array(
64                /* translators: %s: comma-separated list of places, for example "Posts, Pages". */
65                self::FEATURE_SHARING       => __( 'Sharing buttons currently appear on: %s.', 'jetpack-sharing-likes' ),
66                /* translators: %s: comma-separated list of places, for example "Posts, Pages". */
67                self::FEATURE_LIKES         => __( 'Like buttons currently appear on: %s.', 'jetpack-sharing-likes' ),
68                /* translators: %s: comma-separated list of places, for example "Posts, Pages". */
69                self::FEATURE_COMMENT_LIKES => __( 'Comment Likes currently appear on comments on: %s.', 'jetpack-sharing-likes' ),
70            );
71            $summary   = sprintf( $somewhere[ $feature ] ?? $somewhere[ self::FEATURE_SHARING ], implode( ', ', $labels ) );
72        }
73
74        printf(
75            '<p class="description">%1$s <a href="#%2$s">%3$s</a></p>',
76            esc_html( $summary ),
77            esc_attr( self::ANCHOR ),
78            esc_html__( 'Change where they appear', 'jetpack-sharing-likes' )
79        );
80    }
81
82    /**
83     * Every place the buttons can appear: `index` for the front page, archives and search, then public post types.
84     *
85     * @return string[]
86     */
87    public static function choices(): array {
88        $choices = array_values( get_post_types( array( 'public' => true ) ) );
89        array_unshift( $choices, 'index' );
90
91        return $choices;
92    }
93
94    /**
95     * Render the section.
96     */
97    public static function render(): void {
98        $shown = self::selected_post_types();
99
100        $choices = self::choices();
101        ?>
102        <div class="jetpack-sharing-settings__section" id="<?php echo esc_attr( self::ANCHOR ); ?>">
103            <h2><?php echo esc_html( self::heading() ); ?></h2>
104            <?php ob_start(); ?>
105                <table class="form-table">
106                    <tbody>
107                    <?php
108                    /**
109                     * Filters the HTML at the beginning of the "Show button on" row.
110                     *
111                     * @module sharedaddy
112                     *
113                     * @since jetpack-2.1.0
114                     *
115                     * @param string $var Opening HTML tag at the beginning of the "Show button on" row.
116                     */
117                    echo apply_filters( 'sharing_show_buttons_on_row_start', '<tr valign="top">' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
118                    ?>
119                        <th scope="row"></th>
120                        <td>
121                            <fieldset>
122                                <legend class="screen-reader-text"><span><?php echo esc_html( self::heading() ); ?></span></legend>
123                                <?php foreach ( $choices as $choice ) : ?>
124                                    <label>
125                                        <input type="checkbox" name="show[]" value="<?php echo esc_attr( $choice ); ?>" <?php checked( in_array( $choice, $shown, true ) ); ?> />
126                                        <?php echo esc_html( self::label_for( $choice ) ); ?>
127                                    </label><br />
128                                <?php endforeach; ?>
129                            </fieldset>
130                        </td>
131                    <?php
132                    /**
133                     * Filters the HTML at the end of the "Show button on" row.
134                     *
135                     * @module sharedaddy
136                     *
137                     * @since jetpack-2.1.0
138                     *
139                     * @param string $var Closing HTML tag at the end of the "Show button on" row.
140                     */
141                    echo apply_filters( 'sharing_show_buttons_on_row_end', '</tr>' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
142                    ?>
143                    </tbody>
144                </table>
145            <?php Settings_Form::render_fields( Settings_Form::SECTION_PLACEMENT, (string) ob_get_clean() ); ?>
146        </div>
147        <?php
148    }
149
150    /**
151     * Heading naming the features this section governs.
152     *
153     * "Show buttons on" sat directly below the Like buttons section and read as
154     * though it belonged to it. Naming the features makes the shared scope
155     * legible without a second line of copy.
156     */
157    private static function heading(): string {
158        $key = implode(
159            '+',
160            array_keys(
161                array_filter(
162                    array(
163                        'sharing'  => Section_State::configures( Sharing_Section::state() ),
164                        'likes'    => Section_State::configures( Likes_Section::state() ),
165                        'comments' => Environment::comment_likes_follow_likes_settings(),
166                    )
167                )
168            )
169        );
170
171        $headings = array(
172            'sharing+likes+comments' => __( 'Where sharing buttons, Like buttons, and Comment Likes appear', 'jetpack-sharing-likes' ),
173            'sharing+likes'          => __( 'Where sharing and Like buttons appear', 'jetpack-sharing-likes' ),
174            'sharing+comments'       => __( 'Where sharing buttons and Comment Likes appear', 'jetpack-sharing-likes' ),
175            'likes+comments'         => __( 'Where Like buttons and Comment Likes appear', 'jetpack-sharing-likes' ),
176            'likes'                  => __( 'Where Like buttons appear', 'jetpack-sharing-likes' ),
177            'comments'               => __( 'Where Comment Likes appear', 'jetpack-sharing-likes' ),
178        );
179
180        return $headings[ $key ] ?? __( 'Where sharing buttons appear', 'jetpack-sharing-likes' );
181    }
182
183    /**
184     * Post types currently set to show buttons.
185     *
186     * Falls back to the same defaults the features themselves apply when the
187     * option has never been saved. Reading it raw would render every checkbox
188     * unchecked on a site where the buttons are in fact live, and the next save
189     * would then write that back and turn them off.
190     *
191     * @return string[]
192     */
193    public static function selected_post_types(): array {
194        if ( ! self::is_saved() ) {
195            return self::default_post_types();
196        }
197
198        return self::normalize_show( get_option( 'sharing-options' )['global']['show'] );
199    }
200
201    /**
202     * Whether a placement is stored, rather than left to each feature's default.
203     */
204    public static function is_saved(): bool {
205        $sharing = get_option( 'sharing-options', array() );
206
207        return is_array( $sharing ) && isset( $sharing['global']['show'] );
208    }
209
210    /**
211     * Store where the buttons appear, replacing whatever was stored.
212     *
213     * An empty list means "nowhere". Anything that is not a public post type or
214     * `index` is dropped, so a crafted payload cannot widen where buttons render.
215     *
216     * @param array $post_types Post type slugs, plus `index` for the archive pages.
217     */
218    public static function update( array $post_types ): void {
219        $options = get_option( 'sharing-options' );
220        if ( ! is_array( $options ) ) {
221            $options = array();
222        }
223
224        // Sites carry a malformed `global` (see #6121), and writing into it in place
225        // would fatal where the services save, which rebuilds it wholesale, does not.
226        if ( ! isset( $options['global'] ) || ! is_array( $options['global'] ) ) {
227            $options['global'] = array();
228        }
229
230        $allowed   = array_values( get_post_types( array( 'public' => true ) ) );
231        $allowed[] = 'index';
232
233        $options['global']['show'] = array_values( array_intersect( array_filter( $post_types, 'is_scalar' ), $allowed ) );
234
235        update_option( 'sharing-options', $options );
236    }
237
238    /**
239     * A stored `show` value as a list of post types.
240     *
241     * Older sites stored a single keyword rather than a list, and both
242     * `Sharing_Service::get_global_options()` and `Jetpack_Likes_Settings::get_options()`
243     * still map it, so it is live data rather than a historical curiosity.
244     *
245     * @param mixed $shown Stored `sharing-options['global']['show']` value.
246     * @return string[]
247     */
248    public static function normalize_show( $shown ): array {
249        if ( is_scalar( $shown ) ) {
250            $legacy = array(
251                'posts'       => array( 'post', 'page' ),
252                'index'       => array( 'index' ),
253                'posts-index' => array( 'post', 'page', 'index' ),
254            );
255            $shown  = $legacy[ $shown ] ?? array();
256        }
257
258        return array_values( array_filter( (array) $shown, 'is_string' ) );
259    }
260
261    /**
262     * Where buttons appear on a site that has never saved this section.
263     *
264     * The two features apply different defaults:
265     * - sharing: posts and pages;
266     * - Likes: those, plus public post types that support comments.
267     *
268     * Reporting the narrower set would understate where Like buttons are, so
269     * defer to Likes whenever it is the feature running.
270     *
271     * @return string[]
272     */
273    private static function default_post_types(): array {
274        $defaults = array( 'post', 'page' );
275
276        if ( ! Environment::likes_settings_in_use() ) {
277            return $defaults;
278        }
279
280        if ( ! class_exists( 'Jetpack_Likes_Settings' ) ) {
281            return $defaults;
282        }
283
284        $options = ( new \Jetpack_Likes_Settings() )->get_options();
285
286        return isset( $options['show'] ) ? self::normalize_show( $options['show'] ) : $defaults;
287    }
288
289    /**
290     * Human-readable label for a post type choice.
291     *
292     * @param string $choice Post type slug, or 'index' for the archive pages.
293     */
294    public static function label_for( string $choice ): string {
295        if ( 'index' === $choice ) {
296            return __( 'Front Page, Archive Pages, and Search Results', 'jetpack-sharing-likes' );
297        }
298
299        $post_type = get_post_type_object( $choice );
300
301        return $post_type instanceof \WP_Post_Type ? $post_type->labels->name : $choice;
302    }
303}