PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.0
2.9.0 2.8.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 All 50 releases
thinkrank / includes / api / class-email-report-endpoint.php

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

214 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email Report REST Endpoint
4 *
5 * Three routes:
6 * GET /thinkrank/v1/email-report/config — returns the resolved config + section catalog
7 * POST /thinkrank/v1/email-report/config — switches the report on or off
8 * POST /thinkrank/v1/email-report/test-send — triggers an immediate one-off send
9 *
10 * Permissions: admin (`manage_options`) + valid REST nonce.
11 *
12 * @package ThinkRank
13 * @subpackage API
14 * @since 1.9.0
15 */
16
17 declare(strict_types=1);
18
19 namespace ThinkRank\API;
20
21 use ThinkRank\API\Traits\CSRF_Protection;
22 use ThinkRank\SEO\Email_Report_Manager;
23 use WP_REST_Controller;
24 use WP_REST_Request;
25 use WP_REST_Response;
26 use WP_Error;
27
28 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-csrf-protection.php';
29
30 if (!defined('ABSPATH')) {
31 exit;
32 }
33
34 /**
35 * Email_Report_Endpoint
36 *
37 * @since 1.9.0
38 */
39 final class Email_Report_Endpoint extends WP_REST_Controller {
40 use CSRF_Protection;
41
42 protected $namespace = 'thinkrank/v1';
43 protected $rest_base = 'email-report';
44
45 private ?Email_Report_Manager $manager = null;
46
47 public function register_routes(): void {
48 register_rest_route(
49 $this->namespace,
50 '/' . $this->rest_base . '/config',
51 [
52 [
53 'methods' => 'GET',
54 'callback' => [$this, 'get_config'],
55 'permission_callback' => [$this, 'check_admin_read_permissions'],
56 ],
57 [
58 'methods' => 'POST',
59 'callback' => [$this, 'save_config'],
60 'permission_callback' => [$this, 'check_admin_csrf_permissions'],
61 'args' => [
62 'enabled' => [
63 'type' => 'boolean',
64 'sanitize_callback' => 'rest_sanitize_boolean',
65 ],
66 ],
67 ],
68 ]
69 );
70
71 register_rest_route(
72 $this->namespace,
73 '/' . $this->rest_base . '/test-send',
74 [
75 [
76 'methods' => 'POST',
77 'callback' => [$this, 'test_send'],
78 'permission_callback' => [$this, 'check_admin_csrf_permissions'],
79 ],
80 ]
81 );
82 }
83
84 /**
85 * GET /email-report/config
86 *
87 * Returns the resolved config (on/off, frequency, recipients, sections,
88 * last skip reason) along with the section catalog, the next scheduled
89 * run and the readiness of the data sources — whether Search Console is
90 * connected, so the panel can say a report is paused rather than let it
91 * look healthy (#742).
92 */
93 public function get_config(WP_REST_Request $request): WP_REST_Response {
94 $manager = $this->resolve_manager();
95 if ($manager === null) {
96 return new WP_REST_Response([
97 'error' => __('Email Report Manager unavailable.', 'thinkrank'),
98 ], 500);
99 }
100
101 return new WP_REST_Response([
102 'config' => $manager->config()->get(),
103 'sections' => $manager->registry()->describe_for_ui(),
104 'next_run' => $manager->scheduler()->next_run_iso(),
105 'readiness' => $manager->data_provider()->readiness(),
106 ]);
107 }
108
109 /**
110 * POST /email-report/config
111 */
112 public function save_config(WP_REST_Request $request): WP_REST_Response {
113 $manager = $this->resolve_manager();
114 if ($manager === null) {
115 return new WP_REST_Response([
116 'success' => false,
117 'message' => __('Email Report Manager unavailable.', 'thinkrank'),
118 ], 500);
119 }
120
121 $input = [];
122 if ($request->has_param('enabled')) {
123 $input['enabled'] = (bool) $request->get_param('enabled');
124 }
125
126 $saved = $manager->config()->save($input);
127
128 return new WP_REST_Response([
129 'success' => true,
130 'config' => $saved,
131 'next_run' => $manager->scheduler()->next_run_iso(),
132 'readiness' => $manager->data_provider()->readiness(),
133 ]);
134 }
135
136 /**
137 * POST /email-report/test-send
138 */
139 public function test_send(WP_REST_Request $request): WP_REST_Response {
140 $manager = $this->resolve_manager();
141 if ($manager === null) {
142 return new WP_REST_Response([
143 'success' => false,
144 'message' => __('Email Report Manager unavailable.', 'thinkrank'),
145 ], 500);
146 }
147
148 $result = $manager->generator()->generate_test();
149
150 $status = !empty($result['success']) ? 200 : 400;
151 return new WP_REST_Response([
152 'success' => (bool) ($result['success'] ?? false),
153 // True when the test went out as the "connect Search Console"
154 // email rather than a report, so the panel can say so.
155 'not_connected' => !empty($result['not_connected']),
156 'result' => $result,
157 ], $status);
158 }
159
160 /**
161 * Permission for read endpoints. Same admin gate, but no CSRF
162 * (GET requests don't require it).
163 */
164 public function check_admin_read_permissions(WP_REST_Request $request) {
165 if (!is_user_logged_in()) {
166 return new WP_Error('rest_forbidden', __('Not logged in.', 'thinkrank'), ['status' => 401]);
167 }
168 if (!current_user_can('manage_options')) {
169 return new WP_Error('rest_forbidden', __('Insufficient permissions.', 'thinkrank'), ['status' => 403]);
170 }
171 return true;
172 }
173
174 /**
175 * Permission for the state-changing POST endpoints (save config / test-send).
176 *
177 * These write the site-global report config and can trigger a send of private
178 * analytics, so they require admin (manage_options) plus CSRF verification —
179 * NOT the shared edit_posts-level check_csrf_permissions() trait, which would
180 * let a Contributor overwrite the config and exfiltrate the report. Matches the
181 * manage_options gate on the GET route.
182 */
183 public function check_admin_csrf_permissions(WP_REST_Request $request) {
184 if (!is_user_logged_in()) {
185 return new WP_Error('rest_forbidden', __('Not logged in.', 'thinkrank'), ['status' => 401]);
186 }
187 if (!current_user_can('manage_options')) {
188 return new WP_Error('rest_forbidden', __('Insufficient permissions.', 'thinkrank'), ['status' => 403]);
189 }
190 if (!$this->verify_request_nonce($request)) {
191 return new WP_Error('rest_forbidden', __('Invalid security token. Please refresh the page and try again.', 'thinkrank'), ['status' => 403]);
192 }
193 return true;
194 }
195
196 /**
197 * Reach into the plugin DI container for the Email_Report_Manager
198 * instance built at boot.
199 */
200 private function resolve_manager(): ?Email_Report_Manager {
201 if ($this->manager !== null) {
202 return $this->manager;
203 }
204 if (function_exists('thinkrank')) {
205 $component = thinkrank()->get_component('email_report');
206 if ($component instanceof Email_Report_Manager) {
207 $this->manager = $component;
208 return $this->manager;
209 }
210 }
211 return null;
212 }
213 }
214