PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.3.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.3.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
thinkrank / includes / abilities / settings / class-update-global-settings.php

class-update-global-settings.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.3.0, at includes/abilities/settings/class-update-global-settings.php

180 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Update global settings ability.
4 *
5 * @package ThinkRank\Abilities\Settings
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Settings;
11
12 use ThinkRank\Abilities\Ability_Base;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17
18 /**
19 * Updates ThinkRank global SEO settings.
20 *
21 * ThinkRank stores global SEO as per-post-type templates in the
22 * `thinkrank_global_seo_settings` option. This ability therefore requires a
23 * `post_type` and merges the provided settings into that post type's template.
24 */
25 class Update_Global_Settings extends Ability_Base {
26 /**
27 * Option storing per-post-type global SEO templates.
28 */
29 private const OPTION_NAME = 'thinkrank_global_seo_settings';
30
31 /**
32 * Constructor.
33 */
34 public function __construct() {
35 $this->id = 'thinkrank/update-global-settings';
36 $this->label = __( 'Update ThinkRank Global Settings', 'thinkrank' );
37 $this->description = __( 'Update ThinkRank global SEO settings for a specific post type. ThinkRank stores global SEO as per-post-type templates, so a post type is required. Read the current values with get-global-settings first; only the keys you pass are changed.', 'thinkrank' );
38 }
39
40 /**
41 * {@inheritDoc}
42 *
43 * @return array<string, bool|float|string>
44 */
45 public function get_annotations() {
46 return [
47 'readonly' => false,
48 'destructive' => true,
49 'idempotent' => true,
50 'priority' => 2.0,
51 'openWorldHint' => false,
52 ];
53 }
54
55 /**
56 * {@inheritDoc}
57 *
58 * @return array<string, mixed>
59 */
60 public function get_input_schema() {
61 return [
62 'type' => 'object',
63 'additionalProperties' => false,
64 'properties' => [
65 'post_type' => [
66 'type' => 'string',
67 'description' => __( 'The post type whose global SEO template to update.', 'thinkrank' ),
68 ],
69 'settings' => [
70 'type' => 'object',
71 'description' => __( 'Key-value pairs of ThinkRank global settings to merge into the post type template.', 'thinkrank' ),
72 ],
73 ],
74 'required' => [ 'post_type', 'settings' ],
75 ];
76 }
77
78 /**
79 * {@inheritDoc}
80 *
81 * @return array<string, mixed>
82 */
83 public function get_output_schema() {
84 return [
85 'type' => 'object',
86 'properties' => [
87 'success' => [ 'type' => 'boolean' ],
88 'message' => [ 'type' => 'string' ],
89 'post_type' => [ 'type' => 'string' ],
90 ],
91 ];
92 }
93
94 /**
95 * Execute ability.
96 *
97 * @param array<string, mixed> $input Ability input payload.
98 * @return array<string, mixed>|\WP_Error
99 */
100 public function execute( $input ) {
101 $post_type = isset( $input['post_type'] ) ? sanitize_key( (string) $input['post_type'] ) : '';
102 $settings = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
103
104 if ( '' === $post_type || ! post_type_exists( $post_type ) ) {
105 return new \WP_Error(
106 'thinkrank_invalid_post_type',
107 __( 'A valid post type slug is required.', 'thinkrank' ),
108 [ 'status' => 400 ]
109 );
110 }
111
112 // Apply the shared Global SEO target policy (public + viewable + not on
113 // the deny list) so the ability rejects the same types the REST endpoint
114 // and the admin UI do.
115 if ( ! \ThinkRank\SEO\Global_SEO_Post_Types::is_allowed( $post_type ) ) {
116 return new \WP_Error(
117 'thinkrank_non_public_post_type',
118 __( 'Global SEO settings are only available for valid Global SEO target post types.', 'thinkrank' ),
119 [ 'status' => 400 ]
120 );
121 }
122
123 if ( empty( $settings ) ) {
124 return new \WP_Error(
125 'thinkrank_missing_settings_payload',
126 __( 'A settings payload is required.', 'thinkrank' ),
127 [ 'status' => 400 ]
128 );
129 }
130
131 // Normalize through the SAME per-field contract the REST endpoint uses so
132 // both write paths coerce identically (booleans, enums, clamped numerics,
133 // nested robots subkeys) and neither can persist string booleans or
134 // unknown nested keys.
135 $settings = \ThinkRank\API\Global_SEO_Endpoint::normalize_settings_patch( $settings );
136
137 $all = get_option( self::OPTION_NAME, [] );
138 if ( ! is_array( $all ) ) {
139 $all = [];
140 }
141
142 // Patch contract: this ability performs a PARTIAL update. Top-level keys
143 // in the payload replace the stored value; the two nested robots
144 // structures (robots_meta, advanced_robots_meta) are merged subkey-wise so
145 // a partial nested patch preserves sibling subkeys instead of wiping them.
146 $existing = isset( $all[ $post_type ] ) && is_array( $all[ $post_type ] ) ? $all[ $post_type ] : [];
147 $merged = $existing;
148 foreach ( $settings as $key => $value ) {
149 if (
150 in_array( $key, [ 'robots_meta', 'advanced_robots_meta' ], true )
151 && isset( $existing[ $key ] ) && is_array( $existing[ $key ] ) && is_array( $value )
152 ) {
153 $merged[ $key ] = array_merge( $existing[ $key ], $value );
154 } else {
155 $merged[ $key ] = $value;
156 }
157 }
158 $unchanged = ( $merged === $existing );
159 $all[ $post_type ] = $merged;
160
161 $updated = update_option( self::OPTION_NAME, $all );
162
163 // update_option() returns false both on a write failure and on a no-op
164 // (value unchanged). Only the former is an error.
165 if ( ! $updated && ! $unchanged ) {
166 return new \WP_Error(
167 'thinkrank_save_failed',
168 __( 'Failed to save global SEO settings.', 'thinkrank' ),
169 [ 'status' => 500 ]
170 );
171 }
172
173 return [
174 'success' => true,
175 'message' => __( 'Global SEO settings updated.', 'thinkrank' ),
176 'post_type' => $post_type,
177 ];
178 }
179 }
180