Sharing. * * @package automattic/jetpack-sharing-likes */ declare( strict_types = 1 ); namespace Automattic\Jetpack\Sharing_Likes\Settings; use Automattic\Jetpack\Modules; /** * Processes the screen's form submissions. * * Each section posts its own action with its own nonce, so saving one section * never runs another section's handlers. */ final class Post_Handler { /** * Field naming the requested action. */ private const ACTION_FIELD = 'jetpack_sharing_action'; /** * Hook the handler up. */ public static function init(): void { add_action( 'admin_init', array( __CLASS__, 'maybe_handle' ) ); } /** * Dispatch a submission, if this request is one. */ public static function maybe_handle(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- identifying the screen; the nonce is verified below. if ( ! isset( $_GET['page'] ) || Settings_Page::SLUG !== $_GET['page'] ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified per action below. if ( ! isset( $_POST[ self::ACTION_FIELD ] ) ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified per action below. $action = sanitize_key( wp_unslash( $_POST[ self::ACTION_FIELD ] ) ); $redirect = null; switch ( $action ) { case 'activate-likes': $redirect = self::activate_module( 'likes', Likes_Section::NONCE_ACTION ); break; case 'activate-sharing': $redirect = self::activate_module( 'sharedaddy', Sharing_Section::NONCE_ACTION ); break; case 'switch-to-block-likes': $redirect = self::switch_likes_to_block(); break; case 'switch-to-block-sharing': $redirect = self::switch_sharing_to_block(); break; case 'save-settings': $redirect = self::save_settings(); break; } if ( null === $redirect ) { return; } wp_safe_redirect( $redirect ); exit; } /** * Stop producing legacy sharing buttons, so the block can take over. * * This is a migration, not the section's off switch: it is what the Jetpack * dashboard's "Switch to the … block" button does, and it leaves the block * itself untouched. Simple has no module to deactivate, so it removes every * service instead, which the services list can undo. * * @return string URL to send the browser back to. */ private static function switch_sharing_to_block(): string { check_admin_referer( Sharing_Section::NONCE_ACTION ); if ( Environment::is_simple_site() ) { self::remove_all_sharing_services(); } else { ( new Modules() )->deactivate( 'sharedaddy' ); } return self::redirect_url( true ); } /** * The Like buttons counterpart of `switch_sharing_to_block()`. * * On Simple it turns off Likes and Reblogs for every post, since the legacy * widget renders for either. Posts that opted in individually keep their * buttons, and Comment Likes has no block to move to, so it is left alone. * * @return string URL to send the browser back to. */ private static function switch_likes_to_block(): string { check_admin_referer( Likes_Section::NONCE_ACTION ); if ( Environment::is_simple_site() ) { update_option( 'disabled_likes', 1 ); update_option( 'disabled_reblogs', 1 ); } else { ( new Modules() )->deactivate( 'likes' ); } return self::redirect_url( true ); } /** * Leave sharedaddy no services to render. */ private static function remove_all_sharing_services(): void { // Preferred over writing the option, because wpcom hooks the state change it announces. if ( class_exists( 'Sharing_Service' ) ) { ( new \Sharing_Service() )->set_blog_services( array(), array() ); return; } update_option( 'sharing-services', array( 'visible' => array(), 'hidden' => array(), ) ); } /** * Save every section that put fields on the form. * * Only those: the others' fields were not on the screen, and reading their * absence as "off" would switch them off. * * @return string URL to send the browser back to. */ private static function save_settings(): string { check_admin_referer( Settings_Form::NONCE_ACTION ); $sections = Settings_Form::posted_sections(); $comment_likes_held = true; // Before placement, because the services save rebuilds the global options it lives in. if ( in_array( Settings_Form::SECTION_SHARING, $sections, true ) ) { self::save_sharing_options(); } if ( in_array( Settings_Form::SECTION_PLACEMENT, $sections, true ) ) { self::save_placement(); } if ( in_array( Settings_Form::SECTION_LIKES, $sections, true ) ) { self::save_likes(); } if ( in_array( Settings_Form::SECTION_COMMENT_LIKES, $sections, true ) && Environment::likes_supported() ) { $comment_likes_held = self::save_comment_likes(); } // Once, from whichever section rendered `Services_Config::global_options()`; never both. if ( array_intersect( array( Settings_Form::SECTION_SHARING, Settings_Form::SECTION_EXTRAS ), $sections ) ) { self::save_global_options( $sections ); } return $comment_likes_held ? self::redirect_url( true ) : add_query_arg( Settings_Page::COMMENT_LIKES_UNCHANGED, '1', self::redirect_url( true ) ); } /** * Save the rows that close the settings table, ours and then third parties'. * * @param string[] $sections Sections the submitted form carried fields for. */ private static function save_global_options( array $sections ): void { // Only the services section renders it, and `is_available()` can have turned true // since the form was built, so the claim decides rather than the environment. if ( in_array( Settings_Form::SECTION_SHARING, $sections, true ) ) { Sharing_Resources::save(); } Twitter_Site_Tag::save(); /** This action is documented in projects/packages/sharing-likes/src/settings/class-services-config.php */ do_action( 'sharing_admin_update' ); } /** * Save the services list's own settings: button style and label. */ private static function save_sharing_options(): void { // The section renders only when this class is loaded, but the request can claim it regardless. if ( ! class_exists( 'Sharing_Service' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; set_global_options() validates each field. $data = $_POST; // set_global_options() rebuilds the global array from defaults, so a payload with no `show` would clear placement. if ( ! isset( $data['show'] ) ) { $data['show'] = Placement_Section::selected_post_types(); } ( new \Sharing_Service() )->set_global_options( $data ); } /** * Save the Like buttons settings. */ private static function save_likes(): void { if ( 'off' === self::posted_choice( 'wpl_default' ) ) { update_option( 'disabled_likes', 1 ); } else { delete_option( 'disabled_likes' ); } if ( Environment::is_simple_site() ) { if ( 'off' === self::posted_choice( 'jetpack_reblogs_enabled' ) ) { update_option( 'disabled_reblogs', 1 ); } else { delete_option( 'disabled_reblogs' ); } } } /** * Save the Comment Likes checkbox: the option on Simple, the module on Atomic and Jetpack sites. * * @return bool Whether Comment Likes now match the checkbox. */ private static function save_comment_likes(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified by the caller. $enabled = ! empty( $_POST['jetpack_comment_likes_enabled'] ); if ( Environment::is_simple_site() ) { update_option( 'jetpack_comment_likes_enabled', $enabled ? 1 : 0 ); return true; } // `deactivate()` fires its hooks even when the module was already off. if ( Environment::comment_likes_enabled() === $enabled ) { return true; } if ( $enabled ) { ( new Modules() )->activate( 'comment-likes', false, false ); } else { ( new Modules() )->deactivate( 'comment-likes' ); } // A host can force the module either way, and activation needs a connected owner. return Environment::comment_likes_enabled() === $enabled; } /** * Save where the buttons appear. */ private static function save_placement(): void { $options = get_option( 'sharing-options' ); if ( ! is_array( $options ) ) { $options = array(); } // Sites carry a malformed `global` (see #6121), and writing into it in place // would fatal where the services save, which rebuilds it wholesale, does not. if ( ! isset( $options['global'] ) || ! is_array( $options['global'] ) ) { $options['global'] = array(); } $allowed = array_values( get_post_types( array( 'public' => true ) ) ); $allowed[] = 'index'; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; the values are checked against an allowlist below. $posted = isset( $_POST['show'] ) && is_array( $_POST['show'] ) ? wp_unslash( $_POST['show'] ) : array(); $posted = array_filter( $posted, 'is_scalar' ); $options['global']['show'] = array_values( array_intersect( $posted, $allowed ) ); update_option( 'sharing-options', $options ); } /** * One of a radio group's values, defaulting to "on" when nothing was posted. * * @param string $field Field name. */ private static function posted_choice( string $field ): string { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- callers verify before reading. if ( empty( $_POST[ $field ] ) ) { return 'on'; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- callers verify before reading. return sanitize_text_field( wp_unslash( $_POST[ $field ] ) ); } /** * Where to send the browser once a submission is handled. * * @param bool $show_saved_notice Whether the screen should confirm a save. */ private static function redirect_url( bool $show_saved_notice ): string { $url = admin_url( 'options-general.php?page=' . Settings_Page::SLUG ); return $show_saved_notice ? $url . '&update=saved' : $url; } /** * Turn a module back on, then reload the screen. * * Reached only from the OFF variant, where no block route exists and nothing * else on the site will bring the feature back. Sites that can use the block * are not offered it, matching the Jetpack dashboard. * * @param string $module Module slug. * @param string $nonce_action Nonce action the submitting section uses. * @return string URL to send the browser back to. */ private static function activate_module( string $module, string $nonce_action ): string { check_admin_referer( $nonce_action ); ( new Modules() )->activate( $module, false, false ); return self::redirect_url( false ); } /** * Hidden field naming the action a form is submitting. * * @param string $action Action name, matching a case above. */ public static function render_action_field( string $action ): void { printf( '', esc_attr( self::ACTION_FIELD ), esc_attr( $action ) ); } /** * Markup for a single-button form submitting one of the actions above. * * @param string $action Action name, matching a case above. * @param string $nonce_action Nonce action for the submitting section. * @param string $label Button label. * @param bool $primary Whether this is the only action in its state. */ public static function render_action_form( string $action, string $nonce_action, string $label, bool $primary = true ): void { ?>