← All changes
|
includes/abilities/settings/class-settings-key-map.php
+96
-27
2.9.0
→
2.10.0
View file →
| @@ -120,9 +120,22 @@ | ||
| 120 | 120 | |
| 121 | 121 | // Business / Local SEO. Feeds LocalBusiness schema. |
| 122 | 122 | 'local_seo_enabled' => self::boolean( __( 'Whether Local SEO output and LocalBusiness schema are enabled.', 'thinkrank' ) ), |
| 123 | 123 | 'business_name' => self::string( __( 'Registered business name.', 'thinkrank' ) ), |
| 124 | - 'business_type' => self::string( __( 'schema.org business type, e.g. "Restaurant", "Store".', 'thinkrank' ) ), | |
| 124 | + // Enumerated rather than free-form: the value goes straight into | |
| 125 | + // LocalBusiness schema, so an invented type is invalid structured | |
| 126 | + // data. ~150 schema.org subtypes are accepted (#623). | |
| 127 | + // | |
| 128 | + // '' is in the list because it is a real stored state ("not set"): | |
| 129 | + // the store holds it on every site that never opened Local SEO, and | |
| 130 | + // without it the READ ability failed its own output schema on those | |
| 131 | + // sites and there was no way to clear the value over MCP. It goes | |
| 132 | + // last so the root stays first, which read() falls back to. | |
| 133 | + 'business_type' => [ | |
| 134 | + 'type' => 'string', | |
| 135 | + 'enum' => array_merge( \ThinkRank\Config\Local_Business_Types_Config::get_types(), [ '' ] ), | |
| 136 | + 'description' => __( 'schema.org LocalBusiness type, e.g. "Restaurant", "HealthAndBeautyBusiness". "LocalBusiness" is the general-purpose default and is always valid. An empty string means not set, and is published as LocalBusiness.', 'thinkrank' ), | |
| 137 | + ], | |
| 125 | 138 | 'business_email' => self::string( __( 'Public contact email address.', 'thinkrank' ) ), |
| 126 | 139 | 'business_phone' => self::string( __( 'Public contact telephone number.', 'thinkrank' ) ), |
| 127 | 140 | 'business_address' => self::string( __( 'Street address.', 'thinkrank' ) ), |
| 128 | 141 | 'business_city' => self::string( __( 'City or locality.', 'thinkrank' ) ), |
| @@ -204,8 +217,17 @@ | ||
| 204 | 217 | * |
| 205 | 218 | * The store keeps everything as strings ("1"/""), so each value is coerced |
| 206 | 219 | * back to the type the schema advertises before it reaches an agent. |
| 207 | 220 | * |
| 221 | + * The same properties are the read ability's OUTPUT schema, which the | |
| 222 | + * Abilities API validates, and one value that fails it fails the whole | |
| 223 | + * call. So what comes out has to fit the schema for any stored value, not | |
| 224 | + * only for what today's save path writes: a business type imported before | |
| 225 | + * the list was enumerated, or a crawler rule whose crawler a filter has | |
| 226 | + * since removed, used to turn every get-site-identity-settings call into | |
| 227 | + * ability_invalid_output. Hence the recursion into objects, and the enum | |
| 228 | + * fallback in read_value(). | |
| 229 | + * | |
| 208 | 230 | * @param array<string, array<string, mixed>> $properties Schema properties for the category. |
| 209 | 231 | * @param array<string, mixed> $stored Settings as the manager returns them. |
| 210 | 232 | * @return array<string, mixed> Exposed settings, one entry per declared key. |
| 211 | 233 | */ |
| @@ -212,39 +234,86 @@ | ||
| 212 | 234 | public static function read( array $properties, array $stored ): array { |
| 213 | 235 | $out = []; |
| 214 | 236 | |
| 215 | 237 | foreach ( $properties as $key => $property ) { |
| 216 | - $value = $stored[ $key ] ?? null; | |
| 238 | + $out[ $key ] = self::read_value( $property, $stored[ $key ] ?? null ); | |
| 239 | + } | |
| 217 | 240 | |
| 218 | - $type = $property['type'] ?? 'string'; | |
| 241 | + return $out; | |
| 242 | + } | |
| 219 | 243 | |
| 220 | - // A union type ('string' or a list of them) keeps whichever shape | |
| 221 | - // it arrived in: casting to string would flatten a list, and | |
| 222 | - // casting to array would replace a name with [] (#692). | |
| 223 | - if ( is_array( $type ) ) { | |
| 224 | - $out[ $key ] = is_array( $value ) | |
| 225 | - ? array_values( array_map( 'strval', $value ) ) | |
| 226 | - : (string) ( $value ?? '' ); | |
| 227 | - continue; | |
| 228 | - } | |
| 244 | + /** | |
| 245 | + * Coerce one stored value to the shape its property declares. | |
| 246 | + * | |
| 247 | + * An enumerated string that holds something outside its enum reads as the | |
| 248 | + * enum's FIRST entry. Every enum in this map is ordered so that entry is | |
| 249 | + * the value its manager falls back to for anything unrecognised | |
| 250 | + * (`automatic`, `auto`, `allow`, `LocalBusiness`), so the agent is told | |
| 251 | + * what the site actually does rather than a value it would be refused if | |
| 252 | + * it wrote it back. | |
| 253 | + * | |
| 254 | + * @since 2.10.0 | |
| 255 | + * | |
| 256 | + * @param array<string, mixed> $property Schema property. | |
| 257 | + * @param mixed $value Stored value, or null when absent. | |
| 258 | + * @return mixed | |
| 259 | + */ | |
| 260 | + private static function read_value( array $property, $value ) { | |
| 261 | + $type = $property['type'] ?? 'string'; | |
| 229 | 262 | |
| 230 | - switch ( $type ) { | |
| 231 | - case 'boolean': | |
| 232 | - $out[ $key ] = (bool) $value; | |
| 233 | - break; | |
| 234 | - case 'integer': | |
| 235 | - $out[ $key ] = (int) $value; | |
| 236 | - break; | |
| 237 | - case 'array': | |
| 238 | - case 'object': | |
| 239 | - $out[ $key ] = is_array( $value ) ? $value : []; | |
| 240 | - break; | |
| 241 | - default: | |
| 242 | - $out[ $key ] = (string) ( $value ?? '' ); | |
| 243 | - } | |
| 263 | + // A union type ('string' or a list of them) keeps whichever shape | |
| 264 | + // it arrived in: casting to string would flatten a list, and | |
| 265 | + // casting to array would replace a name with [] (#692). | |
| 266 | + if ( is_array( $type ) ) { | |
| 267 | + return is_array( $value ) | |
| 268 | + ? array_values( array_map( 'strval', array_filter( $value, 'is_scalar' ) ) ) | |
| 269 | + : ( is_scalar( $value ) ? (string) $value : '' ); | |
| 244 | 270 | } |
| 245 | 271 | |
| 246 | - return $out; | |
| 272 | + switch ( $type ) { | |
| 273 | + case 'boolean': | |
| 274 | + return (bool) $value; | |
| 275 | + case 'integer': | |
| 276 | + return (int) $value; | |
| 277 | + case 'array': | |
| 278 | + if ( ! is_array( $value ) ) { | |
| 279 | + return []; | |
| 280 | + } | |
| 281 | + if ( isset( $property['items'] ) && is_array( $property['items'] ) ) { | |
| 282 | + $items = $property['items']; | |
| 283 | + return array_values( array_map( static fn( $item ) => self::read_value( $items, $item ), $value ) ); | |
| 284 | + } | |
| 285 | + return $value; | |
| 286 | + case 'object': | |
| 287 | + if ( ! is_array( $value ) ) { | |
| 288 | + return []; | |
| 289 | + } | |
| 290 | + if ( empty( $property['properties'] ) || ! is_array( $property['properties'] ) ) { | |
| 291 | + return $value; | |
| 292 | + } | |
| 293 | + | |
| 294 | + $declared = $property['properties']; | |
| 295 | + $closed = isset( $property['additionalProperties'] ) && false === $property['additionalProperties']; | |
| 296 | + $object = []; | |
| 297 | + | |
| 298 | + foreach ( $value as $name => $item ) { | |
| 299 | + if ( isset( $declared[ $name ] ) ) { | |
| 300 | + $object[ $name ] = self::read_value( $declared[ $name ], $item ); | |
| 301 | + } elseif ( ! $closed ) { | |
| 302 | + $object[ $name ] = $item; | |
| 303 | + } | |
| 304 | + } | |
| 305 | + | |
| 306 | + return $object; | |
| 307 | + default: | |
| 308 | + $string = is_scalar( $value ) ? (string) $value : ''; | |
| 309 | + | |
| 310 | + if ( ! empty( $property['enum'] ) && ! in_array( $string, $property['enum'], true ) ) { | |
| 311 | + return (string) reset( $property['enum'] ); | |
| 312 | + } | |
| 313 | + | |
| 314 | + return $string; | |
| 315 | + } | |
| 247 | 316 | } |
| 248 | 317 | |
| 249 | 318 | /** |
| 250 | 319 | * Coerce an incoming patch to the declared types, dropping unknown keys. |