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