PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
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 1.28.0 1.27.0 All 56 releases
← All changes | includes/seo/class-schema-input-validator.php +90 -5 2.0.2 → 2.14.1 View file →
@@ -126,8 +126,40 @@
126 126 'Review' => [
127 127 'required_fields' => ['@type', 'itemReviewed', 'reviewRating', 'author'],
128 128 'optional_fields' => ['reviewBody', 'datePublished', 'publisher', 'name', 'url'],
129 129 'max_length' => ['name' => 110, 'reviewBody' => 500]
130 + ],
131 + // The WebPage family. #624 made all four selectable in the metabox and
132 + // taught the generate/validate/optimize/preview routes, Schema_Builder,
133 + // Schema_Factory and Schema_Graph about them — but not this array, and
134 + // deploy_schema_markup() gates every entry on it. So each one generated
135 + // cleanly, validated at a score of 100, reported deployment_ready, then
136 + // failed deployment with "Invalid schema type: AboutPage" and no row
137 + // written. Exactly the #462 Review bug, one array and two releases
138 + // later, which is why SchemaInputValidatorCoverageTest now scans the
139 + // dropdown instead of trusting this list to be extended by hand.
140 + //
141 + // `name` and `url` are the two populate_webpage_schema() always sets;
142 + // the rest are conditional, so they are optional here.
143 + 'WebPage' => [
144 + 'required_fields' => ['@type', 'name', 'url'],
145 + 'optional_fields' => ['description', 'datePublished', 'dateModified', 'isPartOf', 'breadcrumb', 'primaryImageOfPage', 'inLanguage', 'mainEntity'],
146 + 'max_length' => ['name' => 110, 'description' => 160]
147 + ],
148 + 'AboutPage' => [
149 + 'required_fields' => ['@type', 'name', 'url'],
150 + 'optional_fields' => ['description', 'datePublished', 'dateModified', 'isPartOf', 'breadcrumb', 'primaryImageOfPage', 'inLanguage', 'mainEntity'],
151 + 'max_length' => ['name' => 110, 'description' => 160]
152 + ],
153 + 'ContactPage' => [
154 + 'required_fields' => ['@type', 'name', 'url'],
155 + 'optional_fields' => ['description', 'datePublished', 'dateModified', 'isPartOf', 'breadcrumb', 'primaryImageOfPage', 'inLanguage', 'mainEntity'],
156 + 'max_length' => ['name' => 110, 'description' => 160]
157 + ],
158 + 'ProfilePage' => [
159 + 'required_fields' => ['@type', 'name', 'url'],
160 + 'optional_fields' => ['description', 'datePublished', 'dateModified', 'isPartOf', 'breadcrumb', 'primaryImageOfPage', 'inLanguage', 'mainEntity'],
161 + 'max_length' => ['name' => 110, 'description' => 160]
130 162 ]
131 163 ];
132 164
133 165 /**
@@ -234,9 +266,9 @@
234 266 $result['errors'] = array_merge($result['errors'], $field_validation['errors']);
235 267 }
236 268
237 269 // 6. Validate data types and formats
238 - $format_validation = $this->validate_data_formats($sanitized_data, $schema_type);
270 + $format_validation = $this->validate_data_formats($sanitized_data, $schema_type, $schema_data);
239 271 if (!$format_validation['valid']) {
240 272 $result['errors'] = array_merge($result['errors'], $format_validation['errors']);
241 273 }
242 274 $result['warnings'] = array_merge($result['warnings'], $format_validation['warnings']);
@@ -550,11 +582,12 @@
550 582 * @since 1.0.0
551 583 *
552 584 * @param array $schema_data Schema data
553 585 * @param string $schema_type Schema type
586 + * @param array $raw_data Schema data as submitted, before sanitization.
554 587 * @return array Validation result
555 588 */
556 - private function validate_data_formats(array $schema_data, string $schema_type): array {
589 + private function validate_data_formats(array $schema_data, string $schema_type, array $raw_data = []): array {
557 590 $result = ['valid' => true, 'errors' => [], 'warnings' => []];
558 591
559 592 foreach ($schema_data as $field => $value) {
560 593 // sameAs is a list, so its members never reached the string branch
@@ -560,15 +593,27 @@
560 593 // sameAs is a list, so its members never reached the string branch
561 594 // below and free text entered in a social-profile field saved
562 595 // cleanly, then shipped as invalid structured data (#480).
563 596 if (is_array($value) && in_array($field, ['url', 'sameAs', 'logo', 'image'], true)) {
564 - foreach ($value as $item) {
565 - if (!is_string($item) || '' === trim($item)) {
597 + // Validated after sanitization, but reported as entered:
598 + // esc_url_raw() turns "not a url" into "http://not%20a%20url",
599 + // which the user never typed (#949 review).
600 + $items = $this->url_candidates($value);
601 + $raw_items = is_array($raw_data[$field] ?? null) ? $this->url_candidates($raw_data[$field]) : [];
602 + if (count($raw_items) !== count($items)) {
603 + // A non-string member sanitized into a string would shift
604 + // the positions; fall back rather than name the wrong one.
605 + $raw_items = [];
606 + }
607 +
608 + foreach ($items as $index => $item) {
609 + if ('' === trim($item)) {
566 610 continue;
567 611 }
568 612
569 613 if (!$this->is_valid_url($item)) {
570 - $result['errors'][] = "Invalid URL format for field: {$field} ({$item})";
614 + $shown = $raw_items[$index] ?? $item;
615 + $result['errors'][] = "Invalid URL format for field: {$field} ({$shown})";
571 616 $result['valid'] = false;
572 617 }
573 618 }
574 619
@@ -604,8 +649,47 @@
604 649 return $result;
605 650 }
606 651
607 652 /**
653 + * The URL strings inside an array-valued URL field.
654 + *
655 + * The field is either a list (`sameAs`, several `image` URLs) or a single
656 + * node such as the `ImageObject` Schema_Builder emits for a configured
657 + * logo. Checking every member of a node URL-checked its `@type` and
658 + * `width`, so "ImageObject" failed as an invalid URL and the deploy route
659 + * skipped — then retired — the site's Organization (#949). Only a node's
660 + * `url` / `contentUrl` are URLs; a list may hold strings or such nodes.
661 + *
662 + * @since 2.14.1
663 + *
664 + * @param array $value Array-valued URL field.
665 + * @return string[] URL strings to validate.
666 + */
667 + private function url_candidates(array $value): array {
668 + $node_urls = static function (array $node): array {
669 + return array_values(array_filter(
670 + [$node['url'] ?? null, $node['contentUrl'] ?? null],
671 + 'is_string'
672 + ));
673 + };
674 +
675 + if (array_values($value) !== $value) {
676 + return $node_urls($value);
677 + }
678 +
679 + $urls = [];
680 + foreach ($value as $item) {
681 + if (is_string($item)) {
682 + $urls[] = $item;
683 + } elseif (is_array($item)) {
684 + $urls = array_merge($urls, $node_urls($item));
685 + }
686 + }
687 +
688 + return $urls;
689 + }
690 +
691 + /**
608 692 * Validate content lengths
609 693 *
610 694 * @since 1.0.0
611 695 *
@@ -982,8 +1066,9 @@
982 1066 'enable_howto_schema' => 'boolean',
983 1067 'enable_product_schema' => 'boolean',
984 1068 'enable_local_business' => 'boolean',
985 1069 'enable_breadcrumbs_schema' => 'boolean',
1070 + 'enable_accordion_faq_schema' => 'boolean',
986 1071 ];
987 1072
988 1073 foreach ($options as $key => $value) {
989 1074 $sanitized_key = sanitize_key($key);