PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/seo/class-schema-input-validator.php +87 -12 2.0.12.7.0 View file →
@@ -118,8 +118,16 @@
118 118 'VideoObject' => [
119 119 'required_fields' => ['@type', 'name', 'thumbnailUrl', 'uploadDate'],
120 120 'optional_fields' => ['description', 'contentUrl', 'embedUrl', 'duration', 'url'],
121 121 'max_length' => ['name' => 110, 'description' => 160]
122 + ],
123 + // Offered by the metabox Schema Type dropdown and registered in
124 + // Schema_Factory, but missing here — so a Review could be generated and
125 + // never deployed (#462). Field list mirrors Schema_Factory::Review.
126 + 'Review' => [
127 + 'required_fields' => ['@type', 'itemReviewed', 'reviewRating', 'author'],
128 + 'optional_fields' => ['reviewBody', 'datePublished', 'publisher', 'name', 'url'],
129 + 'max_length' => ['name' => 110, 'reviewBody' => 500]
122 130 ]
123 131 ];
124 132
125 133 /**
@@ -363,15 +371,29 @@
363 371 $result['errors'][] = 'Invalid @context value. Must be "https://schema.org"';
364 372 $result['valid'] = false;
365 373 }
366 374
367 - // Check for required @type
375 + // Check for required @type.
376 + // `@type` may be an array — "@type": ["Product","Offer"] is valid
377 + // JSON-LD. Comparing an array against a string emitted an "Array to
378 + // string conversion" warning and always failed (#468), so match if the
379 + // expected type appears anywhere in the list.
368 380 if (!isset($schema_data['@type'])) {
369 381 $result['errors'][] = 'Missing required @type field';
370 382 $result['valid'] = false;
371 - } elseif ($schema_data['@type'] !== $schema_type) {
372 - $result['errors'][] = "Schema @type '{$schema_data['@type']}' does not match expected type '{$schema_type}'";
373 - $result['valid'] = false;
383 + } else {
384 + $declared_types = is_array($schema_data['@type'])
385 + ? array_map('strval', $schema_data['@type'])
386 + : [(string) $schema_data['@type']];
387 +
388 + if (!in_array($schema_type, $declared_types, true)) {
389 + $result['errors'][] = sprintf(
390 + "Schema @type '%s' does not match expected type '%s'",
391 + implode(', ', $declared_types),
392 + $schema_type
393 + );
394 + $result['valid'] = false;
395 + }
374 396 }
375 397
376 398 return $result;
377 399 }
@@ -534,8 +556,26 @@
534 556 private function validate_data_formats(array $schema_data, string $schema_type): array {
535 557 $result = ['valid' => true, 'errors' => [], 'warnings' => []];
536 558
537 559 foreach ($schema_data as $field => $value) {
560 + // sameAs is a list, so its members never reached the string branch
561 + // below and free text entered in a social-profile field saved
562 + // cleanly, then shipped as invalid structured data (#480).
563 + if (is_array($value) && in_array($field, ['url', 'sameAs', 'logo', 'image'], true)) {
564 + foreach ($value as $item) {
565 + if (!is_string($item) || '' === trim($item)) {
566 + continue;
567 + }
568 +
569 + if (!$this->is_valid_url($item)) {
570 + $result['errors'][] = "Invalid URL format for field: {$field} ({$item})";
571 + $result['valid'] = false;
572 + }
573 + }
574 +
575 + continue;
576 + }
577 +
538 578 if (is_string($value)) {
539 579 // Validate URLs
540 580 if (in_array($field, ['url', 'sameAs', 'logo', 'image'], true) && !empty($value)) {
541 581 if (!$this->is_valid_url($value)) {
@@ -721,14 +761,25 @@
721 761 $result['errors'][] = 'Invalid user or user not logged in';
722 762 return $result;
723 763 }
724 764
725 - // Check operation-specific permissions
765 + // Check operation-specific permissions.
766 + //
767 + // #457 loosened the route permission_callbacks to the delegable
768 + // `thinkrank_schema` capability, but these handler-level checks still
769 + // demanded edit_posts / publish_posts / manage_options — so a role
770 + // granted Schema access could generate and validate but was denied on
771 + // deploy, bulk operations and everything site-context. That is exactly
772 + // the symptom #457 set out to fix (#470). A holder of thinkrank_schema
773 + // satisfies any schema operation; the built-in caps remain as the
774 + // fallback for roles that never went through the Role Manager.
775 + $has_schema_cap = user_can($user_id, 'thinkrank_schema');
776 +
726 777 switch ($operation) {
727 778 case 'generate':
728 779 case 'validate':
729 780 case 'optimize':
730 - if (!user_can($user_id, 'edit_posts')) {
781 + if (!$has_schema_cap && !user_can($user_id, 'edit_posts')) {
731 782 $result['errors'][] = 'Insufficient permissions for schema generation/validation';
732 783 return $result;
733 784 }
734 785 break;
@@ -733,9 +784,9 @@
733 784 }
734 785 break;
735 786
736 787 case 'deploy':
737 - if (!user_can($user_id, 'publish_posts')) {
788 + if (!$has_schema_cap && !user_can($user_id, 'publish_posts')) {
738 789 $result['errors'][] = 'Insufficient permissions for schema deployment';
739 790 return $result;
740 791 }
741 792 break;
@@ -741,9 +792,9 @@
741 792 break;
742 793
743 794 case 'manage_settings':
744 795 case 'bulk_operations':
745 - if (!user_can($user_id, 'manage_options')) {
796 + if (!$has_schema_cap && !user_can($user_id, 'manage_options')) {
746 797 $result['errors'][] = 'Insufficient permissions for schema management';
747 798 return $result;
748 799 }
749 800 break;
@@ -824,9 +875,14 @@
824 875 // SECURITY: Check site-level permissions for site context.
825 876 // user_can($user_id, …) rather than current_user_can() so this
826 877 // agrees with the rest of the validator outside a REST request,
827 878 // where the current user and $user_id can differ (cron, CLI).
828 - if (!$user_id || !user_can($user_id, 'manage_options')) {
879 + //
880 + // Accepts the delegable `thinkrank_schema` capability as well as
881 + // manage_options: /schema/settings already lets a delegated role
882 + // edit site schema settings, so blocking site-context generate and
883 + // deploy for the same role was inconsistent (#470).
884 + if (!$user_id || (!user_can($user_id, 'thinkrank_schema') && !user_can($user_id, 'manage_options'))) {
829 885 $result['errors'][] = 'Access denied: You need administrator privileges for site-level schema operations';
830 886 return $result;
831 887 }
832 888 }
@@ -901,14 +957,33 @@
901 957 * @return array Sanitized options
902 958 */
903 959 public function sanitize_options(array $options): array {
904 960 $sanitized = [];
961 + // Anything omitted here is dropped before the manager sees it, which is
962 + // why apply_content_schema_settings_from_options() and the per-request
963 + // schema-type opt-in were unreachable from REST (#470). The list now
964 + // covers every option the generate path actually reads.
965 + //
966 + // `validation_level` previously allowed 'basic' and rejected 'lenient',
967 + // disagreeing with Schema_Settings_Config, validate_settings() and the
968 + // update-settings ability, which all use 'lenient'.
969 + // `deployment_method` no longer advertises microdata/rdfa, which
970 + // determine_deployment_method() hardcodes away to json_ld anyway.
905 971 $allowed_options = [
906 - 'deployment_method' => ['json_ld', 'microdata', 'rdfa'],
907 - 'validation_level' => ['strict', 'moderate', 'basic'],
972 + 'deployment_method' => ['json_ld'],
973 + 'validation_level' => ['strict', 'moderate', 'lenient'],
908 974 'include_meta' => 'boolean',
909 975 'minify_output' => 'boolean',
910 - 'cache_duration' => 'integer'
976 + 'cache_duration' => 'integer',
977 + 'rich_snippets_optimization' => 'boolean',
978 + 'knowledge_graph' => 'boolean',
979 + 'auto_generate_schema' => 'boolean',
980 + 'enable_article_schema' => 'boolean',
981 + 'enable_faq_schema' => 'boolean',
982 + 'enable_howto_schema' => 'boolean',
983 + 'enable_product_schema' => 'boolean',
984 + 'enable_local_business' => 'boolean',
985 + 'enable_breadcrumbs_schema' => 'boolean',
911 986 ];
912 987
913 988 foreach ($options as $key => $value) {
914 989 $sanitized_key = sanitize_key($key);