PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta
16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / json-endpoints / class.wpcom-json-api-site-settings-endpoint.php
jetpack / json-endpoints Last commit date
jetpack 1 month ago class.wpcom-json-api-add-widget-endpoint.php 8 months ago class.wpcom-json-api-autosave-post-v1-1-endpoint.php 8 months ago class.wpcom-json-api-bulk-delete-post-endpoint.php 8 months ago class.wpcom-json-api-bulk-restore-post-endpoint.php 8 months ago class.wpcom-json-api-bulk-update-comments-endpoint.php 2 months ago class.wpcom-json-api-comment-endpoint.php 8 months ago class.wpcom-json-api-delete-media-endpoint.php 8 months ago class.wpcom-json-api-delete-media-v1-1-endpoint.php 8 months ago class.wpcom-json-api-edit-media-v1-2-endpoint.php 2 months ago class.wpcom-json-api-get-autosave-v1-1-endpoint.php 8 months ago class.wpcom-json-api-get-comment-counts-endpoint.php 8 months ago class.wpcom-json-api-get-comment-endpoint.php 8 months ago class.wpcom-json-api-get-comment-history-endpoint.php 8 months ago class.wpcom-json-api-get-comments-tree-endpoint.php 8 months ago class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php 8 months ago class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php 8 months ago class.wpcom-json-api-get-customcss.php 8 months ago class.wpcom-json-api-get-media-endpoint.php 8 months ago class.wpcom-json-api-get-media-v1-1-endpoint.php 8 months ago class.wpcom-json-api-get-media-v1-2-endpoint.php 8 months ago class.wpcom-json-api-get-post-counts-v1-1-endpoint.php 1 month ago class.wpcom-json-api-get-post-endpoint.php 8 months ago class.wpcom-json-api-get-post-v1-1-endpoint.php 2 weeks ago class.wpcom-json-api-get-site-endpoint.php 2 weeks ago class.wpcom-json-api-get-site-v1-2-endpoint.php 2 weeks ago class.wpcom-json-api-get-taxonomies-endpoint.php 3 months ago class.wpcom-json-api-get-taxonomy-endpoint.php 8 months ago class.wpcom-json-api-get-term-endpoint.php 8 months ago class.wpcom-json-api-list-comments-endpoint.php 1 month ago class.wpcom-json-api-list-dropdown-pages-endpoint.php 8 months ago class.wpcom-json-api-list-embeds-endpoint.php 8 months ago class.wpcom-json-api-list-media-endpoint.php 8 months ago class.wpcom-json-api-list-media-v1-1-endpoint.php 2 months ago class.wpcom-json-api-list-media-v1-2-endpoint.php 8 months ago class.wpcom-json-api-list-post-type-taxonomies-endpoint.php 8 months ago class.wpcom-json-api-list-post-types-endpoint.php 8 months ago class.wpcom-json-api-list-posts-endpoint.php 1 month ago class.wpcom-json-api-list-posts-v1-1-endpoint.php 1 month ago class.wpcom-json-api-list-posts-v1-2-endpoint.php 1 month ago class.wpcom-json-api-list-roles-endpoint.php 8 months ago class.wpcom-json-api-list-shortcodes-endpoint.php 8 months ago class.wpcom-json-api-list-terms-endpoint.php 8 months ago class.wpcom-json-api-list-users-endpoint.php 1 month ago class.wpcom-json-api-menus-v1-1-endpoint.php 2 months ago class.wpcom-json-api-post-endpoint.php 8 months ago class.wpcom-json-api-post-v1-1-endpoint.php 3 months ago class.wpcom-json-api-render-embed-endpoint.php 8 months ago class.wpcom-json-api-render-embed-reversal-endpoint.php 8 months ago class.wpcom-json-api-render-endpoint.php 2 months ago class.wpcom-json-api-render-shortcode-endpoint.php 8 months ago class.wpcom-json-api-sharing-buttons-endpoint.php 2 months ago class.wpcom-json-api-site-settings-endpoint.php 6 days ago class.wpcom-json-api-site-settings-v1-2-endpoint.php 8 months ago class.wpcom-json-api-site-settings-v1-3-endpoint.php 8 months ago class.wpcom-json-api-site-settings-v1-4-endpoint.php 1 month ago class.wpcom-json-api-site-user-endpoint.php 8 months ago class.wpcom-json-api-taxonomy-endpoint.php 8 months ago class.wpcom-json-api-update-comment-endpoint.php 6 months ago class.wpcom-json-api-update-customcss.php 8 months ago class.wpcom-json-api-update-media-endpoint.php 8 months ago class.wpcom-json-api-update-media-v1-1-endpoint.php 2 months ago class.wpcom-json-api-update-post-endpoint.php 2 months ago class.wpcom-json-api-update-post-v1-1-endpoint.php 2 months ago class.wpcom-json-api-update-post-v1-2-endpoint.php 2 months ago class.wpcom-json-api-update-site-homepage-endpoint.php 8 months ago class.wpcom-json-api-update-site-logo-endpoint.php 8 months ago class.wpcom-json-api-update-taxonomy-endpoint.php 7 months ago class.wpcom-json-api-update-term-endpoint.php 8 months ago class.wpcom-json-api-update-user-endpoint.php 8 months ago class.wpcom-json-api-upload-media-endpoint.php 8 months ago class.wpcom-json-api-upload-media-v1-1-endpoint.php 8 months ago
class.wpcom-json-api-site-settings-endpoint.php
1596 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Manage settings via the WordPress.com REST API.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection_Shared_Functions;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit( 0 );
12 }
13
14 new WPCOM_JSON_API_Site_Settings_Endpoint(
15 array(
16 'description' => 'Get detailed settings information about a site.',
17 'group' => '__do_not_document',
18 'stat' => 'sites:X',
19 'max_version' => '1.1',
20 'new_version' => '1.2',
21 'method' => 'GET',
22 'path' => '/sites/%s/settings',
23 'path_labels' => array(
24 '$site' => '(int|string) Site ID or domain',
25 ),
26
27 'query_parameters' => array(
28 'context' => false,
29 ),
30
31 'response_format' => WPCOM_JSON_API_Site_Settings_Endpoint::$site_format,
32
33 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings',
34 )
35 );
36
37 new WPCOM_JSON_API_Site_Settings_Endpoint(
38 array(
39 'description' => 'Update settings for a site.',
40 'group' => '__do_not_document',
41 'stat' => 'sites:X',
42 'max_version' => '1.1',
43 'new_version' => '1.2',
44 'method' => 'POST',
45 'path' => '/sites/%s/settings',
46 'a_new_very_long_key' => 'blabla',
47 'path_labels' => array(
48 '$site' => '(int|string) Site ID or domain',
49 ),
50
51 'request_format' => array(
52 'migration_source_site_domain' => '(string) The source site URL, from the migration flow',
53 'in_site_migration_flow' => '(string) The migration flow the site is in',
54 'blogname' => '(string) Blog name',
55 'blogdescription' => '(string) Blog description',
56 'default_pingback_flag' => '(bool) Notify blogs linked from article?',
57 'default_ping_status' => '(bool) Allow link notifications from other blogs?',
58 'default_comment_status' => '(bool) Allow comments on new articles?',
59 'blog_public' => '(string) Site visibility; -1: private, 0: discourage search engines, 1: allow search engines',
60 'wpcom_data_sharing_opt_out' => '(bool) Did the site opt out of sharing public content with third parties and research partners?',
61 'jetpack_sync_non_public_post_stati' => '(bool) allow sync of post and pages with non-public posts stati',
62 'jetpack_relatedposts_enabled' => '(bool) Enable related posts?',
63 'jetpack_relatedposts_show_context' => '(bool) Show post\'s tags and category in related posts?',
64 'jetpack_relatedposts_show_date' => '(bool) Show date in related posts?',
65 'jetpack_relatedposts_show_headline' => '(bool) Show headline in related posts?',
66 'jetpack_relatedposts_show_thumbnails' => '(bool) Show thumbnails in related posts?',
67 'jetpack_protect_whitelist' => '(array) List of IP addresses to always allow',
68 'instant_search_enabled' => '(bool) Enable the new Jetpack Instant Search interface',
69 'jetpack_search_enabled' => '(bool) Enable Jetpack Search',
70 'jetpack_search_supported' => '(bool) Jetpack Search is supported',
71 'infinite_scroll' => '(bool) Support infinite scroll of posts?',
72 'default_category' => '(int) Default post category',
73 'default_post_format' => '(string) Default post format',
74 'require_name_email' => '(bool) Require comment authors to fill out name and email?',
75 'comment_registration' => '(bool) Require users to be registered and logged in to comment?',
76 'close_comments_for_old_posts' => '(bool) Automatically close comments on old posts?',
77 'close_comments_days_old' => '(int) Age at which to close comments',
78 'thread_comments' => '(bool) Enable threaded comments?',
79 'thread_comments_depth' => '(int) Depth to thread comments',
80 'page_comments' => '(bool) Break comments into pages?',
81 'comments_per_page' => '(int) Number of comments to display per page',
82 'default_comments_page' => '(string) newest|oldest Which page of comments to display first',
83 'comment_order' => '(string) asc|desc Order to display comments within page',
84 'comments_notify' => '(bool) Email me when someone comments?',
85 'moderation_notify' => '(bool) Email me when a comment is helf for moderation?',
86 'social_notifications_like' => '(bool) Email me when someone likes my post?',
87 'social_notifications_reblog' => '(bool) Email me when someone reblogs my post?',
88 'social_notifications_subscribe' => '(bool) Email me when someone subscribes to my blog?',
89 'comment_moderation' => '(bool) Moderate comments for manual approval?',
90 'comment_previously_approved' => '(bool) Moderate comments unless author has a previously-approved comment?',
91 'comment_max_links' => '(int) Moderate comments that contain X or more links',
92 'moderation_keys' => '(string) Words or phrases that trigger comment moderation, one per line',
93 'disallowed_keys' => '(string) Words or phrases that mark comment spam, one per line',
94 'lang_id' => '(int) ID for language blog is written in',
95 'wga' => '(array) Google Analytics Settings',
96 'disabled_likes' => '(bool) Are likes globally disabled (they can still be turned on per post)?',
97 'disabled_reblogs' => '(bool) Are reblogs disabled on posts?',
98 'jetpack_comment_likes_enabled' => '(bool) Are comment likes enabled for all comments?',
99 'sharing_button_style' => '(string) Style to use for sharing buttons (icon-text, icon, text, or official)',
100 'sharing_label' => '(string) Label to use for sharing buttons, e.g. "Share this:"',
101 'sharing_show' => '(string|array:string) Post type or array of types where sharing buttons are to be displayed',
102 'sharing_open_links' => '(string) Link target for sharing buttons (same or new)',
103 'twitter_via' => '(string) Twitter username to include in tweets when people share using the Twitter button',
104 'jetpack-twitter-cards-site-tag' => '(string) The Twitter username of the owner of the site\'s domain.',
105 'eventbrite_api_token' => '(int) The Keyring token ID for an Eventbrite token to associate with the site',
106 'timezone_string' => '(string) PHP-compatible timezone string like \'UTC-5\'',
107 'gmt_offset' => '(int) Site offset from UTC in hours',
108 'date_format' => '(string) PHP Date-compatible date format',
109 'time_format' => '(string) PHP Date-compatible time format',
110 'start_of_week' => '(int) Starting day of week (0 = Sunday, 6 = Saturday)',
111 'jetpack_testimonial' => '(bool) Whether testimonial custom post type is enabled for the site',
112 'jetpack_testimonial_posts_per_page' => '(int) Number of testimonials to show per page',
113 'jetpack_portfolio' => '(bool) Whether portfolio custom post type is enabled for the site',
114 'jetpack_portfolio_posts_per_page' => '(int) Number of portfolio projects to show per page',
115 Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => '(string) The seo meta description for the site.',
116 Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => '(array) SEO meta title formats. Allowed keys: front_page, posts, pages, groups, archives',
117 'verification_services_codes' => '(array) Website verification codes. Allowed keys: google, pinterest, bing, yandex, facebook',
118 'markdown_supported' => '(bool) Whether markdown is supported for this site',
119 'wpcom_publish_posts_with_markdown' => '(bool) Whether markdown is enabled for posts',
120 'wpcom_publish_comments_with_markdown' => '(bool) Whether markdown is enabled for comments',
121 'site_icon' => '(int) Media attachment ID to use as site icon. Set to zero or an otherwise empty value to clear',
122 'api_cache' => '(bool) Turn on/off the Jetpack JSON API cache',
123 'posts_per_page' => '(int) Number of posts to show on blog pages',
124 'posts_per_rss' => '(int) Number of posts to show in the RSS feed',
125 'rss_use_excerpt' => '(bool) Whether the RSS feed will use post excerpts',
126 'launchpad_screen' => '(string) Whether or not launchpad is presented and what size it will be',
127 'sm_enabled' => '(bool) Whether the newsletter subscribe modal is enabled',
128 'jetpack_subscribe_overlay_enabled' => '(bool) Whether the newsletter subscribe overlay is enabled',
129 'jetpack_subscribe_floating_button_enabled' => '(bool) Whether the newsletter floating subscribe button is enabled',
130 'jetpack_subscriptions_subscribe_post_end_enabled' => '(bool) Whether the Subscribe block at the end of each post placement is enabled',
131 'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled',
132 'jetpack_subscriptions_subscribe_navigation_enabled' => '(Bool) Whether the Subscribe block navigation placement is enabled',
133 'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt',
134 'jetpack_waf_automatic_rules' => '(bool) Whether the WAF should enforce automatic firewall rules',
135 'jetpack_waf_ip_allow_list' => '(string) List of IP addresses to always allow',
136 'jetpack_waf_ip_allow_list_enabled' => '(bool) Whether the IP allow list is enabled',
137 'jetpack_waf_ip_block_list' => '(string) List of IP addresses the WAF should always block',
138 'jetpack_waf_ip_block_list_enabled' => '(bool) Whether the IP block list is enabled',
139 'jetpack_waf_share_data' => '(bool) Whether the WAF should share basic data with Jetpack',
140 'jetpack_waf_share_debug_data' => '(bool) Whether the WAF should share debug data with Jetpack',
141 'jetpack_waf_automatic_rules_last_updated_timestamp' => '(int) Timestamp of the last time the automatic rules were updated',
142 'mcp_abilities' => '(array) List of MCP Abilities',
143 ),
144
145 'response_format' => array(
146 'updated' => '(array)',
147 ),
148
149 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/settings',
150 )
151 );
152
153 /**
154 * Manage Site settings endpoint.
155 *
156 * @phan-constructor-used-for-side-effects
157 */
158 class WPCOM_JSON_API_Site_Settings_Endpoint extends WPCOM_JSON_API_Endpoint {
159
160 /**
161 * Site format.
162 *
163 * @var array
164 */
165 public static $site_format = array(
166 'ID' => '(int) Site ID',
167 'name' => '(string) Title of site',
168 'description' => '(string) Tagline or description of site',
169 'URL' => '(string) Full URL to the site',
170 'lang' => '(string) Primary language code of the site',
171 'locale_variant' => '(string) Locale variant code for the site, if set',
172 'settings' => '(array) An array of options/settings for the blog. Only viewable by users with post editing rights to the site.',
173 );
174
175 /**
176 * Endpoint response
177 *
178 * GET /sites/%s/settings
179 * POST /sites/%s/settings
180 *
181 * @param string $path Path.
182 * @param int $blog_id Blog ID.
183 */
184 public function callback( $path = '', $blog_id = 0 ) {
185 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
186 if ( is_wp_error( $blog_id ) ) {
187 return $blog_id;
188 }
189
190 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
191 // Source & include the infinite scroll compatibility files prior to loading theme functions.
192 add_filter( 'restapi_theme_action_copy_dirs', array( 'WPCOM_JSON_API_Site_Settings_Endpoint', 'wpcom_restapi_copy_theme_plugin_actions' ) );
193 $this->load_theme_functions();
194 }
195
196 if ( ! is_user_logged_in() ) {
197 return new WP_Error( 'Unauthorized', 'You must be logged-in to manage settings.', 401 );
198 } elseif ( ! current_user_can( 'manage_options' ) ) {
199 return new WP_Error( 'Forbidden', 'You do not have the capability to manage settings for this site.', 403 );
200 }
201
202 if ( 'GET' === $this->api->method ) {
203 /**
204 * Fires on each GET request to a specific endpoint.
205 *
206 * @module json-api
207 *
208 * @since 3.2.0
209 *
210 * @param string sites.
211 */
212 do_action( 'wpcom_json_api_objects', 'sites' );
213 return $this->get_settings_response();
214 } elseif ( 'POST' === $this->api->method ) {
215 return $this->update_settings();
216 } else {
217 return new WP_Error( 'bad_request', 'An unsupported request method was used.' );
218 }
219 }
220
221 /**
222 * Includes additional theme-specific files to be included in REST API theme
223 * context loading action copying.
224 *
225 * @see WPCOM_JSON_API_Endpoint#load_theme_functions
226 * @see the_neverending_home_page_theme_support
227 *
228 * @param array $copy_dirs Array of files to be included in theme context.
229 */
230 public static function wpcom_restapi_copy_theme_plugin_actions( $copy_dirs ) {
231 $theme_name = get_stylesheet();
232 $default_file_name = WP_CONTENT_DIR . "/mu-plugins/infinity/themes/{$theme_name}.php";
233
234 /**
235 * Filter the path to the Infinite Scroll compatibility file.
236 *
237 * @module infinite-scroll
238 *
239 * @since 2.0.0
240 *
241 * @param string $str IS compatibility file path.
242 * @param string $theme_name Theme name.
243 */
244 $customization_file = apply_filters( 'infinite_scroll_customization_file', $default_file_name, $theme_name );
245
246 if ( is_readable( $customization_file ) ) {
247 require_once $customization_file;
248 $copy_dirs[] = $customization_file;
249 }
250
251 return $copy_dirs;
252 }
253
254 /**
255 * Determines whether jetpack_relatedposts is supported
256 *
257 * @return bool
258 */
259 public function jetpack_relatedposts_supported() {
260 $wpcom_related_posts_theme_blacklist = array(
261 'Expound',
262 'Traveler',
263 'Opti',
264 'Currents',
265 );
266 return ( ! in_array( wp_get_theme()->get( 'Name' ), $wpcom_related_posts_theme_blacklist, true ) );
267 }
268
269 /**
270 * Returns category details
271 *
272 * @param WP_Term $category Category object.
273 *
274 * @return array
275 */
276 public function get_category_details( $category ) {
277 return array(
278 'value' => $category->term_id,
279 'name' => $category->name,
280 );
281 }
282
283 /**
284 * Returns an option value as the result of the callable being applied to
285 * it if a value is set, otherwise null.
286 *
287 * @param string $option_name Option name.
288 * @param callable $cast_callable Callable to invoke on option value.
289 *
290 * @return int|null Numeric option value or null.
291 */
292 protected function get_cast_option_value_or_null( $option_name, $cast_callable ) {
293 $option_value = get_option( $option_name, null );
294 if ( $option_value === null ) {
295 return $option_value;
296 }
297
298 return call_user_func( $cast_callable, $option_value );
299 }
300
301 /**
302 * Collects the necessary information to return for a get settings response.
303 *
304 * @return array
305 */
306 public function get_settings_response() {
307 $response = array();
308
309 // Allow update in later versions.
310 /**
311 * Filter the structure of site settings to return.
312 *
313 * @module json-api
314 *
315 * @since 3.9.3
316 *
317 * @param array $site_format Data structure.
318 */
319 $response_format = apply_filters( 'site_settings_site_format', self::$site_format );
320
321 $blog_id = (int) $this->api->get_blog_id_for_output();
322 $site = $this->get_platform()->get_site( $blog_id );
323
324 foreach ( array_keys( $response_format ) as $key ) {
325
326 // refactoring to change lang parameter to locale in 1.2.
327 $lang_or_locale = $this->get_locale( $key );
328 if ( $lang_or_locale ) {
329 $response[ $key ] = $lang_or_locale;
330 continue;
331 }
332
333 switch ( $key ) {
334 case 'ID':
335 $response[ $key ] = $blog_id;
336 break;
337 case 'name':
338 $name = get_bloginfo( 'name' );
339 $response[ $key ] = is_string( $name ) ? htmlspecialchars_decode( $name, ENT_QUOTES ) : '';
340 break;
341 case 'description':
342 $description = get_bloginfo( 'description' );
343 $response[ $key ] = is_string( $description ) ? htmlspecialchars_decode( $description, ENT_QUOTES ) : '';
344 break;
345 case 'URL':
346 $response[ $key ] = (string) home_url();
347 break;
348 case 'locale_variant':
349 if ( function_exists( 'wpcom_l10n_get_blog_locale_variant' ) ) {
350 $blog_locale_variant = wpcom_l10n_get_blog_locale_variant();
351 if ( $blog_locale_variant ) {
352 $response[ $key ] = $blog_locale_variant;
353 }
354 }
355 break;
356 case 'settings':
357 $jetpack_relatedposts_options = Jetpack_Options::get_option( 'relatedposts', array() );
358 // If the option's enabled key is NOT SET, it is considered enabled by the plugin.
359 if ( ! isset( $jetpack_relatedposts_options['enabled'] ) ) {
360 $jetpack_relatedposts_options['enabled'] = true;
361 }
362
363 $jetpack_relatedposts_options['enabled'] =
364 $jetpack_relatedposts_options['enabled']
365 && $site->is_module_active( 'related-posts' );
366
367 $jetpack_search_supported = false;
368 if ( function_exists( 'wpcom_is_jetpack_search_supported' ) ) {
369 $jetpack_search_supported = wpcom_is_jetpack_search_supported( $blog_id );
370 }
371
372 $jetpack_search_active =
373 $jetpack_search_supported
374 && $site->is_module_active( 'search' );
375
376 // array_values() is necessary to ensure the array starts at index 0.
377 $post_categories = array_values(
378 array_map(
379 array( $this, 'get_category_details' ),
380 get_categories( array( 'hide_empty' => false ) )
381 )
382 );
383
384 // Make sure we are returning a consistent type
385 if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
386 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
387 }
388 $newsletter_category_ids = Jetpack_Newsletter_Category_Helper::get_category_ids();
389
390 $api_cache = $site->is_jetpack() ? (bool) get_option( 'jetpack_api_cache_enabled' ) : true;
391
392 // Get Sites MCP settings
393 $mcp_abilities = $this->get_site_mcp_abilities();
394
395 $response[ $key ] = array(
396 // also exists as "options".
397 'admin_url' => get_admin_url(),
398 'default_ping_status' => 'closed' !== get_option( 'default_ping_status' ),
399 'default_comment_status' => 'closed' !== get_option( 'default_comment_status' ),
400
401 // new stuff starts here.
402 'instant_search_enabled' => (bool) get_option( 'instant_search_enabled' ),
403 'blog_public' => (int) get_option( 'blog_public' ),
404 'wpcom_data_sharing_opt_out' => (bool) get_option( 'wpcom_data_sharing_opt_out' ),
405 'jetpack_sync_non_public_post_stati' => (bool) Jetpack_Options::get_option( 'sync_non_public_post_stati' ),
406 'jetpack_relatedposts_allowed' => (bool) $this->jetpack_relatedposts_supported(),
407 'jetpack_relatedposts_enabled' => (bool) $jetpack_relatedposts_options['enabled'],
408 'jetpack_relatedposts_show_context' => ! empty( $jetpack_relatedposts_options['show_context'] ),
409 'jetpack_relatedposts_show_date' => ! empty( $jetpack_relatedposts_options['show_date'] ),
410 'jetpack_relatedposts_show_headline' => ! empty( $jetpack_relatedposts_options['show_headline'] ),
411 'jetpack_relatedposts_show_thumbnails' => ! empty( $jetpack_relatedposts_options['show_thumbnails'] ),
412 'jetpack_search_enabled' => $jetpack_search_active,
413 'jetpack_search_supported' => (bool) $jetpack_search_supported,
414 'default_category' => (int) get_option( 'default_category' ),
415 'post_categories' => (array) $post_categories,
416 'default_post_format' => get_option( 'default_post_format' ),
417 'default_pingback_flag' => (bool) get_option( 'default_pingback_flag' ),
418 'require_name_email' => (bool) get_option( 'require_name_email' ),
419 'comment_registration' => (bool) get_option( 'comment_registration' ),
420 'close_comments_for_old_posts' => (bool) get_option( 'close_comments_for_old_posts' ),
421 'close_comments_days_old' => (int) get_option( 'close_comments_days_old' ),
422 'thread_comments' => (bool) get_option( 'thread_comments' ),
423 'thread_comments_depth' => (int) get_option( 'thread_comments_depth' ),
424 'page_comments' => (bool) get_option( 'page_comments' ),
425 'comments_per_page' => (int) get_option( 'comments_per_page' ),
426 'default_comments_page' => get_option( 'default_comments_page' ),
427 'comment_order' => get_option( 'comment_order' ),
428 'comments_notify' => (bool) get_option( 'comments_notify' ),
429 'moderation_notify' => (bool) get_option( 'moderation_notify' ),
430 'social_notifications_like' => ( 'on' === get_option( 'social_notifications_like' ) ),
431 'social_notifications_reblog' => ( 'on' === get_option( 'social_notifications_reblog' ) ),
432 'social_notifications_subscribe' => ( 'on' === get_option( 'social_notifications_subscribe' ) ),
433 'comment_moderation' => (bool) get_option( 'comment_moderation' ),
434 'comment_whitelist' => (bool) get_option( 'comment_previously_approved' ),
435 'comment_previously_approved' => (bool) get_option( 'comment_previously_approved' ),
436 'comment_max_links' => (int) get_option( 'comment_max_links' ),
437 'moderation_keys' => get_option( 'moderation_keys' ),
438 'blacklist_keys' => get_option( 'disallowed_keys' ),
439 'disallowed_keys' => get_option( 'disallowed_keys' ),
440 'lang_id' => defined( 'IS_WPCOM' ) && IS_WPCOM
441 ? get_lang_id_by_code( wpcom_l10n_get_blog_locale_variant( $blog_id, true ) )
442 : get_option( 'lang_id' ),
443 'site_vertical_id' => (string) get_option( 'site_vertical_id' ),
444 'jetpack_cloudflare_analytics' => get_option( 'jetpack_cloudflare_analytics' ),
445 'disabled_likes' => (bool) get_option( 'disabled_likes' ),
446 'disabled_reblogs' => (bool) get_option( 'disabled_reblogs' ),
447 'jetpack_comment_likes_enabled' => (bool) get_option( 'jetpack_comment_likes_enabled', false ),
448 'twitter_via' => (string) get_option( 'twitter_via' ),
449 'jetpack-twitter-cards-site-tag' => (string) get_option( 'jetpack-twitter-cards-site-tag' ),
450 'eventbrite_api_token' => $this->get_cast_option_value_or_null( 'eventbrite_api_token', 'intval' ),
451 'gmt_offset' => get_option( 'gmt_offset' ),
452 'timezone_string' => get_option( 'timezone_string' ),
453 'date_format' => get_option( 'date_format' ),
454 'time_format' => get_option( 'time_format' ),
455 'start_of_week' => get_option( 'start_of_week' ),
456 'woocommerce_onboarding_profile' => (array) get_option( 'woocommerce_onboarding_profile', array() ),
457 'woocommerce_store_address' => (string) get_option( 'woocommerce_store_address' ),
458 'woocommerce_store_address_2' => (string) get_option( 'woocommerce_store_address_2' ),
459 'woocommerce_store_city' => (string) get_option( 'woocommerce_store_city' ),
460 'woocommerce_default_country' => (string) get_option( 'woocommerce_default_country' ),
461 'woocommerce_store_postcode' => (string) get_option( 'woocommerce_store_postcode' ),
462 'jetpack_testimonial' => (bool) get_option( 'jetpack_testimonial', '0' ),
463 'jetpack_testimonial_posts_per_page' => (int) get_option( 'jetpack_testimonial_posts_per_page', '10' ),
464 'jetpack_portfolio' => (bool) get_option( 'jetpack_portfolio', '0' ),
465 'jetpack_portfolio_posts_per_page' => (int) get_option( 'jetpack_portfolio_posts_per_page', '10' ),
466 'markdown_supported' => true,
467 'site_icon' => $this->get_cast_option_value_or_null( 'site_icon', 'intval' ),
468 Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION => get_option( Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION, '' ),
469 Jetpack_SEO_Titles::TITLE_FORMATS_OPTION => get_option( Jetpack_SEO_Titles::TITLE_FORMATS_OPTION, array() ),
470 'api_cache' => $api_cache,
471 'posts_per_page' => (int) get_option( 'posts_per_page' ),
472 'posts_per_rss' => (int) get_option( 'posts_per_rss' ),
473 'rss_use_excerpt' => (bool) get_option( 'rss_use_excerpt' ),
474 'launchpad_screen' => (string) get_option( 'launchpad_screen' ),
475 'wpcom_newsletter_send_default' => (bool) get_option( 'wpcom_newsletter_send_default', true ),
476 'wpcom_featured_image_in_email' => ( function () use ( $site ) {
477 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
478 $registered_date = method_exists( $site, 'get_registered_date' ) ? $site->get_registered_date() : '';
479 // Compare to May 2, 2025 (ISO 8601 format)
480 if ( $registered_date && $registered_date !== '0000-00-00T00:00:00+00:00' && strtotime( $registered_date ) >= strtotime( '2025-05-02T00:00:00+00:00' ) ) {
481 return (bool) get_option( 'wpcom_featured_image_in_email', true );
482 }
483 }
484 // For all other sites, use the saved value or default to false for legacy behavior.
485 return (bool) get_option( 'wpcom_featured_image_in_email', false );
486 } )(),
487 'jetpack_gravatar_in_email' => (bool) get_option( 'jetpack_gravatar_in_email', true ),
488 'jetpack_author_in_email' => (bool) get_option( 'jetpack_author_in_email', true ),
489 'jetpack_post_date_in_email' => (bool) get_option( 'jetpack_post_date_in_email', true ),
490 'wpcom_newsletter_categories' => $newsletter_category_ids,
491 'wpcom_newsletter_categories_enabled' => (bool) get_option( 'wpcom_newsletter_categories_enabled' ),
492 'sm_enabled' => (bool) get_option( 'sm_enabled' ),
493 'jetpack_subscribe_overlay_enabled' => (bool) get_option( 'jetpack_subscribe_overlay_enabled' ),
494 'jetpack_subscribe_floating_button_enabled' => (bool) get_option( 'jetpack_subscribe_floating_button_enabled' ),
495 'jetpack_subscriptions_subscribe_post_end_enabled' => (bool) get_option( 'jetpack_subscriptions_subscribe_post_end_enabled' ),
496 'jetpack_subscriptions_login_navigation_enabled' => (bool) get_option( 'jetpack_subscriptions_login_navigation_enabled' ),
497 'jetpack_subscriptions_subscribe_navigation_enabled' => (bool) get_option( 'jetpack_subscriptions_subscribe_navigation_enabled' ),
498 'wpcom_gifting_subscription' => (bool) get_option( 'wpcom_gifting_subscription', $this->get_wpcom_gifting_subscription_default() ),
499 'wpcom_reader_views_enabled' => (bool) get_option( 'wpcom_reader_views_enabled', true ),
500 'wpcom_subscription_emails_use_excerpt' => (bool) get_option( 'wpcom_subscription_emails_use_excerpt' ),
501 'jetpack_subscriptions_reply_to' => (string) $this->get_subscriptions_reply_to_option(),
502 'jetpack_subscriptions_from_name' => (string) get_option( 'jetpack_subscriptions_from_name' ),
503 'show_on_front' => (string) get_option( 'show_on_front' ),
504 'page_on_front' => (string) get_option( 'page_on_front' ),
505 'page_for_posts' => (string) get_option( 'page_for_posts' ),
506 'subscription_options' => $this->get_subscription_options_in_user_locale(),
507 'supports_free_tier_customization' => true,
508 'jetpack_verbum_subscription_modal' => (bool) get_option( 'jetpack_verbum_subscription_modal', true ),
509 'enable_verbum_commenting' => (bool) get_option( 'enable_verbum_commenting', true ),
510 'enable_blocks_comments' => (bool) get_option( 'enable_blocks_comments', true ),
511 'highlander_comment_form_prompt' => $this->get_highlander_comment_form_prompt_option(),
512 'jetpack_comment_form_color_scheme' => (string) get_option( 'jetpack_comment_form_color_scheme' ),
513 'in_site_migration_flow' => (string) get_option( 'in_site_migration_flow', '' ),
514 'migration_source_site_domain' => (string) get_option( 'migration_source_site_domain' ),
515 'jetpack_waf_automatic_rules' => (bool) get_option( 'jetpack_waf_automatic_rules' ),
516 'jetpack_waf_ip_allow_list' => (string) get_option( 'jetpack_waf_ip_allow_list' ),
517 'jetpack_waf_ip_allow_list_enabled' => (bool) get_option( 'jetpack_waf_ip_allow_list_enabled' ),
518 'jetpack_waf_ip_block_list' => (string) get_option( 'jetpack_waf_ip_block_list' ),
519 'jetpack_waf_ip_block_list_enabled' => (bool) get_option( 'jetpack_waf_ip_block_list_enabled' ),
520 'jetpack_waf_share_data' => (bool) get_option( 'jetpack_waf_share_data' ),
521 'jetpack_waf_share_debug_data' => (bool) get_option( 'jetpack_waf_share_debug_data' ),
522 'jetpack_waf_automatic_rules_last_updated_timestamp' => (int) get_option( 'jetpack_waf_automatic_rules_last_updated_timestamp' ),
523 'is_fully_managed_agency_site' => (bool) get_option( 'is_fully_managed_agency_site' ),
524 'wpcom_hide_action_bar' => (bool) get_option( 'wpcom_hide_action_bar' ),
525 'mcp_abilities' => $mcp_abilities,
526 );
527
528 require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
529 if ( class_exists( 'Jetpack_Memberships' ) ) {
530 $response[ $key ]['newsletter_has_active_plan'] = count( Jetpack_Memberships::get_all_newsletter_plan_ids( false ) ) > 0;
531 // Read-only/derived: the free tier's markdown description rendered to
532 // safe HTML, colocated with subscription_options so it's
533 // read-after-write consistent. Not part of the writable
534 // subscription_options bag (which would round-trip and persist it).
535 $response[ $key ]['free_tier_description_rendered'] = Jetpack_Memberships::render_tier_description_html(
536 ( (array) get_option( 'subscription_options' ) )['free_tier_description'] ?? ''
537 );
538 }
539
540 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
541 $response[ $key ]['wpcom_publish_posts_with_markdown'] = (bool) WPCom_Markdown::get_instance()->is_posting_enabled();
542 $response[ $key ]['wpcom_publish_comments_with_markdown'] = (bool) WPCom_Markdown::get_instance()->is_commenting_enabled();
543
544 // WPCOM-specific Infinite Scroll Settings.
545 if ( is_callable( array( 'The_Neverending_Home_Page', 'get_settings' ) ) ) {
546 /**
547 * Clear the cached copy of widget info so it's pulled fresh from blog options.
548 * It was primed during the initial load under the __REST API site__'s context.
549 *
550 * @see wp_get_sidebars_widgets https://core.trac.wordpress.org/browser/trunk/src/wp-includes/widgets.php?rev=42374#L931
551 */
552 $GLOBALS['_wp_sidebars_widgets'] = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
553
554 $infinite_scroll_settings = The_Neverending_Home_Page::get_settings();
555 $response[ $key ]['infinite_scroll'] = get_option( 'infinite_scroll', true ) && 'scroll' === $infinite_scroll_settings->type;
556 if ( $infinite_scroll_settings->footer_widgets || 'click' === $infinite_scroll_settings->requested_type ) {
557 // The blog has footer widgets -- infinite scroll is blocked.
558 $response[ $key ]['infinite_scroll_blocked'] = 'footer';
559 } else {
560 $response[ $key ]['infinite_scroll_blocked'] = false;
561 }
562 }
563 }
564
565 // allow future versions of this endpoint to support additional settings keys.
566 /**
567 * Filter the current site setting in the returned response.
568 *
569 * @module json-api
570 *
571 * @since 3.9.3
572 * @since 13.6 Added the API object parameter.
573 *
574 * @param mixed $response_item A single site setting.
575 * @param WPCOM_JSON_API_Site_Settings_Endpoint $this The API object.
576 */
577 $response[ $key ] = apply_filters( 'site_settings_endpoint_get', $response[ $key ], $this );
578
579 if ( class_exists( 'Sharing_Service' ) ) {
580 $ss = new Sharing_Service();
581 $sharing = $ss->get_global_options();
582 $response[ $key ]['sharing_button_style'] = (string) $sharing['button_style'];
583 $response[ $key ]['sharing_label'] = (string) $sharing['sharing_label'];
584 $response[ $key ]['sharing_show'] = (array) $sharing['show'];
585 $response[ $key ]['sharing_open_links'] = (string) $sharing['open_links'];
586 }
587
588 $response[ $key ]['jetpack_protect_whitelist'] = Brute_Force_Protection_Shared_Functions::format_allow_list();
589
590 if ( ! current_user_can( 'edit_posts' ) ) {
591 unset( $response[ $key ] );
592 }
593 break;
594 }
595 }
596 return $response;
597 }
598
599 /**
600 * Get the default value for the wpcom_gifting_subscription option.
601 * The default value is the inverse of the plan's auto_renew setting.
602 *
603 * @return bool
604 */
605 protected function get_wpcom_gifting_subscription_default() {
606 if ( function_exists( 'wpcom_get_site_purchases' ) && function_exists( 'wpcom_purchase_has_feature' ) ) {
607 $purchases = wpcom_get_site_purchases();
608
609 foreach ( $purchases as $purchase ) {
610 if ( wpcom_purchase_has_feature( $purchase, \WPCOM_Features::SUBSCRIPTION_GIFTING ) ) {
611 /*
612 * We set default value as false when expiration date not match the following:
613 * - 54 days before the annual plan expiration.
614 * - 5 days before the monthly plan expiration.
615 * This is to match the gifting banner logic.
616 */
617 $days_of_warning = str_contains( $purchase->product_slug, 'monthly' ) ? 5 : 54;
618 $seconds_until_expiration = strtotime( $purchase->expiry_date ) - time();
619 if ( $seconds_until_expiration >= $days_of_warning * DAY_IN_SECONDS ) {
620 return false;
621 }
622
623 // We set default to the inverse of auto-renew.
624 if ( isset( $purchase->auto_renew ) ) {
625 return ! $purchase->auto_renew;
626 } elseif ( isset( $purchase->user_allows_auto_renew ) ) {
627 return ! $purchase->user_allows_auto_renew;
628 }
629 }
630 }
631 }
632 return false;
633 }
634
635 /**
636 * Reads `subscription_options` with the current user's locale active, to
637 * ensure that the defaults would be translated when displaying to the user
638 * or comparing the options before saving.
639 *
640 * @return array The `subscription_options` value, defaults included.
641 */
642 private function get_subscription_options_in_user_locale() {
643 $switched_locale = false;
644
645 if ( function_exists( 'wpcom_switch_to_user_locale' ) ) {
646 // Compare the locales before/after switch to decide if we should switch back
647 $locale_before = determine_locale();
648 // @phan-suppress-next-line PhanUndeclaredFunction -- Checked above. See also https://github.com/phan/phan/issues/1204.
649 wpcom_switch_to_user_locale();
650 $switched_locale = determine_locale() !== $locale_before;
651 }
652
653 // Resolve the defaults the same way get_option() does for a missing row (via the
654 // `default_option_*` filter with $passed_default = false), then let any stored
655 // sub-keys take precedence. Passing an array default keeps get_option() from
656 // re-populating the defaults, so a partial row stays partial before the merge.
657 $default_subscription_options = (array) apply_filters( 'default_option_subscription_options', array(), 'subscription_options', false );
658 $stored_subscription_options = (array) get_option( 'subscription_options', array() );
659 $subscription_options = array_merge( $default_subscription_options, $stored_subscription_options );
660
661 if ( $switched_locale ) {
662 restore_previous_locale();
663 }
664
665 return $subscription_options;
666 }
667
668 /**
669 * Get list of all site level MCP abilities.
670 *
671 * @return array
672 */
673 private function get_all_site_mcp_abilities(): array {
674 $all_abilities = array();
675 $ability_registry_file = WP_CONTENT_DIR . '/mu-plugins/wpcom-mcp/includes/AbilitiesRegistry/Registry/AbilityRegistry.php';
676 if ( file_exists( $ability_registry_file ) ) {
677 require_once $ability_registry_file;
678 // @phan-suppress-next-line PhanUndeclaredClassMethod
679 $abilities_resources = Automattic\WpcomMcp\AbilitiesRegistry\Registry\AbilityRegistry::get_resources_for_server( 'site-level' );
680 // @phan-suppress-next-line PhanUndeclaredClassMethod
681 $abilities_tools = Automattic\WpcomMcp\AbilitiesRegistry\Registry\AbilityRegistry::get_tools_for_server( 'site-level' );
682 // @phan-suppress-next-line PhanUndeclaredClassMethod
683 $abilities_prompts = Automattic\WpcomMcp\AbilitiesRegistry\Registry\AbilityRegistry::get_prompts_for_server( 'site-level' );
684 $all_abilities = array_merge( $abilities_resources, $abilities_tools, $abilities_prompts );
685 }
686 return apply_filters( 'jetpack_site_mcp_abilities', $all_abilities );
687 }
688
689 /**
690 * Get ability meta from config.
691 *
692 * @param string $ability_name Ability name, i.e. wpcom-mcp/posts-search.
693 *
694 * @return array
695 */
696 private function get_mcp_abilities_metadata( string $ability_name ): array {
697 $ability_meta = array();
698 $ability_registry_file = WP_CONTENT_DIR . '/mu-plugins/wpcom-mcp/includes/AbilitiesRegistry/Registry/AbilityRegistry.php';
699 if ( file_exists( $ability_registry_file ) ) {
700 require_once $ability_registry_file;
701 // @phan-suppress-next-line PhanUndeclaredClassMethod
702 $ability_meta = Automattic\WpcomMcp\AbilitiesRegistry\Registry\AbilityRegistry::get_metadata( $ability_name );
703 }
704 return apply_filters( 'jetpack_site_mcp_ability_meta', $ability_meta, $ability_name );
705 }
706
707 /**
708 * Get MCP abilities for the current site.
709 *
710 * @return array
711 */
712 public function get_site_mcp_abilities(): array {
713 $current_mcp_abilities = get_option( 'mcp_abilities', array() );
714 if ( ! is_array( $current_mcp_abilities ) ) {
715 $current_mcp_abilities = array();
716 }
717
718 $all_abilities = $this->get_all_site_mcp_abilities();
719 if ( empty( $all_abilities ) ) {
720 return array();
721 }
722
723 $computed_abilities = array();
724 foreach ( $all_abilities as $ability_name ) {
725 // Get base metadata first
726 $ability_meta = $this->get_mcp_abilities_metadata( $ability_name );
727 if ( ! empty( $ability_meta ) ) {
728 // Use stored value or fall back to metadata default
729 $enabled = $current_mcp_abilities[ $ability_name ] ?? $ability_meta['enabled'] ?? false;
730
731 $computed_abilities[ $ability_name ] = array(
732 'name' => $ability_name,
733 'title' => $ability_meta['title'] ?? '',
734 'description' => $ability_meta['description'] ?? '',
735 'category' => $ability_meta['category'] ?? '',
736 'type' => $ability_meta['type'] ?? '',
737 'enabled' => (bool) $enabled,
738 );
739 }
740 }
741 return $computed_abilities;
742 }
743
744 /**
745 * Sets the MCP abilities for the current site.
746 *
747 * @param mixed $value MCP abilities array.
748 *
749 * @return true|WP_Error
750 */
751 public function set_site_mcp_abilities( $value ) {
752 // Validate input format
753 if ( ! is_array( $value ) ) {
754 return new WP_Error( 'invalid_format', __( 'Site MCP abilities must be an array', 'jetpack' ) );
755 }
756
757 $all_abilities = $this->get_all_site_mcp_abilities();
758
759 // Filter ability names that don't exist
760 $value = array_filter(
761 $value,
762 function ( $ability_name ) use ( $all_abilities ) {
763 return in_array( $ability_name, $all_abilities, true );
764 },
765 ARRAY_FILTER_USE_KEY
766 );
767
768 // Validate each ability exists and value is boolean-like
769 foreach ( $value as $ability_name => $enabled ) {
770 if ( ! is_string( $ability_name ) || ( ! WPCOM_JSON_API::is_truthy( $enabled ) && ! WPCOM_JSON_API::is_falsy( $enabled ) ) ) {
771 $error_message = sprintf(
772 // Translators: %s is an MCP ability name
773 __( 'Invalid ability: %s', 'jetpack' ),
774 $ability_name
775 );
776 return new WP_Error( 'invalid_ability', $error_message );
777 }
778 }
779
780 update_option( 'mcp_abilities', $value );
781
782 return true;
783 }
784
785 /**
786 * Get locale.
787 *
788 * @param string $key Language.
789 */
790 protected function get_locale( $key ) {
791 if ( 'lang' === $key ) {
792 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
793 return (string) get_blog_lang_code();
794 } else {
795 return get_locale();
796 }
797 }
798
799 return false;
800 }
801
802 /**
803 * Updates site settings for authorized users
804 *
805 * @return array|WP_Error
806 */
807 public function update_settings() {
808 /*
809 * $this->input() retrieves posted arguments whitelisted and casted to the $request_format
810 * specs that get passed in when this class is instantiated
811 */
812 $input = $this->input();
813 $unfiltered_input = $this->input( false, false );
814 /**
815 * Filters the settings to be updated on the site.
816 *
817 * @module json-api
818 *
819 * @since 3.6.0
820 * @since 6.1.1 Added $unfiltered_input parameter.
821 *
822 * @param array $input Associative array of site settings to be updated.
823 * Cast and filtered based on documentation.
824 * @param array $unfiltered_input Associative array of site settings to be updated.
825 * Neither cast nor filtered. Contains raw input.
826 */
827 $input = apply_filters( 'rest_api_update_site_settings', $input, $unfiltered_input );
828
829 $blog_id = get_current_blog_id();
830
831 $jetpack_relatedposts_options = array();
832 $sharing_options = array();
833 $updated = array();
834
835 if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
836 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
837 }
838
839 foreach ( $input as $key => $value ) {
840
841 if ( ! is_array( $value ) ) {
842 $value = trim( $value );
843 }
844
845 // preserve the raw value before unslashing the value. The slashes need to be preserved for date and time formats.
846 $raw_value = $value;
847 $value = wp_unslash( $value );
848
849 switch ( $key ) {
850
851 case 'default_ping_status':
852 case 'default_comment_status':
853 // settings are stored as closed|open.
854 $coerce_value = ( $value ) ? 'open' : 'closed';
855 if ( update_option( $key, $coerce_value ) ) {
856 $updated[ $key ] = $value;
857 }
858 break;
859 case 'launchpad_screen':
860 if ( in_array( $value, array( 'full', 'off', 'minimized' ), true ) ) {
861 if ( update_option( $key, $value ) ) {
862 $updated[ $key ] = $value;
863 }
864 }
865 break;
866 case 'jetpack_protect_whitelist':
867 if ( class_exists( 'Brute_Force_Protection_Shared_Functions' ) ) {
868 $result = Brute_Force_Protection_Shared_Functions::save_allow_list( $value );
869 if ( is_wp_error( $result ) ) {
870 return $result;
871 }
872 $updated[ $key ] = Brute_Force_Protection_Shared_Functions::format_allow_list();
873 }
874 break;
875 case 'jetpack_sync_non_public_post_stati':
876 Jetpack_Options::update_option( 'sync_non_public_post_stati', $value );
877 break;
878 case 'jetpack_search_enabled':
879 if ( $value ) {
880 Jetpack::activate_module( $blog_id, 'search' );
881 } else {
882 // @phan-suppress-next-line PhanParamTooMany -- Phan doesn't know about the WP.com variant of the Jetpack class.
883 Jetpack::deactivate_module( $blog_id, 'search' );
884 }
885 $updated[ $key ] = (bool) $value;
886 break;
887 case 'jetpack_relatedposts_enabled':
888 case 'jetpack_relatedposts_show_context':
889 case 'jetpack_relatedposts_show_date':
890 case 'jetpack_relatedposts_show_thumbnails':
891 case 'jetpack_relatedposts_show_headline':
892 if ( ! $this->jetpack_relatedposts_supported() ) {
893 break;
894 }
895 if ( 'jetpack_relatedposts_enabled' === $key ) {
896 if ( $value ) {
897 Jetpack::activate_module( $blog_id, 'related-posts' );
898 } else {
899 // @phan-suppress-next-line PhanParamTooMany -- Phan doesn't know about the WP.com variant of the Jetpack class.
900 Jetpack::deactivate_module( $blog_id, 'related-posts' );
901 }
902 }
903 $just_the_key = substr( $key, 21 );
904 $jetpack_relatedposts_options[ $just_the_key ] = $value;
905 break;
906
907 case 'social_notifications_like':
908 case 'social_notifications_reblog':
909 case 'social_notifications_subscribe':
910 // settings are stored as on|off.
911 $coerce_value = ( $value ) ? 'on' : 'off';
912 if ( update_option( $key, $coerce_value ) ) {
913 $updated[ $key ] = $value;
914 }
915 break;
916
917 case 'cloudflare_analytics':
918 if ( ! isset( $value['code'] ) || ! preg_match( '/^$|^[a-fA-F0-9]+$/i', $value['code'] ) ) {
919 return new WP_Error( 'invalid_code', __( 'Invalid Cloudflare Analytics ID', 'jetpack' ) );
920 }
921
922 if ( update_option( $key, $value ) ) {
923 $updated[ $key ] = $value;
924 }
925 break;
926
927 case 'jetpack_testimonial':
928 case 'jetpack_portfolio':
929 case 'jetpack_comment_likes_enabled':
930 case 'wpcom_reader_views_enabled':
931 case 'jetpack_verbum_subscription_modal':
932 // settings are stored as 1|0.
933 $coerce_value = (int) $value;
934 if ( update_option( $key, $coerce_value ) ) {
935 $updated[ $key ] = (bool) $value;
936 }
937 break;
938
939 case 'jetpack_testimonial_posts_per_page':
940 case 'jetpack_portfolio_posts_per_page':
941 // settings are stored as numeric.
942 $coerce_value = (int) $value;
943 if ( update_option( $key, $coerce_value ) ) {
944 $updated[ $key ] = $coerce_value;
945 }
946 break;
947
948 // Sharing options.
949 case 'sharing_button_style':
950 case 'sharing_show':
951 case 'sharing_open_links':
952 $sharing_options[ preg_replace( '/^sharing_/', '', $key ) ] = $value;
953 break;
954 case 'sharing_label':
955 $sharing_options[ $key ] = $value;
956 break;
957
958 // Keyring token option.
959 case 'eventbrite_api_token':
960 // These options can only be updated for sites hosted on WordPress.com.
961 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
962 if ( empty( $value ) || WPCOM_JSON_API::is_falsy( $value ) ) {
963 if ( delete_option( $key ) ) {
964 $updated[ $key ] = null;
965 }
966 } elseif ( update_option( $key, $value ) ) {
967 $updated[ $key ] = (int) $value;
968 }
969 }
970 break;
971
972 case 'api_cache':
973 if ( empty( $value ) || WPCOM_JSON_API::is_falsy( $value ) ) {
974 if ( delete_option( 'jetpack_api_cache_enabled' ) ) {
975 $updated[ $key ] = false;
976 }
977 } elseif ( update_option( 'jetpack_api_cache_enabled', true ) ) {
978 $updated[ $key ] = true;
979 }
980 break;
981
982 case 'timezone_string':
983 /*
984 * Map UTC+- timezones to gmt_offsets and set timezone_string to empty
985 * https://github.com/WordPress/WordPress/blob/4.4.2/wp-admin/options.php#L175
986 */
987 if ( ! empty( $value ) && preg_match( '/^UTC[+-]/', $value ) ) {
988 $gmt_offset = preg_replace( '/UTC\+?/', '', $value );
989 if ( update_option( 'gmt_offset', $gmt_offset ) ) {
990 $updated['gmt_offset'] = $gmt_offset;
991 }
992
993 $value = '';
994 }
995
996 /*
997 * Always set timezone_string either with the given value or with an
998 * empty string
999 */
1000 if ( update_option( $key, $value ) ) {
1001 $updated[ $key ] = $value;
1002 }
1003 break;
1004
1005 case 'subscription_options':
1006 if ( ! is_array( $value ) ) {
1007 break;
1008 }
1009
1010 $allowed_keys = array( 'invitation', 'comment_follow', 'welcome', 'subscribe_modal_heading', 'free_tier_description', 'hide_free_tier' );
1011 $filtered_value = array_filter(
1012 $value,
1013 function ( $key ) use ( $allowed_keys ) {
1014 return in_array( $key, $allowed_keys, true );
1015 },
1016 ARRAY_FILTER_USE_KEY
1017 );
1018
1019 if ( empty( $filtered_value ) ) {
1020 break;
1021 }
1022
1023 // `hide_free_tier` is a boolean flag, so pull it out before the HTML
1024 // sanitization below (which expects strings). Parse it with is_truthy()
1025 // so stringy booleans (e.g. "false", "0") are interpreted correctly
1026 // rather than being treated as truthy by a plain `! empty()`.
1027 $has_hide_free_tier = array_key_exists( 'hide_free_tier', $filtered_value );
1028 $hide_free_tier = $has_hide_free_tier && WPCOM_JSON_API::is_truthy( $filtered_value['hide_free_tier'] );
1029 unset( $filtered_value['hide_free_tier'] );
1030
1031 array_walk_recursive(
1032 $filtered_value,
1033 function ( &$value ) {
1034 $value = wp_kses(
1035 $value,
1036 array(
1037 'a' => array(
1038 'href' => array(),
1039 ),
1040 )
1041 );
1042 }
1043 );
1044
1045 // Normalize whitespace-only `subscribe_modal_heading` input to empty so
1046 // the modal template's `empty()` fallback fires. PHP's `empty()` treats
1047 // `" "` as non-empty, which would otherwise render a blank heading.
1048 if ( isset( $filtered_value['subscribe_modal_heading'] ) ) {
1049 $filtered_value['subscribe_modal_heading'] = trim( $filtered_value['subscribe_modal_heading'] );
1050 }
1051
1052 // The free tier description is stored as plain markdown source, so strip
1053 // all HTML and cap its length to match the paid-tier description field.
1054 // WordPress core guarantees mb_substr() (polyfilled in wp-includes/compat.php
1055 // when the mbstring extension is unavailable), so it's safe to use directly.
1056 // A JSON payload could supply a non-scalar (array/object) for this field,
1057 // which would fatal in wp_kses()/mb_substr() on PHP 8+, so drop invalid values.
1058 if ( isset( $filtered_value['free_tier_description'] ) ) {
1059 if ( is_scalar( $filtered_value['free_tier_description'] ) ) {
1060 $filtered_value['free_tier_description'] = mb_substr( wp_kses( (string) $filtered_value['free_tier_description'], array() ), 0, 500 );
1061 } else {
1062 unset( $filtered_value['free_tier_description'] );
1063 }
1064 }
1065
1066 if ( $has_hide_free_tier ) {
1067 $filtered_value['hide_free_tier'] = $hide_free_tier;
1068 }
1069
1070 // Clients that render the settings form tend to post the whole
1071 // `subscription_options` bag back, including sub-keys the user never
1072 // touched. This could result in a translated value inadvertently
1073 // saved in the database.
1074 // Get the value from db or the default options populated by filter.
1075 $current_subscription_options = $this->get_subscription_options_in_user_locale();
1076 $changed_subscription_options = array();
1077
1078 foreach ( $filtered_value as $subscription_option_key => $subscription_option_value ) {
1079 $current_subscription_option = $current_subscription_options[ $subscription_option_key ] ?? null;
1080
1081 // The incoming value has already been through wp_kses() above, and
1082 // wp_kses() is not guaranteed to be byte-preserving — it rewrites
1083 // attribute quoting, and differently across WordPress versions. Put the
1084 // current value through the same pass so the comparison reflects a real
1085 // edit rather than a sanitizer rewrite; without this, a default carrying
1086 // markup (`invitation`) never compares equal and is persisted on every
1087 // save. Safe to apply to an already-sanitized value: the pass is
1088 // idempotent.
1089 if ( is_string( $current_subscription_option ) ) {
1090 $current_subscription_option = wp_kses(
1091 $current_subscription_option,
1092 array(
1093 'a' => array(
1094 'href' => array(),
1095 ),
1096 )
1097 );
1098 }
1099
1100 // A sub-key the site has never stored reads as unset everywhere this
1101 // option is consumed, so an empty incoming value for it is not a change.
1102 if (
1103 null === $current_subscription_option
1104 && ( '' === $subscription_option_value || false === $subscription_option_value )
1105 ) {
1106 continue;
1107 }
1108
1109 if ( $current_subscription_option === $subscription_option_value ) {
1110 continue;
1111 }
1112
1113 $changed_subscription_options[ $subscription_option_key ] = $subscription_option_value;
1114 }
1115
1116 if ( empty( $changed_subscription_options ) ) {
1117 break;
1118 }
1119
1120 // Get the value from the database or an empty array.
1121 $old_subscription_options = get_option( 'subscription_options', array() );
1122 $new_subscription_options = array_merge( $old_subscription_options, $changed_subscription_options );
1123
1124 if ( update_option( $key, $new_subscription_options ) ) {
1125 $updated[ $key ] = $changed_subscription_options;
1126 }
1127 break;
1128
1129 case 'woocommerce_onboarding_profile':
1130 // Allow boolean values but sanitize_text_field everything else.
1131 $sanitized_value = (array) $value;
1132 array_walk_recursive(
1133 $sanitized_value,
1134 function ( &$value ) {
1135 if ( ! is_bool( $value ) ) {
1136 $value = sanitize_text_field( $value );
1137 }
1138 }
1139 );
1140 if ( update_option( $key, $sanitized_value ) ) {
1141 $updated[ $key ] = $sanitized_value;
1142 }
1143 break;
1144
1145 case 'woocommerce_store_address':
1146 case 'woocommerce_store_address_2':
1147 case 'woocommerce_store_city':
1148 case 'woocommerce_default_country':
1149 case 'woocommerce_store_postcode':
1150 $sanitized_value = sanitize_text_field( $value );
1151 if ( update_option( $key, $sanitized_value ) ) {
1152 $updated[ $key ] = $sanitized_value;
1153 }
1154 break;
1155
1156 case 'date_format':
1157 case 'time_format':
1158 // settings are stored as strings.
1159 // raw_value is used to help preserve any escaped characters that might exist in the formatted string.
1160 $sanitized_value = sanitize_text_field( $raw_value );
1161 if ( update_option( $key, $sanitized_value ) ) {
1162 $updated[ $key ] = $sanitized_value;
1163 }
1164 break;
1165
1166 case 'start_of_week':
1167 // setting is stored as int in 0-6 range (days of week).
1168 $coerce_value = (int) $value;
1169 $limit_value = ( $coerce_value >= 0 && $coerce_value <= 6 ) ? $coerce_value : 0;
1170 if ( update_option( $key, $limit_value ) ) {
1171 $updated[ $key ] = $limit_value;
1172 }
1173 break;
1174
1175 case 'site_icon':
1176 /*
1177 * settings are stored as deletable numeric (all empty
1178 * values as delete intent), validated as media image
1179 */
1180 if ( empty( $value ) || WPCOM_JSON_API::is_falsy( $value ) ) {
1181 /**
1182 * Fallback mechanism to clear a third party site icon setting. Can be used
1183 * to unset the option when an API request instructs the site to remove the site icon.
1184 *
1185 * @module json-api
1186 *
1187 * @since 4.10
1188 */
1189 if ( delete_option( $key ) || apply_filters( 'rest_api_site_icon_cleared', false ) ) {
1190 $updated[ $key ] = null;
1191 }
1192 } elseif ( is_numeric( $value ) ) {
1193 $coerce_value = (int) $value;
1194 if ( wp_attachment_is_image( $coerce_value ) && update_option( $key, $coerce_value ) ) {
1195 $updated[ $key ] = $coerce_value;
1196 }
1197 }
1198 break;
1199
1200 case Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION:
1201 if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() && ! Jetpack_SEO_Utils::has_legacy_front_page_meta() ) {
1202 return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 );
1203 }
1204
1205 if ( ! is_string( $value ) ) {
1206 return new WP_Error( 'invalid_input', __( 'Invalid SEO meta description value.', 'jetpack' ), 400 );
1207 }
1208
1209 $new_description = Jetpack_SEO_Utils::update_front_page_meta_description( $value );
1210
1211 if ( ! empty( $new_description ) ) {
1212 $updated[ $key ] = $new_description;
1213 }
1214 break;
1215
1216 case Jetpack_SEO_Titles::TITLE_FORMATS_OPTION:
1217 if ( ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
1218 if ( Jetpack_SEO_Utils::has_legacy_front_page_meta() ) {
1219 break;
1220 }
1221 return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 );
1222 }
1223
1224 if ( ! Jetpack_SEO_Titles::are_valid_title_formats( $value ) ) {
1225 return new WP_Error( 'invalid_input', __( 'Invalid SEO title format.', 'jetpack' ), 400 );
1226 }
1227
1228 $new_title_formats = Jetpack_SEO_Titles::update_title_formats( $value );
1229
1230 if ( ! empty( $new_title_formats ) ) {
1231 $updated[ $key ] = $new_title_formats;
1232 }
1233 break;
1234
1235 case 'verification_services_codes':
1236 $verification_codes = jetpack_verification_validate( $value );
1237
1238 if ( update_option( 'verification_services_codes', $verification_codes ) ) {
1239 $updated[ $key ] = $verification_codes;
1240 }
1241 break;
1242
1243 case 'wpcom_publish_posts_with_markdown':
1244 case 'wpcom_publish_comments_with_markdown':
1245 $coerce_value = (bool) $value;
1246 if ( update_option( $key, $coerce_value ) ) {
1247 $updated[ $key ] = $coerce_value;
1248 }
1249 break;
1250
1251 case 'wpcom_gifting_subscription':
1252 $coerce_value = (bool) $value;
1253
1254 /*
1255 * get_option returns a boolean false if the option doesn't exist, otherwise it always returns
1256 * a serialized value. Knowing that we can check if the option already exists.
1257 */
1258 $gift_toggle = get_option( $key );
1259 if ( false === $gift_toggle ) {
1260 // update_option will not create a new option if the initial value is false. So use add_option.
1261 if ( add_option( $key, $coerce_value ) ) {
1262 $updated[ $key ] = $coerce_value;
1263 }
1264 } elseif ( update_option( $key, $coerce_value ) ) { // If the option already exists use update_option.
1265 $updated[ $key ] = $coerce_value;
1266 }
1267 break;
1268
1269 case 'rss_use_excerpt':
1270 $sanitized_value = (int) (bool) $value;
1271 update_option( $key, $sanitized_value );
1272 $updated[ $key ] = $sanitized_value;
1273 break;
1274
1275 case 'wpcom_subscription_emails_use_excerpt':
1276 update_option( 'wpcom_subscription_emails_use_excerpt', (bool) $value );
1277 $updated[ $key ] = (bool) $value;
1278 break;
1279
1280 case 'jetpack_subscriptions_reply_to':
1281 require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
1282 $to_set_value = Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value )
1283 ? (string) $value
1284 : Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to;
1285
1286 if ( update_option( $key, $to_set_value ) ) {
1287 $updated[ $key ] = $to_set_value;
1288 }
1289 break;
1290
1291 case 'jetpack_subscriptions_from_name':
1292 $sanitized_value = sanitize_text_field( $value );
1293 if ( update_option( $key, $sanitized_value ) ) {
1294 $updated[ $key ] = $sanitized_value;
1295 }
1296 break;
1297
1298 case 'instant_search_enabled':
1299 update_option( 'instant_search_enabled', (bool) $value );
1300 $updated[ $key ] = (bool) $value;
1301 break;
1302
1303 case 'lang_id':
1304 /*
1305 * Due to the fact that locale variants are set in a locale_variant option,
1306 * changing locale from variant to primary
1307 * would look like the same lang_id is being saved and update_option would return false,
1308 * even though the correct options would be set by pre_update_option_lang_id,
1309 * so we should always return lang_id as updated.
1310 */
1311 update_option( 'lang_id', (int) $value );
1312 $updated[ $key ] = (int) $value;
1313 break;
1314
1315 case 'wpcom_newsletter_send_default':
1316 update_option( 'wpcom_newsletter_send_default', (int) (bool) $value );
1317 $updated[ $key ] = (int) (bool) $value;
1318 break;
1319
1320 case 'wpcom_featured_image_in_email':
1321 update_option( 'wpcom_featured_image_in_email', (int) (bool) $value );
1322 $updated[ $key ] = (int) (bool) $value;
1323 break;
1324
1325 case Jetpack_Newsletter_Category_Helper::NEWSLETTER_CATEGORIES_OPTION:
1326 $update_newsletter_categories = Jetpack_Newsletter_Category_Helper::save_category_ids( (array) $value );
1327 if ( $update_newsletter_categories ) {
1328 $updated[ $key ] = $update_newsletter_categories;
1329 }
1330
1331 break;
1332
1333 case 'wpcom_newsletter_categories_enabled':
1334 update_option( 'wpcom_newsletter_categories_enabled', (int) (bool) $value );
1335 $updated[ $key ] = (int) (bool) $value;
1336 break;
1337
1338 case 'sm_enabled':
1339 update_option( 'sm_enabled', (int) (bool) $value );
1340 $updated[ $key ] = (int) (bool) $value;
1341 break;
1342
1343 case 'jetpack_subscribe_overlay_enabled':
1344 update_option( 'jetpack_subscribe_overlay_enabled', (int) (bool) $value );
1345 $updated[ $key ] = (int) (bool) $value;
1346 break;
1347
1348 case 'jetpack_subscribe_floating_button_enabled':
1349 update_option( 'jetpack_subscribe_floating_button_enabled', (int) (bool) $value );
1350 $updated[ $key ] = (int) (bool) $value;
1351 break;
1352
1353 case 'jetpack_subscriptions_subscribe_post_end_enabled':
1354 update_option( 'jetpack_subscriptions_subscribe_post_end_enabled', (int) (bool) $value );
1355 $updated[ $key ] = (int) (bool) $value;
1356 break;
1357
1358 case 'jetpack_subscriptions_login_navigation_enabled':
1359 update_option( 'jetpack_subscriptions_login_navigation_enabled', (int) (bool) $value );
1360 $updated[ $key ] = (int) (bool) $value;
1361 break;
1362
1363 case 'jetpack_subscriptions_subscribe_navigation_enabled':
1364 update_option( 'jetpack_subscriptions_subscribe_navigation_enabled', (int) (bool) $value );
1365 $updated[ $key ] = (int) (bool) $value;
1366 break;
1367
1368 case 'show_on_front':
1369 if ( in_array( $value, array( 'page', 'posts' ), true ) && update_option( $key, $value ) ) {
1370 $updated[ $key ] = $value;
1371 }
1372 break;
1373
1374 case 'page_on_front':
1375 case 'page_for_posts':
1376 if ( $value === '' ) { // empty function is not applicable here because '0' may be a valid page id
1377 if ( delete_option( $key ) ) {
1378 $updated[ $key ] = null;
1379 }
1380
1381 break;
1382 }
1383
1384 if ( ! $this->is_valid_page_id( $value ) ) {
1385 break;
1386 }
1387
1388 $related_option_key = $key === 'page_on_front' ? 'page_for_posts' : 'page_on_front';
1389 $related_option_value = get_option( $related_option_key );
1390 if ( $related_option_value === $value ) {
1391 // page_on_front and page_for_posts are not allowed to be the same
1392 break;
1393 }
1394
1395 if ( update_option( $key, $value ) ) {
1396 $updated[ $key ] = $value;
1397 }
1398
1399 break;
1400
1401 case 'in_site_migration_flow':
1402 if ( empty( $value ) ) {
1403 delete_option( 'in_site_migration_flow' );
1404 break;
1405 }
1406
1407 $migration_flow_whitelist = array(
1408 'site-migration',
1409 'migration-signup',
1410 );
1411
1412 if ( ! in_array( $value, $migration_flow_whitelist, true ) ) {
1413 break;
1414 }
1415
1416 update_option( 'in_site_migration_flow', $value );
1417 $updated[ $key ] = $value;
1418 break;
1419
1420 case 'migration_source_site_domain':
1421 // If we get an empty value, delete the option
1422 if ( empty( $value ) ) {
1423 delete_option( 'migration_source_site_domain' );
1424 break;
1425 }
1426
1427 // If we get a non-url value, don't update the option.
1428 if ( wp_http_validate_url( $value ) === false ) {
1429 break;
1430 }
1431
1432 update_option( 'migration_source_site_domain', $value );
1433 $updated[ $key ] = $value;
1434 break;
1435
1436 case 'is_fully_managed_agency_site':
1437 case 'wpcom_hide_action_bar':
1438 $coerce_value = (int) (bool) $value;
1439 if ( update_option( $key, $coerce_value ) ) {
1440 $updated[ $key ] = (bool) $coerce_value;
1441 }
1442 break;
1443
1444 case 'mcp_abilities':
1445 $result = $this->set_site_mcp_abilities( $value );
1446 if ( is_wp_error( $result ) ) {
1447 return $result;
1448 }
1449 $updated[ $key ] = $this->get_site_mcp_abilities();
1450 break;
1451
1452 default:
1453 // allow future versions of this endpoint to support additional settings keys.
1454 if ( has_filter( 'site_settings_endpoint_update_' . $key ) ) {
1455 /**
1456 * Filter current site setting value to be updated.
1457 *
1458 * @module json-api
1459 *
1460 * @since 3.9.3
1461 * @since 13.6 Added the API object parameter.
1462 *
1463 * @param mixed $response_item A single site setting value.
1464 * @param WPCOM_JSON_API_Site_Settings_Endpoint The API object parameter.
1465 */
1466 $value = apply_filters( 'site_settings_endpoint_update_' . $key, $value, $this );
1467
1468 if ( is_wp_error( $value ) ) {
1469 return $value;
1470 }
1471
1472 if ( $value ) {
1473 $updated[ $key ] = $value;
1474 }
1475 break;
1476 }
1477 // no worries, we've already whitelisted and casted arguments above.
1478 if ( update_option( $key, $value ) ) {
1479 $updated[ $key ] = $value;
1480 }
1481 }
1482 }
1483
1484 if ( $jetpack_relatedposts_options !== array() ) {
1485 // track new jetpack_relatedposts options against old.
1486 $old_relatedposts_options = Jetpack_Options::get_option( 'relatedposts' );
1487
1488 $jetpack_relatedposts_options_to_save = $old_relatedposts_options;
1489 foreach ( $jetpack_relatedposts_options as $key => $value ) {
1490 $jetpack_relatedposts_options_to_save[ $key ] = $value;
1491 }
1492
1493 if ( Jetpack_Options::update_option( 'relatedposts', $jetpack_relatedposts_options_to_save ) ) {
1494 foreach ( $jetpack_relatedposts_options as $key => $value ) {
1495 if ( in_array( $key, array( 'show_context', 'show_date' ), true ) ) {
1496 $has_initialized_option = ! isset( $old_relatedposts_options[ $key ] ) && $value;
1497 $has_updated_option = isset( $old_relatedposts_options[ $key ] ) && $value !== $old_relatedposts_options[ $key ];
1498
1499 if ( $has_initialized_option || $has_updated_option ) {
1500 $updated[ 'jetpack_relatedposts_' . $key ] = (bool) $value;
1501 }
1502 } elseif ( isset( $old_relatedposts_options[ $key ] ) && $value !== $old_relatedposts_options[ $key ] ) {
1503 $updated[ 'jetpack_relatedposts_' . $key ] = $value;
1504 }
1505 }
1506 }
1507 }
1508
1509 if ( ! empty( $sharing_options ) && class_exists( 'Sharing_Service' ) ) {
1510 $ss = new Sharing_Service();
1511
1512 /*
1513 * Merge current values with updated, since Sharing_Service expects
1514 * all values to be included when updating
1515 */
1516 $current_sharing_options = $ss->get_global_options();
1517 foreach ( $current_sharing_options as $key => $val ) {
1518 if ( ! isset( $sharing_options[ $key ] ) ) {
1519 $sharing_options[ $key ] = $val;
1520 }
1521 }
1522
1523 $updated_social_options = $ss->set_global_options( $sharing_options );
1524
1525 if ( isset( $input['sharing_button_style'] ) ) {
1526 $updated['sharing_button_style'] = (string) $updated_social_options['button_style'];
1527 }
1528 if ( isset( $input['sharing_label'] ) ) {
1529 // Sharing_Service won't report label as updated if set to default.
1530 $updated['sharing_label'] = (string) $sharing_options['sharing_label'];
1531 }
1532 if ( isset( $input['sharing_show'] ) ) {
1533 $updated['sharing_show'] = (array) $updated_social_options['show'];
1534 }
1535 if ( isset( $input['sharing_open_links'] ) ) {
1536 $updated['sharing_open_links'] = (string) $updated_social_options['open_links'];
1537 }
1538 }
1539
1540 return array(
1541 'updated' => $updated,
1542 );
1543 }
1544
1545 /**
1546 * Get the string value of the jetpack_subscriptions_reply_to option.
1547 * When the option is not set, it will retun 'no-reply'.
1548 *
1549 * @return string
1550 */
1551 protected function get_subscriptions_reply_to_option() {
1552 $reply_to = get_option( 'jetpack_subscriptions_reply_to', null );
1553 if ( $reply_to === null ) {
1554 require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
1555 return Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to;
1556 }
1557 return $reply_to;
1558 }
1559
1560 /**
1561 * Check if the given value is a valid page ID for the current site.
1562 *
1563 * @param mixed $value The value to check.
1564 * @return bool True if the value is a valid page ID for the current site, false otherwise.
1565 */
1566 protected function is_valid_page_id( $value ) {
1567 $all_page_ids = get_all_page_ids();
1568
1569 $valid_page_id = false;
1570 foreach ( $all_page_ids as $page_id ) {
1571 if ( $page_id === (string) $value ) {
1572 $valid_page_id = true;
1573 break;
1574 }
1575 }
1576
1577 return $valid_page_id;
1578 }
1579
1580 /**
1581 * Get the value of the highlander_comment_form_prompt option.
1582 * When the option is not set, it will return the default value.
1583 *
1584 * @return string
1585 */
1586 protected function get_highlander_comment_form_prompt_option() {
1587 $highlander_comment_form_prompt_option = get_option( 'highlander_comment_form_prompt' );
1588
1589 if ( empty( $highlander_comment_form_prompt_option ) ) {
1590 return (string) __( 'Leave a comment', 'jetpack' );
1591 }
1592
1593 return (string) $highlander_comment_form_prompt_option;
1594 }
1595 }
1596