Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (warning)
80.00%
64 / 80
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Post_Handler
80.00% covered (warning)
80.00%
64 / 80
60.00% covered (warning)
60.00%
9 / 15
52.80
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_handle
85.19% covered (warning)
85.19%
23 / 27
0.00% covered (danger)
0.00%
0 / 1
11.39
 switch_sharing_to_block
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 switch_likes_to_block
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 save_settings
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 save_global_options
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 save_sharing_options
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 save_likes
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 save_comment_likes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save_placement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 posted_choice
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 redirect_url
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 activate_feature
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 render_action_field
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 render_action_form
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Handles form submissions from 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 * Processes the screen's form submissions.
14 *
15 * Each section posts its own action with its own nonce, so saving one section
16 * never runs another section's handlers.
17 */
18final class Post_Handler {
19
20    /**
21     * Field naming the requested action.
22     */
23    private const ACTION_FIELD = 'jetpack_sharing_action';
24
25    /**
26     * Hook the handler up.
27     */
28    public static function init(): void {
29        add_action( 'admin_init', array( __CLASS__, 'maybe_handle' ) );
30    }
31
32    /**
33     * Dispatch a submission, if this request is one.
34     */
35    public static function maybe_handle(): void {
36        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- identifying the screen; the nonce is verified below.
37        if ( ! isset( $_GET['page'] ) || Settings_Page::SLUG !== $_GET['page'] ) {
38            return;
39        }
40
41        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified per action below.
42        if ( ! isset( $_POST[ self::ACTION_FIELD ] ) ) {
43            return;
44        }
45
46        if ( ! current_user_can( 'manage_options' ) ) {
47            return;
48        }
49
50        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified per action below.
51        $action = sanitize_key( wp_unslash( $_POST[ self::ACTION_FIELD ] ) );
52
53        $redirect = null;
54
55        switch ( $action ) {
56            case 'activate-likes':
57                $redirect = self::activate_feature( Placement_Section::FEATURE_LIKES, Likes_Section::NONCE_ACTION );
58                break;
59            case 'activate-sharing':
60                $redirect = self::activate_feature( Placement_Section::FEATURE_SHARING, Sharing_Section::NONCE_ACTION );
61                break;
62            case 'switch-to-block-likes':
63                $redirect = self::switch_likes_to_block();
64                break;
65            case 'switch-to-block-sharing':
66                $redirect = self::switch_sharing_to_block();
67                break;
68            case 'save-settings':
69                $redirect = self::save_settings();
70                break;
71        }
72
73        if ( null === $redirect ) {
74            return;
75        }
76
77        wp_safe_redirect( $redirect );
78        exit;
79    }
80
81    /**
82     * Hand the Sharing buttons over to the block.
83     *
84     * @return string URL to send the browser back to.
85     */
86    private static function switch_sharing_to_block(): string {
87        check_admin_referer( Sharing_Section::NONCE_ACTION );
88
89        Feature_Actions::switch_to_block( Placement_Section::FEATURE_SHARING );
90
91        return self::redirect_url( true );
92    }
93
94    /**
95     * Hand the Like buttons over to the block.
96     *
97     * @return string URL to send the browser back to.
98     */
99    private static function switch_likes_to_block(): string {
100        check_admin_referer( Likes_Section::NONCE_ACTION );
101
102        Feature_Actions::switch_to_block( Placement_Section::FEATURE_LIKES );
103
104        return self::redirect_url( true );
105    }
106
107    /**
108     * Save every section that put fields on the form.
109     *
110     * Only those: the others' fields were not on the screen, and reading their
111     * absence as "off" would switch them off.
112     *
113     * @return string URL to send the browser back to.
114     */
115    private static function save_settings(): string {
116        check_admin_referer( Settings_Form::NONCE_ACTION );
117
118        $sections           = Settings_Form::posted_sections();
119        $comment_likes_held = true;
120
121        // Before placement, because the services save rebuilds the global options it lives in.
122        if ( in_array( Settings_Form::SECTION_SHARING, $sections, true ) ) {
123            self::save_sharing_options();
124        }
125
126        if ( in_array( Settings_Form::SECTION_PLACEMENT, $sections, true ) ) {
127            self::save_placement();
128        }
129
130        if ( in_array( Settings_Form::SECTION_LIKES, $sections, true ) ) {
131            self::save_likes();
132        }
133
134        if ( in_array( Settings_Form::SECTION_COMMENT_LIKES, $sections, true ) && Environment::likes_supported() ) {
135            $comment_likes_held = self::save_comment_likes();
136        }
137
138        // Once, from whichever section rendered `Services_Config::global_options()`; never both.
139        if ( array_intersect( array( Settings_Form::SECTION_SHARING, Settings_Form::SECTION_EXTRAS ), $sections ) ) {
140            self::save_global_options( $sections );
141        }
142
143        return $comment_likes_held
144            ? self::redirect_url( true )
145            : add_query_arg( Settings_Page::COMMENT_LIKES_UNCHANGED, '1', self::redirect_url( true ) );
146    }
147
148    /**
149     * Save the rows that close the settings table, ours and then third parties'.
150     *
151     * @param string[] $sections Sections the submitted form carried fields for.
152     */
153    private static function save_global_options( array $sections ): void {
154        // Only the services section renders it, and `is_available()` can have turned true
155        // since the form was built, so the claim decides rather than the environment.
156        if ( in_array( Settings_Form::SECTION_SHARING, $sections, true ) ) {
157            Sharing_Resources::save();
158        }
159
160        Twitter_Site_Tag::save();
161
162        /** This action is documented in projects/packages/sharing-likes/src/settings/class-services-config.php */
163        do_action( 'sharing_admin_update' );
164    }
165
166    /**
167     * Save the services list's own settings: button style and label.
168     */
169    private static function save_sharing_options(): void {
170        // The section renders only when this class is loaded, but the request can claim it regardless.
171        if ( ! Sharing_Options::is_available() ) {
172            return;
173        }
174
175        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; set_global_options() validates each field.
176        Sharing_Options::update( $_POST );
177    }
178
179    /**
180     * Save the Like buttons settings.
181     */
182    private static function save_likes(): void {
183        Likes_Options::set_likes_enabled( 'off' !== self::posted_choice( 'wpl_default' ) );
184
185        if ( Environment::is_simple_site() ) {
186            Likes_Options::set_reblogs_enabled( 'off' !== self::posted_choice( 'jetpack_reblogs_enabled' ) );
187        }
188    }
189
190    /**
191     * Save the Comment Likes checkbox.
192     *
193     * @return bool Whether Comment Likes now match the checkbox.
194     */
195    private static function save_comment_likes(): bool {
196        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified by the caller.
197        return Comment_Likes_Section::update( ! empty( $_POST['jetpack_comment_likes_enabled'] ) );
198    }
199
200    /**
201     * Save where the buttons appear.
202     */
203    private static function save_placement(): void {
204        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; update() checks the values against an allowlist.
205        $posted = isset( $_POST['show'] ) && is_array( $_POST['show'] ) ? wp_unslash( $_POST['show'] ) : array();
206
207        Placement_Section::update( $posted );
208    }
209
210    /**
211     * One of a radio group's values, defaulting to "on" when nothing was posted.
212     *
213     * @param string $field Field name.
214     */
215    private static function posted_choice( string $field ): string {
216        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- callers verify before reading.
217        if ( empty( $_POST[ $field ] ) ) {
218            return 'on';
219        }
220
221        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- callers verify before reading.
222        return sanitize_text_field( wp_unslash( $_POST[ $field ] ) );
223    }
224
225    /**
226     * Where to send the browser once a submission is handled.
227     *
228     * @param bool $show_saved_notice Whether the screen should confirm a save.
229     */
230    private static function redirect_url( bool $show_saved_notice ): string {
231        $url = admin_url( 'options-general.php?page=' . Settings_Page::SLUG );
232
233        return $show_saved_notice ? $url . '&update=saved' : $url;
234    }
235
236    /**
237     * Turn a feature back on, then reload the screen.
238     *
239     * @param string $feature      One of the `Placement_Section::FEATURE_*` constants.
240     * @param string $nonce_action Nonce action the submitting section uses.
241     * @return string URL to send the browser back to.
242     */
243    private static function activate_feature( string $feature, string $nonce_action ): string {
244        check_admin_referer( $nonce_action );
245
246        Feature_Actions::activate( $feature );
247
248        return self::redirect_url( false );
249    }
250
251    /**
252     * Hidden field naming the action a form is submitting.
253     *
254     * @param string $action Action name, matching a case above.
255     */
256    public static function render_action_field( string $action ): void {
257        printf(
258            '<input type="hidden" name="%1$s" value="%2$s" />',
259            esc_attr( self::ACTION_FIELD ),
260            esc_attr( $action )
261        );
262    }
263
264    /**
265     * Markup for a single-button form submitting one of the actions above.
266     *
267     * @param string $action       Action name, matching a case above.
268     * @param string $nonce_action Nonce action for the submitting section.
269     * @param string $label        Button label.
270     * @param bool   $primary      Whether this is the only action in its state.
271     */
272    public static function render_action_form( string $action, string $nonce_action, string $label, bool $primary = true ): void {
273        ?>
274        <form method="post" action="">
275            <input type="hidden" name="<?php echo esc_attr( self::ACTION_FIELD ); ?>" value="<?php echo esc_attr( $action ); ?>" />
276            <?php wp_nonce_field( $nonce_action ); ?>
277            <p><button type="submit" class="<?php echo esc_attr( $primary ? 'button button-primary' : 'button' ); ?>"><?php echo esc_html( $label ); ?></button></p>
278        </form>
279        <?php
280    }
281}