PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-sharing-likes / src / settings / class-post-handler.php

class-post-handler.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/jetpack-sharing-likes/src/settings/class-post-handler.php

376 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles form submissions from Settings > Sharing.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8 declare( strict_types = 1 );
9
10 namespace Automattic\Jetpack\Sharing_Likes\Settings;
11
12 use Automattic\Jetpack\Modules;
13
14 /**
15 * Processes the screen's form submissions.
16 *
17 * Each section posts its own action with its own nonce, so saving one section
18 * never runs another section's handlers.
19 */
20 final class Post_Handler {
21
22 /**
23 * Field naming the requested action.
24 */
25 private const ACTION_FIELD = 'jetpack_sharing_action';
26
27 /**
28 * Hook the handler up.
29 */
30 public static function init(): void {
31 add_action( 'admin_init', array( __CLASS__, 'maybe_handle' ) );
32 }
33
34 /**
35 * Dispatch a submission, if this request is one.
36 */
37 public static function maybe_handle(): void {
38 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- identifying the screen; the nonce is verified below.
39 if ( ! isset( $_GET['page'] ) || Settings_Page::SLUG !== $_GET['page'] ) {
40 return;
41 }
42
43 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified per action below.
44 if ( ! isset( $_POST[ self::ACTION_FIELD ] ) ) {
45 return;
46 }
47
48 if ( ! current_user_can( 'manage_options' ) ) {
49 return;
50 }
51
52 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified per action below.
53 $action = sanitize_key( wp_unslash( $_POST[ self::ACTION_FIELD ] ) );
54
55 $redirect = null;
56
57 switch ( $action ) {
58 case 'activate-likes':
59 $redirect = self::activate_module( 'likes', Likes_Section::NONCE_ACTION );
60 break;
61 case 'activate-sharing':
62 $redirect = self::activate_module( 'sharedaddy', Sharing_Section::NONCE_ACTION );
63 break;
64 case 'switch-to-block-likes':
65 $redirect = self::switch_likes_to_block();
66 break;
67 case 'switch-to-block-sharing':
68 $redirect = self::switch_sharing_to_block();
69 break;
70 case 'save-settings':
71 $redirect = self::save_settings();
72 break;
73 }
74
75 if ( null === $redirect ) {
76 return;
77 }
78
79 wp_safe_redirect( $redirect );
80 exit;
81 }
82
83 /**
84 * Stop producing legacy sharing buttons, so the block can take over.
85 *
86 * This is a migration, not the section's off switch: it is what the Jetpack
87 * dashboard's "Switch to the … block" button does, and it leaves the block
88 * itself untouched. Simple has no module to deactivate, so it removes every
89 * service instead, which the services list can undo.
90 *
91 * @return string URL to send the browser back to.
92 */
93 private static function switch_sharing_to_block(): string {
94 check_admin_referer( Sharing_Section::NONCE_ACTION );
95
96 if ( Environment::is_simple_site() ) {
97 self::remove_all_sharing_services();
98 } else {
99 ( new Modules() )->deactivate( 'sharedaddy' );
100 }
101
102 return self::redirect_url( true );
103 }
104
105 /**
106 * The Like buttons counterpart of `switch_sharing_to_block()`.
107 *
108 * On Simple it turns off Likes and Reblogs for every post, since the legacy
109 * widget renders for either. Posts that opted in individually keep their
110 * buttons, and Comment Likes has no block to move to, so it is left alone.
111 *
112 * @return string URL to send the browser back to.
113 */
114 private static function switch_likes_to_block(): string {
115 check_admin_referer( Likes_Section::NONCE_ACTION );
116
117 if ( Environment::is_simple_site() ) {
118 update_option( 'disabled_likes', 1 );
119 update_option( 'disabled_reblogs', 1 );
120 } else {
121 ( new Modules() )->deactivate( 'likes' );
122 }
123
124 return self::redirect_url( true );
125 }
126
127 /**
128 * Leave sharedaddy no services to render.
129 */
130 private static function remove_all_sharing_services(): void {
131 // Preferred over writing the option, because wpcom hooks the state change it announces.
132 if ( class_exists( 'Sharing_Service' ) ) {
133 ( new \Sharing_Service() )->set_blog_services( array(), array() );
134 return;
135 }
136
137 update_option(
138 'sharing-services',
139 array(
140 'visible' => array(),
141 'hidden' => array(),
142 )
143 );
144 }
145
146 /**
147 * Save every section that put fields on the form.
148 *
149 * Only those: the others' fields were not on the screen, and reading their
150 * absence as "off" would switch them off.
151 *
152 * @return string URL to send the browser back to.
153 */
154 private static function save_settings(): string {
155 check_admin_referer( Settings_Form::NONCE_ACTION );
156
157 $sections = Settings_Form::posted_sections();
158 $comment_likes_held = true;
159
160 // Before placement, because the services save rebuilds the global options it lives in.
161 if ( in_array( Settings_Form::SECTION_SHARING, $sections, true ) ) {
162 self::save_sharing_options();
163 }
164
165 if ( in_array( Settings_Form::SECTION_PLACEMENT, $sections, true ) ) {
166 self::save_placement();
167 }
168
169 if ( in_array( Settings_Form::SECTION_LIKES, $sections, true ) ) {
170 self::save_likes();
171 }
172
173 if ( in_array( Settings_Form::SECTION_COMMENT_LIKES, $sections, true ) && Environment::likes_supported() ) {
174 $comment_likes_held = self::save_comment_likes();
175 }
176
177 // Once, from whichever section rendered `Services_Config::global_options()`; never both.
178 if ( array_intersect( array( Settings_Form::SECTION_SHARING, Settings_Form::SECTION_EXTRAS ), $sections ) ) {
179 self::save_global_options( $sections );
180 }
181
182 return $comment_likes_held
183 ? self::redirect_url( true )
184 : add_query_arg( Settings_Page::COMMENT_LIKES_UNCHANGED, '1', self::redirect_url( true ) );
185 }
186
187 /**
188 * Save the rows that close the settings table, ours and then third parties'.
189 *
190 * @param string[] $sections Sections the submitted form carried fields for.
191 */
192 private static function save_global_options( array $sections ): void {
193 // Only the services section renders it, and `is_available()` can have turned true
194 // since the form was built, so the claim decides rather than the environment.
195 if ( in_array( Settings_Form::SECTION_SHARING, $sections, true ) ) {
196 Sharing_Resources::save();
197 }
198
199 Twitter_Site_Tag::save();
200
201 /** This action is documented in projects/packages/sharing-likes/src/settings/class-services-config.php */
202 do_action( 'sharing_admin_update' );
203 }
204
205 /**
206 * Save the services list's own settings: button style and label.
207 */
208 private static function save_sharing_options(): void {
209 // The section renders only when this class is loaded, but the request can claim it regardless.
210 if ( ! class_exists( 'Sharing_Service' ) ) {
211 return;
212 }
213
214 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; set_global_options() validates each field.
215 $data = $_POST;
216
217 // set_global_options() rebuilds the global array from defaults, so a payload with no `show` would clear placement.
218 if ( ! isset( $data['show'] ) ) {
219 $data['show'] = Placement_Section::selected_post_types();
220 }
221
222 ( new \Sharing_Service() )->set_global_options( $data );
223 }
224
225 /**
226 * Save the Like buttons settings.
227 */
228 private static function save_likes(): void {
229 if ( 'off' === self::posted_choice( 'wpl_default' ) ) {
230 update_option( 'disabled_likes', 1 );
231 } else {
232 delete_option( 'disabled_likes' );
233 }
234
235 if ( Environment::is_simple_site() ) {
236 if ( 'off' === self::posted_choice( 'jetpack_reblogs_enabled' ) ) {
237 update_option( 'disabled_reblogs', 1 );
238 } else {
239 delete_option( 'disabled_reblogs' );
240 }
241 }
242 }
243
244 /**
245 * Save the Comment Likes checkbox: the option on Simple, the module on Atomic and Jetpack sites.
246 *
247 * @return bool Whether Comment Likes now match the checkbox.
248 */
249 private static function save_comment_likes(): bool {
250 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified by the caller.
251 $enabled = ! empty( $_POST['jetpack_comment_likes_enabled'] );
252
253 if ( Environment::is_simple_site() ) {
254 update_option( 'jetpack_comment_likes_enabled', $enabled ? 1 : 0 );
255 return true;
256 }
257
258 // `deactivate()` fires its hooks even when the module was already off.
259 if ( Environment::comment_likes_enabled() === $enabled ) {
260 return true;
261 }
262
263 if ( $enabled ) {
264 ( new Modules() )->activate( 'comment-likes', false, false );
265 } else {
266 ( new Modules() )->deactivate( 'comment-likes' );
267 }
268
269 // A host can force the module either way, and activation needs a connected owner.
270 return Environment::comment_likes_enabled() === $enabled;
271 }
272
273 /**
274 * Save where the buttons appear.
275 */
276 private static function save_placement(): void {
277 $options = get_option( 'sharing-options' );
278 if ( ! is_array( $options ) ) {
279 $options = array();
280 }
281
282 // Sites carry a malformed `global` (see #6121), and writing into it in place
283 // would fatal where the services save, which rebuilds it wholesale, does not.
284 if ( ! isset( $options['global'] ) || ! is_array( $options['global'] ) ) {
285 $options['global'] = array();
286 }
287
288 $allowed = array_values( get_post_types( array( 'public' => true ) ) );
289 $allowed[] = 'index';
290
291 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; the values are checked against an allowlist below.
292 $posted = isset( $_POST['show'] ) && is_array( $_POST['show'] ) ? wp_unslash( $_POST['show'] ) : array();
293 $posted = array_filter( $posted, 'is_scalar' );
294
295 $options['global']['show'] = array_values( array_intersect( $posted, $allowed ) );
296
297 update_option( 'sharing-options', $options );
298 }
299
300 /**
301 * One of a radio group's values, defaulting to "on" when nothing was posted.
302 *
303 * @param string $field Field name.
304 */
305 private static function posted_choice( string $field ): string {
306 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- callers verify before reading.
307 if ( empty( $_POST[ $field ] ) ) {
308 return 'on';
309 }
310
311 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- callers verify before reading.
312 return sanitize_text_field( wp_unslash( $_POST[ $field ] ) );
313 }
314
315 /**
316 * Where to send the browser once a submission is handled.
317 *
318 * @param bool $show_saved_notice Whether the screen should confirm a save.
319 */
320 private static function redirect_url( bool $show_saved_notice ): string {
321 $url = admin_url( 'options-general.php?page=' . Settings_Page::SLUG );
322
323 return $show_saved_notice ? $url . '&update=saved' : $url;
324 }
325
326 /**
327 * Turn a module back on, then reload the screen.
328 *
329 * Reached only from the OFF variant, where no block route exists and nothing
330 * else on the site will bring the feature back. Sites that can use the block
331 * are not offered it, matching the Jetpack dashboard.
332 *
333 * @param string $module Module slug.
334 * @param string $nonce_action Nonce action the submitting section uses.
335 * @return string URL to send the browser back to.
336 */
337 private static function activate_module( string $module, string $nonce_action ): string {
338 check_admin_referer( $nonce_action );
339
340 ( new Modules() )->activate( $module, false, false );
341
342 return self::redirect_url( false );
343 }
344
345 /**
346 * Hidden field naming the action a form is submitting.
347 *
348 * @param string $action Action name, matching a case above.
349 */
350 public static function render_action_field( string $action ): void {
351 printf(
352 '<input type="hidden" name="%1$s" value="%2$s" />',
353 esc_attr( self::ACTION_FIELD ),
354 esc_attr( $action )
355 );
356 }
357
358 /**
359 * Markup for a single-button form submitting one of the actions above.
360 *
361 * @param string $action Action name, matching a case above.
362 * @param string $nonce_action Nonce action for the submitting section.
363 * @param string $label Button label.
364 * @param bool $primary Whether this is the only action in its state.
365 */
366 public static function render_action_form( string $action, string $nonce_action, string $label, bool $primary = true ): void {
367 ?>
368 <form method="post" action="">
369 <input type="hidden" name="<?php echo esc_attr( self::ACTION_FIELD ); ?>" value="<?php echo esc_attr( $action ); ?>" />
370 <?php wp_nonce_field( $nonce_action ); ?>
371 <p><button type="submit" class="<?php echo esc_attr( $primary ? 'button button-primary' : 'button' ); ?>"><?php echo esc_html( $label ); ?></button></p>
372 </form>
373 <?php
374 }
375 }
376