PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.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-ai-insights-endpoint.php

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

322 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Insights API endpoints.
4 *
5 * Admin REST surface for the AI Insights screen (#248 P1 trio):
6 *
7 * GET /ai-insights/traffic — AI referral/crawler dashboard summary
8 * GET /ai-insights/brand — saved queries + check history
9 * POST /ai-insights/brand — save the query list
10 * POST /ai-insights/brand/run — run checks now (paid AI calls, capped)
11 * GET /ai-insights/auto-ai — auto-optimization settings + last run
12 * POST /ai-insights/auto-ai — save auto-optimization settings
13 *
14 * @package ThinkRank
15 * @subpackage API
16 * @since 1.27.0
17 */
18
19 declare(strict_types=1);
20
21 namespace ThinkRank\API;
22
23 use ThinkRank\AI\Brand_Visibility_Checker;
24 use ThinkRank\Core\Settings;
25 use ThinkRank\SEO\Ai_Traffic_Tracker;
26 use ThinkRank\SEO\Auto_Ai_Optimizer;
27 use WP_REST_Controller;
28 use WP_REST_Request;
29 use WP_REST_Response;
30
31 // Prevent direct access
32 if (!defined('ABSPATH')) {
33 exit;
34 }
35
36 /**
37 * REST controller for AI traffic, brand visibility, and auto AI settings.
38 */
39 class Ai_Insights_Endpoint extends WP_REST_Controller {
40
41 /**
42 * API namespace
43 *
44 * @var string
45 */
46 protected $namespace = 'thinkrank/v1';
47
48 /**
49 * API resource base
50 *
51 * @var string
52 */
53 protected $rest_base = 'ai-insights';
54
55 /**
56 * Register routes.
57 *
58 * @return void
59 */
60 public function register_routes(): void {
61 register_rest_route($this->namespace, '/' . $this->rest_base . '/traffic', [
62 'methods' => 'GET',
63 'callback' => [$this, 'get_traffic'],
64 'permission_callback' => [$this, 'check_admin_permissions'],
65 'args' => [
66 'days' => [
67 'required' => false,
68 'type' => 'integer',
69 'default' => 30,
70 'minimum' => 1,
71 'maximum' => 180,
72 ],
73 ],
74 ]);
75
76 register_rest_route($this->namespace, '/' . $this->rest_base . '/brand', [
77 [
78 'methods' => 'GET',
79 'callback' => [$this, 'get_brand'],
80 'permission_callback' => [$this, 'check_admin_permissions'],
81 ],
82 [
83 'methods' => 'POST',
84 'callback' => [$this, 'save_brand_queries'],
85 'permission_callback' => [$this, 'check_admin_permissions'],
86 'args' => [
87 'queries' => [
88 'required' => true,
89 'type' => 'array',
90 'items' => ['type' => 'string'],
91 'sanitize_callback' => static function ($value) {
92 if (!is_array($value)) {
93 return [];
94 }
95 $clean = array_values(array_filter(array_map(
96 static fn($q) => sanitize_text_field((string) $q),
97 $value
98 )));
99 // Clamp to the plan's cap, not the hard ceiling, so
100 // free can't persist queries it may not run.
101 return array_slice($clean, 0, Brand_Visibility_Checker::query_limit());
102 },
103 ],
104 ],
105 ],
106 ]);
107
108 register_rest_route($this->namespace, '/' . $this->rest_base . '/brand/run', [
109 'methods' => 'POST',
110 'callback' => [$this, 'run_brand_checks'],
111 'permission_callback' => [$this, 'check_admin_permissions'],
112 ]);
113
114 register_rest_route($this->namespace, '/' . $this->rest_base . '/brand/history/(?P<id>\d+)', [
115 'methods' => 'DELETE',
116 'callback' => [$this, 'delete_brand_history'],
117 'permission_callback' => [$this, 'check_admin_permissions'],
118 'args' => [
119 'id' => [
120 'required' => true,
121 'type' => 'integer',
122 'minimum' => 1,
123 ],
124 ],
125 ]);
126
127 register_rest_route($this->namespace, '/' . $this->rest_base . '/auto-ai', [
128 [
129 'methods' => 'GET',
130 'callback' => [$this, 'get_auto_ai'],
131 'permission_callback' => [$this, 'check_admin_permissions'],
132 ],
133 [
134 'methods' => 'POST',
135 'callback' => [$this, 'save_auto_ai'],
136 'permission_callback' => [$this, 'check_admin_permissions'],
137 'args' => [
138 'enabled' => [
139 'required' => false,
140 'type' => 'boolean',
141 ],
142 'post_types' => [
143 'required' => false,
144 'type' => 'array',
145 'items' => ['type' => 'string'],
146 'sanitize_callback' => static function ($value) {
147 if (!is_array($value)) {
148 return [];
149 }
150 // Only real, public post types survive.
151 return array_values(array_filter(
152 array_map('sanitize_key', $value),
153 static fn($t) => post_type_exists($t)
154 ));
155 },
156 ],
157 ],
158 ],
159 ]);
160 }
161
162 /**
163 * Admin permission gate (matches the other admin-only endpoints).
164 *
165 * @return bool
166 */
167 public function check_admin_permissions(): bool {
168 return current_user_can('manage_options');
169 }
170
171 /**
172 * AI traffic dashboard summary.
173 *
174 * @param WP_REST_Request $request Request.
175 * @return WP_REST_Response
176 */
177 public function get_traffic(WP_REST_Request $request): WP_REST_Response {
178 $tracker = new Ai_Traffic_Tracker();
179
180 return new WP_REST_Response([
181 'success' => true,
182 'data' => $tracker->summary((int) $request->get_param('days')),
183 ], 200);
184 }
185
186 /**
187 * Saved brand queries + history.
188 *
189 * @return WP_REST_Response
190 */
191 public function get_brand(): WP_REST_Response {
192 $checker = new Brand_Visibility_Checker();
193
194 return new WP_REST_Response([
195 'success' => true,
196 'data' => [
197 'queries' => $checker->queries(),
198 'history' => $checker->history(),
199 // The plan's cap, not the hard ceiling — the UI renders the
200 // limit the user can actually use.
201 'max_queries' => Brand_Visibility_Checker::query_limit(),
202 'plan' => \ThinkRank\Core\Plan_Config::ai_visibility(),
203 'is_pro' => \ThinkRank\Core\Plan_Config::is_pro(),
204 'brand' => (string) get_bloginfo('name'),
205 'host' => (string) wp_parse_url(home_url(), PHP_URL_HOST),
206 ],
207 ], 200);
208 }
209
210 /**
211 * Save the brand query list.
212 *
213 * @param WP_REST_Request $request Request.
214 * @return WP_REST_Response
215 */
216 public function save_brand_queries(WP_REST_Request $request): WP_REST_Response {
217 $queries = (array) $request->get_param('queries');
218 Settings::instance()->set('brand_visibility_queries', $queries);
219
220 return new WP_REST_Response([
221 'success' => true,
222 'data' => ['queries' => $queries],
223 ], 200);
224 }
225
226 /**
227 * Run brand checks now.
228 *
229 * @return WP_REST_Response
230 */
231 public function run_brand_checks(): WP_REST_Response {
232 try {
233 $checker = new Brand_Visibility_Checker();
234 $results = $checker->run();
235 } catch (\Exception $e) {
236 return new WP_REST_Response([
237 'success' => false,
238 'message' => $e->getMessage(),
239 ], 400);
240 }
241
242 return new WP_REST_Response([
243 'success' => true,
244 'data' => [
245 'results' => $results,
246 'history' => (new Brand_Visibility_Checker())->history(),
247 ],
248 ], 200);
249 }
250
251 /**
252 * Delete a single brand check-history row.
253 *
254 * @param WP_REST_Request $request Request.
255 * @return WP_REST_Response
256 */
257 public function delete_brand_history(WP_REST_Request $request): WP_REST_Response {
258 $id = (int) $request->get_param('id');
259 $checker = new Brand_Visibility_Checker();
260 $deleted = $checker->delete_history($id);
261
262 return new WP_REST_Response([
263 'success' => $deleted,
264 'data' => [
265 'id' => $id,
266 'history' => $checker->history(),
267 ],
268 ], $deleted ? 200 : 404);
269 }
270
271 /**
272 * Auto AI settings + last run outcome.
273 *
274 * @return WP_REST_Response
275 */
276 public function get_auto_ai(): WP_REST_Response {
277 $settings = Settings::instance();
278
279 return new WP_REST_Response([
280 'success' => true,
281 'data' => [
282 'enabled' => (bool) $settings->get('auto_ai_meta_enabled', false),
283 'post_types' => (array) $settings->get('auto_ai_meta_post_types', ['post']),
284 'last_run' => get_option(Auto_Ai_Optimizer::LAST_RUN_OPTION, null),
285 // Pro-gated: the UI locks the toggle and shows the upsell.
286 'available' => \ThinkRank\Core\Plan_Config::can('auto_ai_meta', 'ai_visibility'),
287 ],
288 ], 200);
289 }
290
291 /**
292 * Save auto AI settings.
293 *
294 * @param WP_REST_Request $request Request.
295 * @return WP_REST_Response
296 */
297 public function save_auto_ai(WP_REST_Request $request): WP_REST_Response {
298 $settings = Settings::instance();
299
300 if (null !== $request->get_param('enabled')) {
301 $enabled = rest_sanitize_boolean($request->get_param('enabled'));
302
303 // Pro capability. Turning it OFF always works — a lapsed licence
304 // must never trap the setting in the on position.
305 if ($enabled && !\ThinkRank\Core\Plan_Config::can('auto_ai_meta', 'ai_visibility')) {
306 return new WP_REST_Response([
307 'success' => false,
308 'message' => __('Automatic AI metadata is a ThinkRank Pro feature.', 'thinkrank'),
309 'data' => ['requires_pro' => true],
310 ], 403);
311 }
312
313 $settings->set('auto_ai_meta_enabled', $enabled);
314 }
315 if (null !== $request->get_param('post_types')) {
316 $settings->set('auto_ai_meta_post_types', (array) $request->get_param('post_types'));
317 }
318
319 return $this->get_auto_ai();
320 }
321 }
322