PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
← 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.