PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.3
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.3
2.14.3 2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.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 All 58 releases
← All changes | includes/seo/class-schema-input-validator.php +72 -7 2.14.0 → 2.14.3 View file →
@@ -229,8 +229,16 @@
229 229 'errors' => [],
230 230 'warnings' => []
231 231 ];
232 232
233 + // The deploy routes read the type from the node's @type, and a
234 + // LocalBusiness is published under the business type chosen in Local
235 + // SEO ("Restaurant", #1005). Validate it as the LocalBusiness it is;
236 + // the @type match below accepts the subtype.
237 + if (\ThinkRank\Config\Local_Business_Types_Config::is_local_business($schema_type)) {
238 + $schema_type = 'LocalBusiness';
239 + }
240 +
233 241 try {
234 242 // 1. Validate payload size to prevent DoS attacks
235 243 $size_validation = $this->validate_payload_size($schema_data);
236 244 if (!$size_validation['valid']) {
@@ -266,9 +274,9 @@
266 274 $result['errors'] = array_merge($result['errors'], $field_validation['errors']);
267 275 }
268 276
269 277 // 6. Validate data types and formats
270 - $format_validation = $this->validate_data_formats($sanitized_data, $schema_type);
278 + $format_validation = $this->validate_data_formats($sanitized_data, $schema_type, $schema_data);
271 279 if (!$format_validation['valid']) {
272 280 $result['errors'] = array_merge($result['errors'], $format_validation['errors']);
273 281 }
274 282 $result['warnings'] = array_merge($result['warnings'], $format_validation['warnings']);
@@ -416,9 +424,14 @@
416 424 $declared_types = is_array($schema_data['@type'])
417 425 ? array_map('strval', $schema_data['@type'])
418 426 : [(string) $schema_data['@type']];
419 427
420 - if (!in_array($schema_type, $declared_types, true)) {
428 + // A LocalBusiness is deployed under the business subtype the site
429 + // chose (#1005).
430 + $is_local_business_subtype = 'LocalBusiness' === $schema_type
431 + && [] !== array_filter($declared_types, [\ThinkRank\Config\Local_Business_Types_Config::class, 'is_local_business']);
432 +
433 + if (!$is_local_business_subtype && !in_array($schema_type, $declared_types, true)) {
421 434 $result['errors'][] = sprintf(
422 435 "Schema @type '%s' does not match expected type '%s'",
423 436 implode(', ', $declared_types),
424 437 $schema_type
@@ -582,11 +595,12 @@
582 595 * @since 1.0.0
583 596 *
584 597 * @param array $schema_data Schema data
585 598 * @param string $schema_type Schema type
599 + * @param array $raw_data Schema data as submitted, before sanitization.
586 600 * @return array Validation result
587 601 */
588 - private function validate_data_formats(array $schema_data, string $schema_type): array {
602 + private function validate_data_formats(array $schema_data, string $schema_type, array $raw_data = []): array {
589 603 $result = ['valid' => true, 'errors' => [], 'warnings' => []];
590 604
591 605 foreach ($schema_data as $field => $value) {
592 606 // sameAs is a list, so its members never reached the string branch
@@ -592,15 +606,27 @@
592 606 // sameAs is a list, so its members never reached the string branch
593 607 // below and free text entered in a social-profile field saved
594 608 // cleanly, then shipped as invalid structured data (#480).
595 609 if (is_array($value) && in_array($field, ['url', 'sameAs', 'logo', 'image'], true)) {
596 - foreach ($value as $item) {
597 - if (!is_string($item) || '' === trim($item)) {
610 + // Validated after sanitization, but reported as entered:
611 + // esc_url_raw() turns "not a url" into "http://not%20a%20url",
612 + // which the user never typed (#949 review).
613 + $items = $this->url_candidates($value);
614 + $raw_items = is_array($raw_data[$field] ?? null) ? $this->url_candidates($raw_data[$field]) : [];
615 + if (count($raw_items) !== count($items)) {
616 + // A non-string member sanitized into a string would shift
617 + // the positions; fall back rather than name the wrong one.
618 + $raw_items = [];
619 + }
620 +
621 + foreach ($items as $index => $item) {
622 + if ('' === trim($item)) {
598 623 continue;
599 624 }
600 625
601 626 if (!$this->is_valid_url($item)) {
602 - $result['errors'][] = "Invalid URL format for field: {$field} ({$item})";
627 + $shown = $raw_items[$index] ?? $item;
628 + $result['errors'][] = "Invalid URL format for field: {$field} ({$shown})";
603 629 $result['valid'] = false;
604 630 }
605 631 }
606 632
@@ -636,8 +662,47 @@
636 662 return $result;
637 663 }
638 664
639 665 /**
666 + * The URL strings inside an array-valued URL field.
667 + *
668 + * The field is either a list (`sameAs`, several `image` URLs) or a single
669 + * node such as the `ImageObject` Schema_Builder emits for a configured
670 + * logo. Checking every member of a node URL-checked its `@type` and
671 + * `width`, so "ImageObject" failed as an invalid URL and the deploy route
672 + * skipped — then retired — the site's Organization (#949). Only a node's
673 + * `url` / `contentUrl` are URLs; a list may hold strings or such nodes.
674 + *
675 + * @since 2.14.1
676 + *
677 + * @param array $value Array-valued URL field.
678 + * @return string[] URL strings to validate.
679 + */
680 + private function url_candidates(array $value): array {
681 + $node_urls = static function (array $node): array {
682 + return array_values(array_filter(
683 + [$node['url'] ?? null, $node['contentUrl'] ?? null],
684 + 'is_string'
685 + ));
686 + };
687 +
688 + if (array_values($value) !== $value) {
689 + return $node_urls($value);
690 + }
691 +
692 + $urls = [];
693 + foreach ($value as $item) {
694 + if (is_string($item)) {
695 + $urls[] = $item;
696 + } elseif (is_array($item)) {
697 + $urls = array_merge($urls, $node_urls($item));
698 + }
699 + }
700 +
701 + return $urls;
702 + }
703 +
704 + /**
640 705 * Validate content lengths
641 706 *
642 707 * @since 1.0.0
643 708 *
@@ -672,9 +737,9 @@
672 737 * @return bool Validation result
673 738 */
674 739 private function is_valid_url(string $url): bool {
675 740 // Basic URL validation
676 - if (!filter_var($url, FILTER_VALIDATE_URL)) {
741 + if (!\ThinkRank\Core\Url_Validator::is_valid($url)) {
677 742 return false;
678 743 }
679 744
680 745 // Check allowed protocols