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
← All changes | includes/api/class-email-report-endpoint.php +39 -82 2.4.0 → 2.14.0 View file →
@@ -2,16 +2,13 @@
2 2 /**
3 3 * Email Report REST Endpoint
4 4 *
5 5 * Three routes:
6 - * GET /thinkrank/v1/email-report/config — returns config + capability map + section catalog
7 - * POST /thinkrank/v1/email-report/config — saves config (sanitized + capability-clamped)
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 8 * POST /thinkrank/v1/email-report/test-send — triggers an immediate one-off send
9 9 *
10 - * Permissions: admin (`manage_options`) + valid REST nonce. Pro-only
11 - * fields submitted on a free plan are silently dropped by
12 - * Email_Report_Config::sanitize() — we don't 403 on those, since the
13 - * client may not know its current capabilities yet (e.g. mid-downgrade).
10 + * Permissions: admin (`manage_options`) + valid REST nonce.
14 11 *
15 12 * @package ThinkRank
16 13 * @subpackage API
17 14 * @since 1.9.0
@@ -21,9 +18,9 @@
21 18
22 19 namespace ThinkRank\API;
23 20
24 21 use ThinkRank\API\Traits\CSRF_Protection;
25 -use ThinkRank\Core\Plan_Config;
22 +use ThinkRank\Core\Capability_Manager;
26 23 use ThinkRank\SEO\Email_Report_Manager;
27 24 use WP_REST_Controller;
28 25 use WP_REST_Request;
29 26 use WP_REST_Response;
@@ -61,9 +58,14 @@
61 58 [
62 59 'methods' => 'POST',
63 60 'callback' => [$this, 'save_config'],
64 61 'permission_callback' => [$this, 'check_admin_csrf_permissions'],
65 - 'args' => $this->save_args(),
62 + 'args' => [
63 + 'enabled' => [
64 + 'type' => 'boolean',
65 + 'sanitize_callback' => 'rest_sanitize_boolean',
66 + ],
67 + ],
66 68 ],
67 69 ]
68 70 );
69 71
@@ -82,11 +84,13 @@
82 84
83 85 /**
84 86 * GET /email-report/config
85 87 *
86 - * Returns the current per-site config along with the plan's capability
87 - * map and the section catalog so the React panel can render the right
88 - * fields without a second round-trip.
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).
89 93 */
90 94 public function get_config(WP_REST_Request $request): WP_REST_Response {
91 95 $manager = $this->resolve_manager();
92 96 if ($manager === null) {
@@ -95,13 +99,12 @@
95 99 ], 500);
96 100 }
97 101
98 102 return new WP_REST_Response([
99 - 'config' => $manager->config()->get(),
100 - 'capabilities' => Plan_Config::email_report(),
101 - 'sections' => $manager->registry()->describe_for_ui(),
102 - 'next_run' => $manager->scheduler()->next_run_iso(),
103 - 'tokens' => $this->supported_tokens(),
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(),
104 107 ]);
105 108 }
106 109
107 110 /**
@@ -115,20 +118,20 @@
115 118 'message' => __('Email Report Manager unavailable.', 'thinkrank'),
116 119 ], 500);
117 120 }
118 121
119 - $input = $request->get_json_params();
120 - if (!is_array($input)) {
121 - $input = $request->get_params();
122 + $input = [];
123 + if ($request->has_param('enabled')) {
124 + $input['enabled'] = (bool) $request->get_param('enabled');
122 125 }
123 126
124 - $saved = $manager->config()->save(is_array($input) ? $input : []);
127 + $saved = $manager->config()->save($input);
125 128
126 129 return new WP_REST_Response([
127 - 'success' => true,
128 - 'config' => $saved,
129 - 'capabilities' => Plan_Config::email_report(),
130 - 'next_run' => $manager->scheduler()->next_run_iso(),
130 + 'success' => true,
131 + 'config' => $saved,
132 + 'next_run' => $manager->scheduler()->next_run_iso(),
133 + 'readiness' => $manager->data_provider()->readiness(),
131 134 ]);
132 135 }
133 136
134 137 /**
@@ -146,10 +149,13 @@
146 149 $result = $manager->generator()->generate_test();
147 150
148 151 $status = !empty($result['success']) ? 200 : 400;
149 152 return new WP_REST_Response([
150 - 'success' => (bool) ($result['success'] ?? false),
151 - 'result' => $result,
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,
152 158 ], $status);
153 159 }
154 160
155 161 /**
@@ -159,9 +165,11 @@
159 165 public function check_admin_read_permissions(WP_REST_Request $request) {
160 166 if (!is_user_logged_in()) {
161 167 return new WP_Error('rest_forbidden', __('Not logged in.', 'thinkrank'), ['status' => 401]);
162 168 }
163 - if (!current_user_can('manage_options')) {
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')) {
164 172 return new WP_Error('rest_forbidden', __('Insufficient permissions.', 'thinkrank'), ['status' => 403]);
165 173 }
166 174 return true;
167 175 }
@@ -178,9 +186,11 @@
178 186 public function check_admin_csrf_permissions(WP_REST_Request $request) {
179 187 if (!is_user_logged_in()) {
180 188 return new WP_Error('rest_forbidden', __('Not logged in.', 'thinkrank'), ['status' => 401]);
181 189 }
182 - if (!current_user_can('manage_options')) {
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')) {
183 193 return new WP_Error('rest_forbidden', __('Insufficient permissions.', 'thinkrank'), ['status' => 403]);
184 194 }
185 195 if (!$this->verify_request_nonce($request)) {
186 196 return new WP_Error('rest_forbidden', __('Invalid security token. Please refresh the page and try again.', 'thinkrank'), ['status' => 403]);
@@ -189,10 +199,9 @@
189 199 }
190 200
191 201 /**
192 202 * Reach into the plugin DI container for the Email_Report_Manager
193 - * instance built at boot. Falls back to creating one on demand if
194 - * the function doesn't exist yet (defensive — shouldn't happen).
203 + * instance built at boot.
195 204 */
196 205 private function resolve_manager(): ?Email_Report_Manager {
197 206 if ($this->manager !== null) {
198 207 return $this->manager;
@@ -204,58 +213,6 @@
204 213 return $this->manager;
205 214 }
206 215 }
207 216 return null;
208 - }
209 -
210 - /**
211 - * REST args: permissive on type so we accept the full config object
212 - * the panel sends back (including nulls for paid fields the user
213 - * isn't allowed to set). Heavy sanitization happens in
214 - * Email_Report_Config::sanitize() so the cron path benefits too.
215 - *
216 - * Don't add `sanitize_callback` here for nullable fields — the
217 - * sanitized value is what reaches the handler, and
218 - * `esc_url_raw(null)` coerces to '', defeating the point of
219 - * preserving "unset". (WP_REST_Server::respond_to_request runs
220 - * has_valid_params() first and sanitize_params() second, so the
221 - * ['string','null'] type above is what admits the null; sanitizing
222 - * afterwards would throw it away.)
223 - */
224 - private function save_args(): array {
225 - $nullable_string = ['type' => ['string', 'null']];
226 - return [
227 - 'enabled' => [
228 - 'type' => 'boolean',
229 - 'sanitize_callback' => 'rest_sanitize_boolean',
230 - ],
231 - 'frequency_days' => [
232 - 'type' => 'integer',
233 - 'sanitize_callback' => 'absint',
234 - ],
235 - 'recipients' => [
236 - 'type' => ['array', 'string', 'null'],
237 - ],
238 - 'subject_template' => $nullable_string,
239 - 'logo_url' => $nullable_string,
240 - 'logo_link' => $nullable_string,
241 - 'header_background' => $nullable_string,
242 - 'link_to_full_report' => [
243 - 'type' => 'boolean',
244 - 'sanitize_callback' => 'rest_sanitize_boolean',
245 - ],
246 - 'intro_text' => $nullable_string,
247 - 'sections_enabled' => [
248 - 'type' => ['array', 'null'],
249 - ],
250 - 'footer_text' => $nullable_string,
251 - 'additional_css' => $nullable_string,
252 - ];
253 - }
254 -
255 - private function supported_tokens(): array {
256 - if (!function_exists('thinkrank_get_email_report_tokens')) {
257 - require_once THINKRANK_PLUGIN_DIR . 'includes/config/email-report-settings-config.php';
258 - }
259 - return thinkrank_get_email_report_tokens();
260 217 }
261 218 }