options_repository = $options_repository; $this->nonce_service = $nonce_service; $this->capabilities_service = $capabilities_service; $this->logger = $logger; $this->xpath_engine = $xpath_engine; $this->inline_css_generator = $inline_css_generator; } /** * Initialize REST API endpoints. */ public function init(): void { add_action('rest_api_init', [$this, 'register_routes']); } /** * Register REST API routes. */ public function register_routes(): void { register_rest_route( 'adsd/v1', '/rules', [ [ 'methods' => 'GET', 'callback' => [$this, 'handle_list_rules'], 'permission_callback' => [$this, 'check_read_permission'], ], [ 'methods' => 'POST', 'callback' => [$this, 'handle_create_rule'], 'permission_callback' => [$this, 'check_write_permission'], ], ] ); // Reset all rules at once (register before parameterized route to avoid conflicts) register_rest_route( 'adsd/v1', '/rules/reset', [ 'methods' => 'POST', 'callback' => [$this, 'handle_reset_rules'], 'permission_callback' => [$this, 'check_write_permission'], ] ); // Activate existing rule register_rest_route( 'adsd/v1', '/rules/activate-existing', [ 'methods' => 'POST', 'callback' => [$this, 'handle_activate_existing_rule'], 'permission_callback' => [$this, 'check_write_permission'], ] ); register_rest_route( 'adsd/v1', '/rules/(?P[a-zA-Z0-9_-]+)', [ [ 'methods' => 'GET', 'callback' => [$this, 'handle_get_rule'], 'permission_callback' => [$this, 'check_read_permission'], ], [ 'methods' => 'DELETE', 'callback' => [$this, 'handle_delete_rule'], 'permission_callback' => [$this, 'check_write_permission'], ], [ 'methods' => 'POST', 'callback' => [$this, 'handle_update_rule'], 'permission_callback' => [$this, 'check_write_permission'], ], ] ); register_rest_route( 'adsd/v1', '/test', [ 'methods' => 'POST', 'callback' => [$this, 'handle_test_xpath'], 'permission_callback' => [$this, 'check_read_permission'], ] ); // Log management endpoints register_rest_route( 'adsd/v1', '/logs/latest', [ 'methods' => 'GET', 'callback' => [$this, 'handle_get_latest_log'], 'permission_callback' => [$this, 'check_read_permission'], ] ); register_rest_route( 'adsd/v1', '/logs/download', [ 'methods' => 'POST', 'callback' => [$this, 'handle_download_latest_log'], 'permission_callback' => [$this, 'check_write_permission'], ] ); register_rest_route( 'adsd/v1', '/logs/clear', [ 'methods' => 'POST', 'callback' => [$this, 'handle_clear_all_logs'], 'permission_callback' => [$this, 'check_write_permission'], ] ); register_rest_route( 'adsd/v1', '/logs/js', [ 'methods' => 'POST', 'callback' => [$this, 'handle_log_js_message'], 'permission_callback' => [$this, 'check_read_permission'], ] ); } /** * Check read permission. */ public function check_read_permission(): bool { return $this->capabilities_service->current_user_can_access(); } /** * Check write permission. */ public function check_write_permission(): bool { return $this->capabilities_service->current_user_can_manage(); } /** * GET /rules */ public function handle_list_rules(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { $this->logger->warning('Invalid nonce for list rules request'); return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $rules = $this->options_repository->get_rules(); $this->logger->debug('Rules list requested', ['count' => count($rules)]); return new WP_REST_Response(['rules' => $rules]); } /** * GET /rules/{id} */ public function handle_get_rule(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $rule_id = $request->get_param('id'); $rules = $this->options_repository->get_rules(); // Use OptionsRepository to get complete rule data $rule = $this->options_repository->get_rule($rule_id); if ($rule) { return new WP_REST_Response($rule); } return new WP_Error('rule_not_found', __('Rule not found', 'ads-destroyer'), ['status' => 404]); } /** * POST /rules */ public function handle_create_rule(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { $this->logger->warning('Invalid nonce for rule creation request'); return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $params = $request->get_json_params(); $xpath = sanitize_text_field($params['xpath'] ?? ''); $label = sanitize_text_field($params['label'] ?? ''); $active = filter_var($params['active'] ?? true, FILTER_VALIDATE_BOOLEAN); $expires_at = (int) ($params['expires_at'] ?? 0); $page_title = sanitize_text_field($params['page_title'] ?? ''); $page_url = esc_url_raw($params['page_url'] ?? ''); $this->logger->debug('Rule creation request', [ 'xpath' => $xpath, 'label' => $label, 'active' => $active, 'expires_at' => $expires_at, 'user' => wp_get_current_user()->user_login ?? 'unknown' ]); if (empty($xpath)) { $this->logger->warning('Empty XPath in rule creation request'); return new WP_Error('missing_xpath', __('XPath is required', 'ads-destroyer'), ['status' => 400]); } // Get existing rules $rules = $this->options_repository->get_rules(); // Check for duplicate XPath foreach ($rules as $existing_rule) { if ($existing_rule['xpath'] === $xpath) { return new WP_Error( 'duplicate_xpath', __('A rule for this element already exists', 'ads-destroyer'), [ 'status' => 409, 'code' => 'duplicate_xpath', 'existing_rule' => $existing_rule, 'message' => __('A rule for hiding this element already exists. Do you want to activate it?', 'ads-destroyer') ] ); } } // Create new rule $new_rule = $this->options_repository->add_rule([ 'xpath' => $xpath, // translators: %s: XPath expression 'label' => $label ?: sprintf(__('Rule for %s', 'ads-destroyer'), $xpath), 'active' => $active, 'expires_at' => $expires_at, 'page_title' => $page_title, 'page_url' => $page_url, ]); $this->logger->info('New rule created by user: ' . wp_get_current_user()->user_login . ' - XPath: ' . $xpath); // Invalidate inline CSS cache $this->inline_css_generator->invalidate_cache(); return new WP_REST_Response([ 'rule' => $new_rule, 'message' => __('Rule created successfully', 'ads-destroyer'), ]); } /** * POST /rules/activate-existing */ public function handle_activate_existing_rule(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $params = $request->get_json_params(); $rule_id = sanitize_text_field($params['rule_id'] ?? ''); if (empty($rule_id)) { return new WP_Error('missing_rule_id', __('Rule ID is required', 'ads-destroyer'), ['status' => 400]); } // Update rule to active status $updated = $this->options_repository->update_rule($rule_id, ['active' => true]); if (!$updated) { return new WP_Error('rule_not_found', __('Rule not found', 'ads-destroyer'), ['status' => 404]); } $this->logger->info('Rule activated by user: ' . wp_get_current_user()->user_login . ' - Rule ID: ' . $rule_id); return new WP_REST_Response([ 'message' => __('Rule activated successfully', 'ads-destroyer'), ]); } /** * POST /rules/reset */ public function handle_reset_rules(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Security check failed', 'ads-destroyer'), ['status' => 403]); } $this->options_repository->set_option('rules', []); // Invalidate inline CSS cache $this->inline_css_generator->invalidate_cache(); $this->logger->info('All rules reset by user: ' . wp_get_current_user()->user_login); return new WP_REST_Response([ 'message' => __('All rules have been reset', 'ads-destroyer'), ]); } /** * DELETE /rules/{id} */ public function handle_delete_rule(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { $this->logger->warning('Invalid nonce for rule deletion request'); return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $rule_id = $request->get_param('id'); $this->logger->info('Rule deletion requested', ['rule_id' => $rule_id, 'user' => wp_get_current_user()->user_login ?? 'unknown']); // Delete the rule $deleted = $this->options_repository->delete_rule($rule_id); if (!$deleted) { return new WP_Error('rule_not_found', __('Rule not found', 'ads-destroyer'), ['status' => 404]); } $this->logger->info('Rule deleted by user: ' . wp_get_current_user()->user_login . ' - Rule ID: ' . $rule_id); // Invalidate inline CSS cache $this->inline_css_generator->invalidate_cache(); return new WP_REST_Response([ 'message' => __('Rule deleted successfully', 'ads-destroyer'), ]); } /** * POST /rules/{id} */ public function handle_update_rule(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $rule_id = $request->get_param('id'); $params = $request->get_json_params(); $this->logger->debug('update_rule request', ['rule_id' => $rule_id, 'params' => $params]); // Prepare update data $update_data = []; if (isset($params['active'])) { $update_data['active'] = filter_var($params['active'], FILTER_VALIDATE_BOOLEAN); } if (isset($params['label'])) { $update_data['label'] = sanitize_text_field($params['label']); } if (isset($params['xpath'])) { $update_data['xpath'] = sanitize_textarea_field($params['xpath']); } if (isset($params['expires_at'])) { $update_data['expires_at'] = (int)$params['expires_at']; } if (isset($params['page_url'])) { $update_data['page_url'] = esc_url_raw($params['page_url']); } if (isset($params['page_title'])) { $update_data['page_title'] = sanitize_text_field($params['page_title']); } if (isset($params['css_selector'])) { $update_data['css_selector'] = sanitize_text_field($params['css_selector']); } // Update the rule $updated = $this->options_repository->update_rule($rule_id, $update_data); if (!$updated) { $this->logger->warning('Rule not found for update', ['rule_id' => $rule_id]); return new WP_Error('rule_not_found', __('Rule not found', 'ads-destroyer'), ['status' => 404]); } // Get the updated rule using OptionsRepository $updated_rule = $this->options_repository->get_rule($rule_id); if (!$updated_rule) { return new WP_Error('rule_not_found', __('Updated rule not found', 'ads-destroyer'), ['status' => 404]); } $this->logger->info('Rule updated', ['rule_id' => $rule_id, 'user' => wp_get_current_user()->user_login]); $this->logger->debug('update_rule result', ['updated_rule' => $updated_rule]); // Invalidate inline CSS cache $this->inline_css_generator->invalidate_cache(); return new WP_REST_Response($updated_rule); } /** * POST /test */ public function handle_test_xpath(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $params = $request->get_json_params(); $xpath = sanitize_text_field($params['xpath'] ?? ''); if (empty($xpath)) { return new WP_Error('missing_xpath', __('XPath is required', 'ads-destroyer'), ['status' => 400]); } // Test XPath using the XPath engine $result = $this->xpath_engine->test_xpath($xpath); return new WP_REST_Response([ 'ok' => $result['valid'], 'count' => $result['matches'], 'message' => $result['message'], ]); } /** * Handle get latest log REST API request. */ public function handle_get_latest_log(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $latest_file = $this->logger->get_latest_log_file(); if ($latest_file) { $content = $this->logger->read_log_file($latest_file, 1000); return new WP_REST_Response(['content' => $content]); } else { return new WP_REST_Response(['content' => __('No log files found.', 'ads-destroyer')]); } } /** * Handle download latest log REST API request. */ public function handle_download_latest_log(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $latest_file = $this->logger->get_latest_log_file(); if ($latest_file && file_exists($latest_file)) { $content = file_get_contents($latest_file); // Return file content for download return new WP_REST_Response([ 'filename' => 'ads-destroyer-latest.log', 'content' => $content, 'download' => true ]); } else { return new WP_Error('no_file', __('No log files found.', 'ads-destroyer'), ['status' => 404]); } } /** * Handle clear all logs REST API request. */ public function handle_clear_all_logs(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $deleted_count = $this->logger->clear_all_logs(); return new WP_REST_Response([ 'count' => $deleted_count, // translators: %d: Number of deleted log files 'message' => sprintf(__('Cleared %d log files.', 'ads-destroyer'), $deleted_count) ]); } /** * Handle JavaScript log message REST API request. */ public function handle_log_js_message(WP_REST_Request $request): WP_REST_Response|WP_Error { if (!$this->nonce_service->verify_request($request)) { return new WP_Error('invalid_nonce', __('Invalid nonce', 'ads-destroyer'), ['status' => 403]); } $log_entry_json = $request->get_param('log_entry') ?? ''; $log_entry = json_decode($log_entry_json, true); if (!$log_entry) { return new WP_Error('invalid_data', __('Invalid log entry data', 'ads-destroyer'), ['status' => 400]); } $level = $log_entry['level'] ?? 'info'; $message = $log_entry['message'] ?? ''; $context = $log_entry['context'] ?? []; $this->logger->log($level, $message, $context); return new WP_REST_Response(['success' => true]); } }