PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.0
2.14.0 2.13.0 2.12.0 2.11.0 2.10.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 All 55 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.14.0, at includes/api/class-email-report-endpoint.php

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