'POST', 'callback' => [$this, 'handle_click_request'], 'permission_callback' => '__return_true', // Allow everyone (including guests) ]); } public function enqueue_tracker_script() { // Enqueue only on singular posts, pages, and home if (!is_singular() && !is_home() && !is_front_page()) { return; } wp_enqueue_script( 'ailm-click-tracker', plugin_dir_url(__FILE__) . 'assets/click-tracker.js', [], time(), true ); // Pass data to JS wp_localize_script('ailm-click-tracker', 'ailmTracker', [ 'api_url' => rest_url('ailm/v1/ui-state'), 'post_id' => get_the_ID(), 'nonce' => wp_create_nonce('wp_rest') ]); } /** * REST API request handler */ public function handle_click_request(WP_REST_Request $request) { // 1. Bot filter (Server-side) $user_agent = $request->get_header('user_agent'); if (empty($user_agent) || preg_match('/bot|crawl|spider|slurp|facebook|whatsapp|preview/i', $user_agent)) { return new WP_REST_Response(['status' => 'ignored_bot'], 200); } // 2. Data validation $params = $request->get_params(); $post_id = isset($params['post_id']) ? intval($params['post_id']) : 0; $anchor = isset($params['anchor']) ? sanitize_text_field($params['anchor']) : ''; // Расшифровываем URL из Base64 $raw_url = isset($params['target_url']) ? base64_decode($params['target_url']) : ''; $target_url = esc_url_raw($raw_url); // Декодируем тип ссылки (1 -> internal, 2 -> external, 3 -> sponsored) $raw_type = isset($params['link_type']) ? sanitize_text_field($params['link_type']) : '1'; $type_map = ['1' => 'internal', '2' => 'external', '3' => 'sponsored']; $link_type = isset($type_map[$raw_type]) ? $type_map[$raw_type] : 'internal'; if (!$post_id || empty($target_url)) { return new WP_REST_Response(['error' => 'Invalid data'], 400); } // 3. Save to DB global $wpdb; $table = esc_sql(AILM_Clicks_DB::get_table_name()); $today = current_time('Y-m-d'); // Atomic query: If the record exists, increment the counter; if not, create it. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $query = "INSERT INTO {$table} (post_id, target_url, anchor, click_date, click_count, link_type) VALUES (%d, %s, %s, %s, 1, %s) ON DUPLICATE KEY UPDATE click_count = click_count + 1"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query($wpdb->prepare($query, $post_id, $target_url, $anchor, $today, $link_type)); return new WP_REST_Response(['status' => 'ok'], 200); } }