← All changes
|
includes/abilities/settings/class-settings-key-map.php
+101
-27
2.7.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' ) ), |
| @@ -159,8 +172,13 @@ | ||
| 159 | 172 | */ |
| 160 | 173 | public static function sitemap(): array { |
| 161 | 174 | return [ |
| 162 | 175 | 'enabled' => self::boolean( __( 'Whether XML sitemap generation is active.', 'thinkrank' ) ), |
| 176 | + 'delivery_mode' => [ | |
| 177 | + 'type' => 'string', | |
| 178 | + 'enum' => \ThinkRank\SEO\Sitemap_Generator::DELIVERY_MODES, | |
| 179 | + 'description' => __( 'How the sitemap reaches crawlers. "auto" writes files when the WordPress root is writable and serves the sitemap from WordPress when it is not. "static" always writes files, and fails where that is not possible. "dynamic" always serves from WordPress and writes nothing.', 'thinkrank' ), | |
| 180 | + ], | |
| 163 | 181 | 'auto_generate' => self::boolean( __( 'Whether the sitemap regenerates automatically when content changes.', 'thinkrank' ) ), |
| 164 | 182 | 'use_sitemap_index' => self::boolean( __( 'Whether to publish a sitemap index that links per-type sitemaps, rather than one flat file.', 'thinkrank' ) ), |
| 165 | 183 | 'links_per_sitemap' => [ |
| 166 | 184 | 'type' => 'integer', |
| @@ -199,8 +217,17 @@ | ||
| 199 | 217 | * |
| 200 | 218 | * The store keeps everything as strings ("1"/""), so each value is coerced |
| 201 | 219 | * back to the type the schema advertises before it reaches an agent. |
| 202 | 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 | + * | |
| 203 | 230 | * @param array<string, array<string, mixed>> $properties Schema properties for the category. |
| 204 | 231 | * @param array<string, mixed> $stored Settings as the manager returns them. |
| 205 | 232 | * @return array<string, mixed> Exposed settings, one entry per declared key. |
| 206 | 233 | */ |
| @@ -207,39 +234,86 @@ | ||
| 207 | 234 | public static function read( array $properties, array $stored ): array { |
| 208 | 235 | $out = []; |
| 209 | 236 | |
| 210 | 237 | foreach ( $properties as $key => $property ) { |
| 211 | - $value = $stored[ $key ] ?? null; | |
| 238 | + $out[ $key ] = self::read_value( $property, $stored[ $key ] ?? null ); | |
| 239 | + } | |
| 212 | 240 | |
| 213 | - $type = $property['type'] ?? 'string'; | |
| 241 | + return $out; | |
| 242 | + } | |
| 214 | 243 | |
| 215 | - // A union type ('string' or a list of them) keeps whichever shape | |
| 216 | - // it arrived in: casting to string would flatten a list, and | |
| 217 | - // casting to array would replace a name with [] (#692). | |
| 218 | - if ( is_array( $type ) ) { | |
| 219 | - $out[ $key ] = is_array( $value ) | |
| 220 | - ? array_values( array_map( 'strval', $value ) ) | |
| 221 | - : (string) ( $value ?? '' ); | |
| 222 | - continue; | |
| 223 | - } | |
| 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'; | |
| 224 | 262 | |
| 225 | - switch ( $type ) { | |
| 226 | - case 'boolean': | |
| 227 | - $out[ $key ] = (bool) $value; | |
| 228 | - break; | |
| 229 | - case 'integer': | |
| 230 | - $out[ $key ] = (int) $value; | |
| 231 | - break; | |
| 232 | - case 'array': | |
| 233 | - case 'object': | |
| 234 | - $out[ $key ] = is_array( $value ) ? $value : []; | |
| 235 | - break; | |
| 236 | - default: | |
| 237 | - $out[ $key ] = (string) ( $value ?? '' ); | |
| 238 | - } | |
| 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 : '' ); | |
| 239 | 270 | } |
| 240 | 271 | |
| 241 | - 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 | + } | |
| 242 | 316 | } |
| 243 | 317 | |
| 244 | 318 | /** |
| 245 | 319 | * Coerce an incoming patch to the declared types, dropping unknown keys. |