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/api/class-author-archives-endpoint.php +87 -31 1.26.02.7.0 View file →
@@ -13,9 +13,8 @@
13 13 declare(strict_types=1);
14 14
15 15 namespace ThinkRank\API;
16 16
17 -use ThinkRank\API\Traits\CSRF_Protection;
18 17 use ThinkRank\Core\Settings;
19 18 use WP_REST_Controller;
20 19 use WP_REST_Request;
21 20 use WP_REST_Response;
@@ -25,9 +24,16 @@
25 24 if (!defined('ABSPATH')) {
26 25 exit;
27 26 }
28 27
29 -// Load CSRF Protection trait
28 +// Load CSRF Protection trait.
29 +//
30 +// This class does not compose the trait — its routes are guarded by
31 +// check_permissions() plus WordPress' own X-WP-Nonce enforcement for cookie
32 +// auth — but the require stays: of the 13 endpoints that call trait methods
33 +// only 3 require the file themselves, so the other 10 depend on whichever
34 +// endpoint loaded first having pulled it in. Untangling that belongs in an
35 +// API-wide autoload pass, not here.
30 36 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php';
31 37
32 38 /**
33 39 * Author Archives API Endpoint Class
@@ -34,9 +40,8 @@
34 40 *
35 41 * @since 1.0.0
36 42 */
37 43 class Author_Archives_Endpoint extends WP_REST_Controller {
38 - use CSRF_Protection;
39 44
40 45 /**
41 46 * API namespace
42 47 *
@@ -51,8 +56,31 @@
51 56 */
52 57 protected $rest_base = 'author-archives';
53 58
54 59 /**
60 + * Boolean API field => Settings storage key.
61 + *
62 + * @since 1.29.1
63 + * @var array<string, string>
64 + */
65 + private const BOOL_SETTINGS = [
66 + 'enabled' => 'author_archives_enabled',
67 + 'show_in_search_results' => 'author_archives_index',
68 + 'show_empty_archives' => 'author_archives_show_empty',
69 + ];
70 +
71 + /**
72 + * String API field => Settings storage key.
73 + *
74 + * @since 1.29.1
75 + * @var array<string, string>
76 + */
77 + private const STRING_SETTINGS = [
78 + 'title' => 'author_archives_title',
79 + 'meta_description' => 'author_archives_meta_desc',
80 + ];
81 +
82 + /**
55 83 * Register API routes
56 84 *
57 85 * @return void
58 86 */
@@ -94,19 +122,11 @@
94 122 * @return WP_REST_Response|WP_Error Response object
95 123 */
96 124 public function get_settings(WP_REST_Request $request) {
97 125 try {
98 - $settings = Settings::instance();
99 -
100 126 return new WP_REST_Response([
101 127 'success' => true,
102 - 'data' => [
103 - 'enabled' => $settings->get('author_archives_enabled', true),
104 - 'show_in_search_results' => $settings->get('author_archives_index', true),
105 - 'show_empty_archives' => $settings->get('author_archives_show_empty', false),
106 - 'title' => $settings->get('author_archives_title', '%author_name% – %site_title% %page%'),
107 - 'meta_description' => $settings->get('author_archives_meta_desc', 'Articles written by %author_name% on %site_title%')
108 - ],
128 + 'data' => $this->get_current_settings(),
109 129 ], 200);
110 130 } catch (\Exception $e) {
111 131 return new WP_Error(
112 132 'retrieval_failed',
@@ -116,8 +136,29 @@
116 136 }
117 137 }
118 138
119 139 /**
140 + * Read the stored settings in API field shape.
141 + *
142 + * Shared by GET and by the POST response so a save always answers with the
143 + * same values a subsequent read would return.
144 + *
145 + * @since 1.29.1
146 + * @return array<string, bool|string>
147 + */
148 + private function get_current_settings(): array {
149 + $settings = Settings::instance();
150 +
151 + return [
152 + 'enabled' => (bool) $settings->get('author_archives_enabled', true),
153 + 'show_in_search_results' => (bool) $settings->get('author_archives_index', true),
154 + 'show_empty_archives' => (bool) $settings->get('author_archives_show_empty', false),
155 + 'title' => (string) $settings->get('author_archives_title', Settings::DEFAULT_AUTHOR_ARCHIVES_TITLE),
156 + 'meta_description' => (string) $settings->get('author_archives_meta_desc', Settings::DEFAULT_AUTHOR_ARCHIVES_META_DESC),
157 + ];
158 + }
159 +
160 + /**
120 161 * Update author archives settings
121 162 *
122 163 * @param WP_REST_Request $request Request object
123 164 * @return WP_REST_Response|WP_Error Response object
@@ -133,33 +174,46 @@
133 174 ['status' => 400]
134 175 );
135 176 }
136 177
178 + // Reject unrecognised fields rather than reporting them as saved.
179 + // A typo'd key used to come back inside a "saved successfully"
180 + // envelope while nothing was written.
181 + $unknown = array_diff(
182 + array_keys($params),
183 + array_keys(self::BOOL_SETTINGS),
184 + array_keys(self::STRING_SETTINGS)
185 + );
186 + if (!empty($unknown)) {
187 + return new WP_Error(
188 + 'invalid_params',
189 + 'Unknown setting keys: ' . implode(', ', $unknown),
190 + ['status' => 400]
191 + );
192 + }
193 +
137 194 $settings = Settings::instance();
138 - $success = true;
195 + $failed = [];
139 196
140 - // Map and save settings
141 - if (isset($params['enabled'])) {
142 - $success = $settings->set('author_archives_enabled', (bool) $params['enabled']);
197 + // Accumulate failures instead of overwriting $success on each
198 + // field — the old code let the last field's result decide the
199 + // whole response, so an earlier failure reported as a 200.
200 + foreach (self::BOOL_SETTINGS as $field => $key) {
201 + if (isset($params[$field]) && !$settings->set($key, (bool) $params[$field])) {
202 + $failed[] = $field;
203 + }
143 204 }
144 - if (isset($params['show_in_search_results'])) {
145 - $success = $settings->set('author_archives_index', (bool) $params['show_in_search_results']);
205 + foreach (self::STRING_SETTINGS as $field => $key) {
206 + if (isset($params[$field]) && !$settings->set($key, sanitize_text_field((string) $params[$field]))) {
207 + $failed[] = $field;
208 + }
146 209 }
147 - if (isset($params['show_empty_archives'])) {
148 - $success = $settings->set('author_archives_show_empty', (bool) $params['show_empty_archives']);
149 - }
150 - if (isset($params['title'])) {
151 - $success = $settings->set('author_archives_title', sanitize_text_field($params['title']));
152 - }
153 - if (isset($params['meta_description'])) {
154 - $success = $settings->set('author_archives_meta_desc', sanitize_text_field($params['meta_description']));
155 - }
156 210
157 - if (!$success) {
211 + if (!empty($failed)) {
158 212 return new WP_Error(
159 213 'update_failed',
160 - 'Failed to update settings',
161 - ['status' => 500]
214 + 'Failed to update settings: ' . implode(', ', $failed),
215 + ['status' => 500, 'failed_keys' => $failed]
162 216 );
163 217 }
164 218
165 219 return new WP_REST_Response([
@@ -164,9 +218,11 @@
164 218
165 219 return new WP_REST_Response([
166 220 'success' => true,
167 221 'message' => 'Settings saved successfully',
168 - 'data' => $params // Return back what was sent for UI consistency
222 + // Report what is actually stored, not an echo of the request.
223 + // Returning $params advertised unsanitized input as saved state.
224 + 'data' => $this->get_current_settings(),
169 225 ], 200);
170 226 } catch (\Exception $e) {
171 227 return new WP_Error(
172 228 'update_failed',