PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
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 / modules / subscriptions / abilities / class-newsletter-abilities.php
jetpack / modules / subscriptions / abilities Last commit date
class-newsletter-abilities.php 1 month ago
class-newsletter-abilities.php
543 lines
1 <?php
2 /**
3 * Jetpack Newsletter Abilities Registration
4 *
5 * Registers Jetpack Newsletter (subscriptions) abilities with the WordPress
6 * Abilities API.
7 *
8 * @package automattic/jetpack
9 */
10
11 namespace Automattic\Jetpack\Plugin\Abilities;
12
13 use Automattic\Jetpack\Connection\Client;
14 use Automattic\Jetpack\Modules\Subscriptions\Settings as Subscriptions_Settings;
15 use Automattic\Jetpack\WP_Abilities\Registrar;
16 use Jetpack;
17 use Jetpack_Options;
18
19 // The Subscriptions module doesn't load its Settings helpers eagerly. Pull it
20 // in here so `Subscriptions_Settings::$default_reply_to` and
21 // `is_valid_reply_to()` are resolvable when the abilities run.
22 require_once __DIR__ . '/../class-settings.php';
23
24 /**
25 * Registers Jetpack Newsletter abilities with the WordPress Abilities API.
26 *
27 * Exposes a consolidated read of the site's newsletter (subscriptions) settings
28 * and a partial-update writer so AI agents can configure the Newsletter module
29 * through the standard `wp-abilities/v1` REST surface.
30 */
31 class Newsletter_Abilities extends Registrar {
32
33 // Field type tags used in `settings_map()`. Constants (not strings) so a
34 // typo in a `case` label fails fast instead of silently falling through.
35 private const TYPE_BOOL = 'bool';
36 private const TYPE_ON_OFF = 'on_off';
37 private const TYPE_ENUM = 'enum';
38 private const TYPE_STRING = 'string';
39
40 /**
41 * Allowed values for the `reply_to` setting. Mirror of
42 * `Subscriptions_Settings::is_valid_reply_to()` — kept here so it can be
43 * referenced from the JSON Schema enum without loading the Settings class
44 * at file-parse time.
45 */
46 private const REPLY_TO_VALUES = array( 'comment', 'author', 'no-reply' );
47
48 /**
49 * Returns the abilities category, definition, or registered abilities.
50 *
51 * @inheritDoc
52 */
53 public static function get_category_slug(): string {
54 return 'jetpack-newsletter';
55 }
56
57 /**
58 * Returns the abilities category, definition, or registered abilities.
59 *
60 * @inheritDoc
61 */
62 public static function get_category_definition(): array {
63 return array(
64 // "Jetpack" and "Newsletter" are product names and should not be translated.
65 'label' => 'Jetpack Newsletter',
66 'description' => __( 'Abilities for reading and updating Jetpack Newsletter settings.', 'jetpack' ),
67 );
68 }
69
70 /**
71 * Returns the abilities category, definition, or registered abilities.
72 *
73 * @inheritDoc
74 */
75 public static function get_abilities(): array {
76 $settings_object_schema = array(
77 'type' => 'object',
78 'additionalProperties' => false,
79 'properties' => array(
80 'subscribe_post_end_enabled' => array(
81 'type' => 'boolean',
82 'description' => __( 'Show a "subscribe to blog" checkbox at the end of every post. Default true.', 'jetpack' ),
83 ),
84 'subscribe_comments_enabled' => array(
85 'type' => 'boolean',
86 'description' => __( 'Show a "notify me of new comments" checkbox in the comment form. Default true.', 'jetpack' ),
87 ),
88 'notify_admin_on_subscribe' => array(
89 'type' => 'boolean',
90 'description' => __( 'Email the site admin whenever a new subscriber signs up. Default true.', 'jetpack' ),
91 ),
92 'reply_to' => array(
93 'type' => 'string',
94 'enum' => self::REPLY_TO_VALUES,
95 'description' => __( 'Reply-to address for newsletter emails. "comment" routes to the post comment author, "author" to the post author, "no-reply" disables replies. Default "comment".', 'jetpack' ),
96 ),
97 'from_name' => array(
98 'type' => 'string',
99 'description' => __( 'Sender name shown on newsletter emails. Empty string falls back to the site name.', 'jetpack' ),
100 'maxLength' => 200,
101 ),
102 ),
103 );
104
105 return array(
106 'jetpack-newsletter/get-settings' => array(
107 'label' => __( 'Get Newsletter settings', 'jetpack' ),
108 'description' => __(
109 'Return the current Jetpack Newsletter settings as a flat object. Always returns the same five fields: subscribe_post_end_enabled (bool), subscribe_comments_enabled (bool), notify_admin_on_subscribe (bool), reply_to ("comment"|"author"|"no-reply"), and from_name (string). Read-only and idempotent. To change any value, call jetpack-newsletter/update-settings.',
110 'jetpack'
111 ),
112 'input_schema' => array(
113 'type' => 'object',
114 'default' => array(),
115 'properties' => array(),
116 'additionalProperties' => false,
117 ),
118 'output_schema' => $settings_object_schema,
119 'execute_callback' => array( __CLASS__, 'get_settings' ),
120 'permission_callback' => array( __CLASS__, 'can_view_settings' ),
121 'meta' => array(
122 'annotations' => array(
123 'readonly' => true,
124 'destructive' => false,
125 'idempotent' => true,
126 ),
127 'show_in_rest' => true,
128 'mcp' => array(
129 'public' => true,
130 'type' => 'tool', // default is already "tool", but can be explicit.
131 ),
132 ),
133 ),
134
135 'jetpack-newsletter/update-settings' => array(
136 'label' => __( 'Update Newsletter settings', 'jetpack' ),
137 'description' => __(
138 'Update one or more Jetpack Newsletter settings. Any subset of the five fields may be supplied; omitted fields are left untouched. Idempotent — fields whose desired value already matches the current value are not rewritten. Returns { settings: <full current state after the update>, changed: <array of field names that actually transitioned> }. An empty input or input matching the current state returns changed = [].',
139 'jetpack'
140 ),
141 'input_schema' => $settings_object_schema,
142 'output_schema' => array(
143 'type' => 'object',
144 'properties' => array(
145 'settings' => $settings_object_schema,
146 'changed' => array(
147 'type' => 'array',
148 'items' => array( 'type' => 'string' ),
149 'description' => __( 'Names of the fields that actually changed during this call. Empty when the call was a no-op.', 'jetpack' ),
150 ),
151 ),
152 ),
153 'execute_callback' => array( __CLASS__, 'update_settings' ),
154 'permission_callback' => array( __CLASS__, 'can_manage_settings' ),
155 'meta' => array(
156 'annotations' => array(
157 'readonly' => false,
158 'destructive' => false,
159 'idempotent' => true,
160 ),
161 'show_in_rest' => true,
162 'mcp' => array(
163 'public' => true,
164 'type' => 'tool', // default is already "tool", but can be explicit.
165 ),
166 ),
167 ),
168
169 'jetpack-newsletter/get-subscriber-stats' => array(
170 'label' => __( 'Get Newsletter subscriber stats', 'jetpack' ),
171 'description' => __(
172 'Return aggregate subscriber counts for the site. Always returns { all: int, email: int, paid: int }: all is the total subscriber count (email + WordPress.com followers); email is the subset that receives email; paid is the subset on a paid newsletter plan. Numbers are fetched from WordPress.com and cached locally for one hour, so transient network errors yield a stale-but-non-zero response when one is available. Requires an active Jetpack connection — sites without one return jetpack_newsletter_not_connected.',
173 'jetpack'
174 ),
175 'input_schema' => array(
176 'type' => 'object',
177 'default' => array(),
178 'properties' => array(),
179 'additionalProperties' => false,
180 ),
181 'output_schema' => array(
182 'type' => 'object',
183 'properties' => array(
184 'all' => array( 'type' => 'integer' ),
185 'email' => array( 'type' => 'integer' ),
186 'paid' => array( 'type' => 'integer' ),
187 ),
188 ),
189 'execute_callback' => array( __CLASS__, 'get_subscriber_stats' ),
190 'permission_callback' => array( __CLASS__, 'can_view_settings' ),
191 'meta' => array(
192 'annotations' => array(
193 'readonly' => true,
194 'destructive' => false,
195 'idempotent' => true,
196 ),
197 'show_in_rest' => true,
198 'mcp' => array(
199 'public' => true,
200 'type' => 'tool', // default is already "tool", but can be explicit.
201 ),
202 ),
203 ),
204 );
205 }
206
207 /**
208 * Permission check for read abilities. Newsletter settings live on the WP
209 * Newsletter settings screen, which is itself gated on `manage_options`.
210 */
211 public static function can_view_settings(): bool {
212 return current_user_can( 'manage_options' );
213 }
214
215 /**
216 * Permission check for write abilities. Mirrors the gating on the
217 * Newsletter settings screen.
218 */
219 public static function can_manage_settings(): bool {
220 return current_user_can( 'manage_options' );
221 }
222
223 /**
224 * Execute: return the current newsletter settings.
225 *
226 * @param array|null $input Unused — input schema accepts no parameters.
227 * @return array
228 */
229 public static function get_settings( $input = null ): array {
230 unset( $input );
231 return self::current_settings();
232 }
233
234 /**
235 * Transient key for the wpcom subscriber-stats response.
236 */
237 private const SUBSCRIBER_STATS_CACHE_KEY = 'jetpack_newsletter_subscriber_stats';
238
239 /**
240 * Transient TTL for subscriber-stats responses, in seconds.
241 *
242 * Matches the existing legacy widget pattern of an hour-long cache so
243 * agents calling this ability repeatedly don't fan out to wpcom.
244 */
245 private const SUBSCRIBER_STATS_CACHE_TTL = HOUR_IN_SECONDS;
246
247 /**
248 * Execute: fetch (and cache) aggregate subscriber counts from WordPress.com.
249 *
250 * @param array|null $input Unused — input schema accepts no parameters.
251 * @return array|\WP_Error
252 */
253 public static function get_subscriber_stats( $input = null ) {
254 unset( $input );
255
256 $cached = get_transient( self::SUBSCRIBER_STATS_CACHE_KEY );
257 if ( is_array( $cached ) ) {
258 return $cached;
259 }
260
261 if ( ! class_exists( 'Jetpack' ) || ! Jetpack::is_connection_ready() ) {
262 return new \WP_Error(
263 'jetpack_newsletter_not_connected',
264 __( 'Subscriber stats are only available on Jetpack-connected sites. Connect Jetpack and retry.', 'jetpack' )
265 );
266 }
267
268 $site_id = (int) Jetpack_Options::get_option( 'id' );
269 if ( $site_id <= 0 ) {
270 return new \WP_Error(
271 'jetpack_newsletter_not_connected',
272 __( 'No Jetpack site ID is registered. Connect Jetpack and retry.', 'jetpack' )
273 );
274 }
275
276 $response = Client::wpcom_json_api_request_as_blog(
277 sprintf( '/sites/%d/subscribers/stats', $site_id ),
278 '2',
279 array(),
280 null,
281 'wpcom'
282 );
283
284 if ( is_wp_error( $response ) ) {
285 return new \WP_Error(
286 'jetpack_newsletter_subscriber_stats_unavailable',
287 $response->get_error_message()
288 );
289 }
290
291 if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
292 return new \WP_Error(
293 'jetpack_newsletter_subscriber_stats_unavailable',
294 __( 'WordPress.com did not return subscriber stats. Retry shortly.', 'jetpack' )
295 );
296 }
297
298 $body = json_decode( wp_remote_retrieve_body( $response ), true );
299 $counts = is_array( $body ) && isset( $body['counts'] ) && is_array( $body['counts'] )
300 ? $body['counts']
301 : array();
302
303 $stats = array(
304 'all' => isset( $counts['all_subscribers'] ) ? (int) $counts['all_subscribers'] : 0,
305 'email' => isset( $counts['email_subscribers'] ) ? (int) $counts['email_subscribers'] : 0,
306 'paid' => isset( $counts['paid_subscribers'] ) ? (int) $counts['paid_subscribers'] : 0,
307 );
308
309 set_transient( self::SUBSCRIBER_STATS_CACHE_KEY, $stats, self::SUBSCRIBER_STATS_CACHE_TTL );
310
311 return $stats;
312 }
313
314 /**
315 * Execute: idempotent partial update of newsletter settings.
316 *
317 * Validates every supplied field before writing anything, so a malformed
318 * field cannot leave the option set in a partially-updated state.
319 *
320 * @param array|null $input Input matching the ability's input_schema.
321 * @return array|\WP_Error
322 */
323 public static function update_settings( $input = null ) {
324 $input = is_array( $input ) ? $input : array();
325 $map = self::settings_map();
326
327 // Validate + normalize every supplied field up-front. Any failure
328 // short-circuits the call with no writes, so a bad field can't leave
329 // earlier fields in a partially-updated state.
330 $normalized = array();
331 foreach ( $map as $field => $config ) {
332 if ( ! array_key_exists( $field, $input ) ) {
333 continue;
334 }
335
336 $result = self::normalize_input_value( $field, $config, $input[ $field ] );
337 if ( $result instanceof \WP_Error ) {
338 return $result;
339 }
340 $normalized[ $field ] = $result;
341 }
342
343 // Read every field's current value once. This pass also feeds the
344 // post-update response, avoiding a second `get_option` sweep.
345 $current_storage = array();
346 foreach ( $map as $field => $config ) {
347 $current_storage[ $field ] = self::read_option( $config );
348 }
349
350 $changed = array();
351 foreach ( $normalized as $field => $desired ) {
352 // String-cast on both sides because every field's storage form is
353 // scalar (`0`/`1` for BOOL, `'on'`/`'off'` for ON_OFF, plain strings
354 // for ENUM/STRING). New field types added later must keep that
355 // invariant or this comparison will misfire.
356 if ( (string) $desired === (string) $current_storage[ $field ] ) {
357 continue;
358 }
359 update_option( $map[ $field ]['option'], $desired );
360 $current_storage[ $field ] = $desired;
361 $changed[] = $field;
362 }
363
364 $settings = array();
365 foreach ( $map as $field => $config ) {
366 $settings[ $field ] = self::cast_to_response( $config, $current_storage[ $field ] );
367 }
368
369 return array(
370 'settings' => $settings,
371 'changed' => $changed,
372 );
373 }
374
375 /**
376 * Map of public ability field name → backing option config.
377 *
378 * Storage shape (option key, type tag, default, enum). The agent-facing
379 * descriptions and JSON Schema live in `get_abilities()`; this map drives
380 * the storage-side validation, normalization, and casting.
381 *
382 * Kept as a method (not a class constant) so the description strings
383 * referenced from `cast_to_response()` and `normalize_input_value()` can
384 * resolve through `__()` at call time rather than file load time.
385 */
386 private static function settings_map(): array {
387 return array(
388 'subscribe_post_end_enabled' => array(
389 'option' => 'stb_enabled',
390 'type' => self::TYPE_BOOL,
391 'default' => 1,
392 ),
393 'subscribe_comments_enabled' => array(
394 'option' => 'stc_enabled',
395 'type' => self::TYPE_BOOL,
396 'default' => 1,
397 ),
398 'notify_admin_on_subscribe' => array(
399 'option' => 'social_notifications_subscribe',
400 'type' => self::TYPE_ON_OFF,
401 'default' => 'on',
402 ),
403 'reply_to' => array(
404 'option' => 'jetpack_subscriptions_reply_to',
405 'type' => self::TYPE_ENUM,
406 'default' => Subscriptions_Settings::$default_reply_to,
407 'enum' => self::REPLY_TO_VALUES,
408 ),
409 'from_name' => array(
410 'option' => 'jetpack_subscriptions_from_name',
411 'type' => self::TYPE_STRING,
412 'default' => '',
413 'max_length' => 200,
414 ),
415 );
416 }
417
418 /**
419 * Read all settings as the public response shape.
420 */
421 private static function current_settings(): array {
422 $out = array();
423 foreach ( self::settings_map() as $field => $config ) {
424 $out[ $field ] = self::cast_to_response( $config, self::read_option( $config ) );
425 }
426 return $out;
427 }
428
429 /**
430 * Read the raw option for a field config, falling back to its default.
431 *
432 * @param array $config Field config from `settings_map()`.
433 * @return mixed
434 */
435 private static function read_option( array $config ) {
436 return get_option( $config['option'], $config['default'] );
437 }
438
439 /**
440 * Validate + normalize a single input value to the storage form.
441 *
442 * @param string $field Public field name (used in error messages).
443 * @param array $config Field config from `settings_map()`.
444 * @param mixed $value Raw input value.
445 * @return mixed|\WP_Error Storage-form value, or WP_Error when invalid.
446 */
447 private static function normalize_input_value( string $field, array $config, $value ) {
448 switch ( $config['type'] ) {
449 case self::TYPE_BOOL:
450 if ( ! is_bool( $value ) ) {
451 return self::invalid_field( $field, __( 'expected a boolean (true or false).', 'jetpack' ) );
452 }
453 return $value ? 1 : 0;
454
455 case self::TYPE_ON_OFF:
456 if ( ! is_bool( $value ) ) {
457 return self::invalid_field( $field, __( 'expected a boolean (true or false).', 'jetpack' ) );
458 }
459 return $value ? 'on' : 'off';
460
461 case self::TYPE_ENUM:
462 // reply_to is the only enum today and shares its allowed-values
463 // list with `Subscriptions_Settings::is_valid_reply_to()`. Defer
464 // to that validator so the two surfaces can't drift.
465 $valid = 'reply_to' === $field
466 ? Subscriptions_Settings::is_valid_reply_to( $value )
467 : ( is_string( $value ) && in_array( $value, $config['enum'], true ) );
468 if ( ! $valid ) {
469 return self::invalid_field(
470 $field,
471 sprintf(
472 /* translators: %s: comma-separated list of allowed values. */
473 __( 'allowed values are %s.', 'jetpack' ),
474 implode( ', ', $config['enum'] )
475 )
476 );
477 }
478 return $value;
479
480 case self::TYPE_STRING:
481 if ( ! is_string( $value ) ) {
482 return self::invalid_field( $field, __( 'expected a string.', 'jetpack' ) );
483 }
484 $sanitized = sanitize_text_field( $value );
485 if ( isset( $config['max_length'] ) && mb_strlen( $sanitized ) > (int) $config['max_length'] ) {
486 return self::invalid_field(
487 $field,
488 sprintf(
489 /* translators: %d: maximum number of characters. */
490 __( 'must be %d characters or fewer.', 'jetpack' ),
491 (int) $config['max_length']
492 )
493 );
494 }
495 return $sanitized;
496 }
497
498 return self::invalid_field( $field, __( 'unsupported field type.', 'jetpack' ) );
499 }
500
501 /**
502 * Cast a stored option value to the public response shape.
503 *
504 * @param array $config Field config from `settings_map()`.
505 * @param mixed $value Raw stored value.
506 * @return mixed
507 */
508 private static function cast_to_response( array $config, $value ) {
509 switch ( $config['type'] ) {
510 case self::TYPE_BOOL:
511 return 1 === (int) $value;
512 case self::TYPE_ON_OFF:
513 return 'on' === (string) $value;
514 case self::TYPE_ENUM:
515 $value = (string) $value;
516 return in_array( $value, $config['enum'], true ) ? $value : (string) $config['default'];
517 case self::TYPE_STRING:
518 return (string) $value;
519 }
520 return $value;
521 }
522
523 /**
524 * Build a `jetpack_newsletter_invalid_<field>` WP_Error with a message
525 * that names the field and tells the agent how to fix the input.
526 *
527 * @param string $field Public field name; appears in the error code and message.
528 * @param string $reason Translated explanation of the expected value.
529 * @return \WP_Error
530 */
531 private static function invalid_field( string $field, string $reason ): \WP_Error {
532 return new \WP_Error(
533 'jetpack_newsletter_invalid_' . $field,
534 sprintf(
535 /* translators: 1: field name, 2: explanation of the expected value. */
536 __( 'Invalid value for "%1$s": %2$s', 'jetpack' ),
537 $field,
538 $reason
539 )
540 );
541 }
542 }
543