PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.1.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 / api / class-author-archives-endpoint.php

class-author-archives-endpoint.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.1.0, at includes/api/class-author-archives-endpoint.php

251 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Author Archives API Endpoint Class
5 *
6 * REST API endpoints for author archives management
7 *
8 * @package ThinkRank
9 * @subpackage API
10 * @since 1.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\API;
16
17 use ThinkRank\Core\Settings;
18 use WP_REST_Controller;
19 use WP_REST_Request;
20 use WP_REST_Response;
21 use WP_Error;
22
23 // Prevent direct access
24 if (!defined('ABSPATH')) {
25 exit;
26 }
27
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.
36 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php';
37
38 /**
39 * Author Archives API Endpoint Class
40 *
41 * @since 1.0.0
42 */
43 class Author_Archives_Endpoint extends WP_REST_Controller {
44
45 /**
46 * API namespace
47 *
48 * @var string
49 */
50 protected $namespace = 'thinkrank/v1';
51
52 /**
53 * API resource base
54 *
55 * @var string
56 */
57 protected $rest_base = 'author-archives';
58
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 /**
83 * Register API routes
84 *
85 * @return void
86 */
87 public function register_routes(): void {
88 register_rest_route(
89 $this->namespace,
90 '/' . $this->rest_base . '/settings',
91 [
92 [
93 'methods' => 'GET',
94 'callback' => [$this, 'get_settings'],
95 'permission_callback' => [$this, 'check_permissions'],
96 ],
97 [
98 'methods' => 'POST',
99 'callback' => [$this, 'update_settings'],
100 'permission_callback' => [$this, 'check_permissions'],
101 'args' => [
102 'settings' => [
103 'required' => true,
104 'type' => 'object',
105 ],
106 ],
107 ],
108 ]
109 );
110 }
111
112 /**
113 * Get author archives settings
114 *
115 * @param WP_REST_Request $request Request object
116 * @return WP_REST_Response|WP_Error Response object
117 */
118 /**
119 * Get author archives settings
120 *
121 * @param WP_REST_Request $request Request object
122 * @return WP_REST_Response|WP_Error Response object
123 */
124 public function get_settings(WP_REST_Request $request) {
125 try {
126 return new WP_REST_Response([
127 'success' => true,
128 'data' => $this->get_current_settings(),
129 ], 200);
130 } catch (\Exception $e) {
131 return new WP_Error(
132 'retrieval_failed',
133 'Failed to retrieve settings: ' . $e->getMessage(),
134 ['status' => 500]
135 );
136 }
137 }
138
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 /**
161 * Update author archives settings
162 *
163 * @param WP_REST_Request $request Request object
164 * @return WP_REST_Response|WP_Error Response object
165 */
166 public function update_settings(WP_REST_Request $request) {
167 try {
168 $params = $request->get_param('settings');
169
170 if (!is_array($params)) {
171 return new WP_Error(
172 'invalid_params',
173 'Invalid settings format',
174 ['status' => 400]
175 );
176 }
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
194 $settings = Settings::instance();
195 $failed = [];
196
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 }
204 }
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 }
209 }
210
211 if (!empty($failed)) {
212 return new WP_Error(
213 'update_failed',
214 'Failed to update settings: ' . implode(', ', $failed),
215 ['status' => 500, 'failed_keys' => $failed]
216 );
217 }
218
219 return new WP_REST_Response([
220 'success' => true,
221 'message' => 'Settings saved successfully',
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(),
225 ], 200);
226 } catch (\Exception $e) {
227 return new WP_Error(
228 'update_failed',
229 'Failed to update settings: ' . $e->getMessage(),
230 ['status' => 500]
231 );
232 }
233 }
234
235 /**
236 * Check permissions
237 *
238 * @return bool|WP_Error
239 */
240 public function check_permissions() {
241 if (!\ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_author_archives')) {
242 return new WP_Error(
243 'rest_forbidden',
244 'You do not have permission to access this endpoint.',
245 ['status' => 403]
246 );
247 }
248 return true;
249 }
250 }
251