← All changes
|
includes/api/class-instant-indexing-endpoint.php
+152
-10
1.28.0
→
2.9.0
View file →
| @@ -15,13 +15,19 @@ | ||
| 15 | 15 | |
| 16 | 16 | namespace ThinkRank\API; |
| 17 | 17 | |
| 18 | 18 | use ThinkRank\Core\Settings; |
| 19 | +use ThinkRank\SEO\Instant_Indexing_Reconciler; | |
| 19 | 20 | use WP_REST_Controller; |
| 20 | 21 | use WP_REST_Request; |
| 21 | 22 | use WP_REST_Response; |
| 22 | 23 | use WP_Error; |
| 23 | 24 | |
| 25 | +// Prevent direct access | |
| 26 | +if (!defined('ABSPATH')) { | |
| 27 | + exit; | |
| 28 | +} | |
| 29 | + | |
| 24 | 30 | /** |
| 25 | 31 | * Instant Indexing API Endpoints Class |
| 26 | 32 | * |
| 27 | 33 | * Provides REST API endpoints for Instant Indexing operations. |
| @@ -54,8 +60,16 @@ | ||
| 54 | 60 | */ |
| 55 | 61 | private $option_name = 'thinkrank_instant_indexing_settings'; |
| 56 | 62 | |
| 57 | 63 | /** |
| 64 | + * Reconciler instance | |
| 65 | + * | |
| 66 | + * @since 1.31.0 | |
| 67 | + * @var Instant_Indexing_Reconciler|null | |
| 68 | + */ | |
| 69 | + private ?Instant_Indexing_Reconciler $reconciler = null; | |
| 70 | + | |
| 71 | + /** | |
| 58 | 72 | * Register API routes |
| 59 | 73 | * |
| 60 | 74 | * @since 1.0.0 |
| 61 | 75 | */ |
| @@ -150,9 +164,25 @@ | ||
| 150 | 164 | 'limit' => [ |
| 151 | 165 | 'required' => false, |
| 152 | 166 | 'type' => 'integer', |
| 153 | 167 | 'default' => -1 |
| 154 | - ] | |
| 168 | + ], | |
| 169 | + // Both are read by get_submission_history() and neither | |
| 170 | + // was registered, so they arrived uncoerced and | |
| 171 | + // unbounded (#394). | |
| 172 | + 'page' => [ | |
| 173 | + 'required' => false, | |
| 174 | + 'type' => 'integer', | |
| 175 | + 'default' => 1, | |
| 176 | + 'minimum' => 1, | |
| 177 | + ], | |
| 178 | + 'per_page' => [ | |
| 179 | + 'required' => false, | |
| 180 | + 'type' => 'integer', | |
| 181 | + 'default' => 20, | |
| 182 | + 'minimum' => 1, | |
| 183 | + 'maximum' => 100, | |
| 184 | + ], | |
| 155 | 185 | ] |
| 156 | 186 | ], |
| 157 | 187 | [ |
| 158 | 188 | 'methods' => 'DELETE', |
| @@ -160,11 +190,115 @@ | ||
| 160 | 190 | 'permission_callback' => [$this, 'check_manage_permissions'] |
| 161 | 191 | ] |
| 162 | 192 | ] |
| 163 | 193 | ); |
| 194 | + | |
| 195 | + // Coverage report: which published URLs IndexNow actually knows about. | |
| 196 | + register_rest_route( | |
| 197 | + $this->namespace, | |
| 198 | + '/' . $this->rest_base . '/coverage', | |
| 199 | + [ | |
| 200 | + [ | |
| 201 | + 'methods' => 'GET', | |
| 202 | + 'callback' => [$this, 'get_coverage_report'], | |
| 203 | + 'permission_callback' => [$this, 'check_read_permissions'], | |
| 204 | + 'args' => [ | |
| 205 | + 'limit' => [ | |
| 206 | + 'required' => false, | |
| 207 | + 'type' => 'integer', | |
| 208 | + 'default' => Instant_Indexing_Reconciler::REPORT_LIMIT, | |
| 209 | + 'minimum' => 1, | |
| 210 | + 'maximum' => 2000 | |
| 211 | + ], | |
| 212 | + 'offset' => [ | |
| 213 | + 'required' => false, | |
| 214 | + 'type' => 'integer', | |
| 215 | + 'default' => 0, | |
| 216 | + 'minimum' => 0 | |
| 217 | + ] | |
| 218 | + ] | |
| 219 | + ] | |
| 220 | + ] | |
| 221 | + ); | |
| 222 | + | |
| 223 | + // Run a reconciliation pass now instead of waiting for the daily cron. | |
| 224 | + register_rest_route( | |
| 225 | + $this->namespace, | |
| 226 | + '/' . $this->rest_base . '/reconcile', | |
| 227 | + [ | |
| 228 | + [ | |
| 229 | + 'methods' => 'POST', | |
| 230 | + // Resubmits URLs to a third party, so this needs the manage | |
| 231 | + // capability rather than the read one. | |
| 232 | + 'callback' => [$this, 'run_reconciliation'], | |
| 233 | + 'permission_callback' => [$this, 'check_manage_permissions'], | |
| 234 | + 'args' => [ | |
| 235 | + 'dry_run' => [ | |
| 236 | + 'required' => false, | |
| 237 | + 'type' => 'boolean', | |
| 238 | + 'default' => false | |
| 239 | + ] | |
| 240 | + ] | |
| 241 | + ] | |
| 242 | + ] | |
| 243 | + ); | |
| 164 | 244 | } |
| 165 | 245 | |
| 166 | 246 | /** |
| 247 | + * Get the IndexNow coverage report. | |
| 248 | + * | |
| 249 | + * @since 1.31.0 | |
| 250 | + * | |
| 251 | + * @param WP_REST_Request $request Request object | |
| 252 | + * @return WP_REST_Response Response object | |
| 253 | + */ | |
| 254 | + public function get_coverage_report(WP_REST_Request $request): WP_REST_Response { | |
| 255 | + $report = $this->get_reconciler()->build_report( | |
| 256 | + (int) $request->get_param('limit'), | |
| 257 | + (int) $request->get_param('offset') | |
| 258 | + ); | |
| 259 | + | |
| 260 | + return new WP_REST_Response([ | |
| 261 | + 'success' => true, | |
| 262 | + 'data' => $report, | |
| 263 | + ], 200); | |
| 264 | + } | |
| 265 | + | |
| 266 | + /** | |
| 267 | + * Run a reconciliation pass on demand. | |
| 268 | + * | |
| 269 | + * @since 1.31.0 | |
| 270 | + * | |
| 271 | + * @param WP_REST_Request $request Request object | |
| 272 | + * @return WP_REST_Response Response object | |
| 273 | + */ | |
| 274 | + public function run_reconciliation(WP_REST_Request $request): WP_REST_Response { | |
| 275 | + $summary = $this->get_reconciler()->reconcile((bool) $request->get_param('dry_run')); | |
| 276 | + | |
| 277 | + return new WP_REST_Response([ | |
| 278 | + 'success' => true, | |
| 279 | + 'data' => $summary, | |
| 280 | + 'message' => $summary['ran'] | |
| 281 | + ? sprintf('Reconciliation complete: %d URLs examined, %d resubmitted.', $summary['examined'], $summary['retried']) | |
| 282 | + : $summary['reason'], | |
| 283 | + ], 200); | |
| 284 | + } | |
| 285 | + | |
| 286 | + /** | |
| 287 | + * Reconciler instance, built on first use. | |
| 288 | + * | |
| 289 | + * @since 1.31.0 | |
| 290 | + * @return Instant_Indexing_Reconciler | |
| 291 | + */ | |
| 292 | + private function get_reconciler(): Instant_Indexing_Reconciler { | |
| 293 | + if (null === $this->reconciler) { | |
| 294 | + $this->reconciler = new Instant_Indexing_Reconciler(); | |
| 295 | + } | |
| 296 | + | |
| 297 | + return $this->reconciler; | |
| 298 | + } | |
| 299 | + | |
| 300 | + /** | |
| 167 | 301 | * Get settings |
| 168 | 302 | * |
| 169 | 303 | * @since 1.0.0 |
| 170 | 304 | * |
| @@ -215,18 +349,26 @@ | ||
| 215 | 349 | if (empty($params)) { |
| 216 | 350 | $params = $request->get_params(); // Fallback if content-type is not JSON |
| 217 | 351 | } |
| 218 | 352 | |
| 219 | - // Sanitize Post Types | |
| 220 | - $post_types = isset($params['auto_submit_post_types']) ? (array) $params['auto_submit_post_types'] : []; | |
| 221 | - $sanitized_post_types = array_map('sanitize_text_field', $post_types); | |
| 353 | + $current_settings = get_option($this->option_name, []); | |
| 354 | + if (!is_array($current_settings)) { | |
| 355 | + $current_settings = []; | |
| 356 | + } | |
| 357 | + $new_settings = $current_settings; | |
| 222 | 358 | |
| 223 | - // We generally don't let user update API Key directly via update_settings, | |
| 224 | - // they should use regenerate, but if we need to support manual entry: | |
| 225 | - $current_settings = get_option($this->option_name, []); | |
| 226 | - $new_settings = array_merge($current_settings, [ | |
| 227 | - 'auto_submit_post_types' => $sanitized_post_types | |
| 228 | - ]); | |
| 359 | + // Only write the post types when the caller actually sent them. Writing | |
| 360 | + // unconditionally meant a payload of {"enabled": true} cleared the list, | |
| 361 | + // so the feature came on with nothing to submit — and diverged from the | |
| 362 | + // MCP ability, which writes this same option with an array_key_exists() | |
| 363 | + // merge. An explicit empty array still clears, since isset() is true | |
| 364 | + // for one (#562). | |
| 365 | + if (isset($params['auto_submit_post_types'])) { | |
| 366 | + $new_settings['auto_submit_post_types'] = array_values(array_map( | |
| 367 | + 'sanitize_key', | |
| 368 | + (array) $params['auto_submit_post_types'] | |
| 369 | + )); | |
| 370 | + } | |
| 229 | 371 | |
| 230 | 372 | // Save enabled state |
| 231 | 373 | if (isset($params['enabled'])) { |
| 232 | 374 | $new_settings['enabled'] = rest_sanitize_boolean($params['enabled']); |