getTimestamp(); wp_schedule_event($next, 'daily', 'aksrt_dl_token_cleanup'); } } public function cleanup_expired_tokens(): void { global $wpdb; if (!class_exists('AKSRT_DB_Migrator')) return; $table = AKSRT_DB_Migrator::table('dl_token'); if (!$table) return; $now_utc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'); $audit_cutoff = (new DateTimeImmutable('-7 days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'); $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE expires_at < %s", $now_utc )); $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE used_count >= max_uses AND created_at < %s", $audit_cutoff )); } public function register_rewrite(): void { add_rewrite_rule( '^' . self::ENDPOINT . '/([A-Za-z0-9]{1,64})/?$', 'index.php?' . self::QUERY_VAR . '=$matches[1]', 'top' ); } public function add_query_var(array $vars): array { $vars[] = self::QUERY_VAR; return $vars; } public function register_routes(): void { register_rest_route('aksrt/v1', '/download/request', [ 'methods' => 'POST', 'callback' => [$this, 'rest_request'], 'permission_callback' => [$this, 'check_rest_nonce'], ]); register_rest_route('aksrt/v1', '/download/policy/(?P\\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'rest_policy'], 'permission_callback' => '__return_true', ]); } public function check_rest_nonce(WP_REST_Request $request) { $nonce = $request->get_header('x-wp-nonce'); if (!$nonce) { return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]); } if (!wp_verify_nonce($nonce, 'wp_rest')) { return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]); } return true; } public function rest_policy(WP_REST_Request $request) { $post_id = (int) $request['id']; $post = get_post($post_id); if (!$post || !aksrt_is_resource_post($post)) { return new WP_Error('aksrt_invalid', __('资源不存在', 'aksrt'), ['status' => 404]); } $meta = aksrt_get_resource_meta($post); $paid_settings = AKSRT_Settings::get_paid_content_settings(); $cost = aksrt_resource_download_cost($post); $uid = get_current_user_id(); $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']); if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) { $need_captcha = false; } return new WP_REST_Response([ 'post_id' => $post_id, 'price' => (float) $meta['price'], 'cost' => $cost, 'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']), 'require_captcha' => $need_captcha, 'logged_in' => $uid > 0, 'balance' => $uid > 0 && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0, 'sources' => array_map(function ($row, $idx) { $extract_code = trim((string) ($row['extract_code'] ?? '')); return [ 'index' => $idx, 'label' => $row['label'] ?? '', 'type' => $row['type'] ?? 'direct', 'has_extract' => $extract_code !== '', ]; }, $meta['downloads'], array_keys($meta['downloads'])), ], 200); } public function rest_request(WP_REST_Request $request) { $params = $request->get_json_params(); if (!is_array($params)) $params = $request->get_params(); $post_id = isset($params['post_id']) ? (int) $params['post_id'] : 0; $source_index = isset($params['source_index']) ? (int) $params['source_index'] : 0; $post = get_post($post_id); if (!$post || !aksrt_is_resource_post($post)) { return new WP_Error('aksrt_invalid', __('资源不存在', 'aksrt'), ['status' => 404]); } $meta = aksrt_get_resource_meta($post); if (empty($meta['downloads'][$source_index])) { return new WP_Error('aksrt_no_source', __('下载源不存在', 'aksrt'), ['status' => 404]); } $source = $meta['downloads'][$source_index]; $gate = aksrt_can_download($post); if (is_wp_error($gate)) return $gate; $paid_settings = AKSRT_Settings::get_paid_content_settings(); $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']); if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) { $need_captcha = false; } if ($need_captcha && function_exists('aksrt_verify_captcha')) { $cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : []; $verify = aksrt_verify_captcha($cap_payload); if ($verify !== true) { $msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt'); return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]); } } $uid = get_current_user_id(); $cost = aksrt_resource_download_cost($post); if ($cost > 0 && $uid > 0 && class_exists('AKSRT_Credits')) { $deducted = AKSRT_Credits::deduct($uid, $cost, 'download', ['type' => 'post', 'id' => $post_id]); if (is_wp_error($deducted)) return $deducted; } $token = $this->issue_token($uid, $post_id, $source_index, $cost); if (is_wp_error($token)) { if ($cost > 0 && $uid > 0 && class_exists('AKSRT_Credits')) { AKSRT_Credits::add($uid, $cost, 'refund', ['type' => 'post', 'id' => $post_id]); } return $token; } $dl_url = home_url('/' . self::ENDPOINT . '/' . $token); if ($uid > 0 && function_exists('aksrt_add_user_notification')) { aksrt_add_user_notification($uid, [ 'type' => 'download_ready', 'title' => __('资源下载已就绪', 'aksrt'), 'desc' => sprintf(__('《%s》下载链接已生成,可立即开始下载。', 'aksrt'), get_the_title($post)), 'url' => $dl_url, ]); } $extract_code = trim((string) ($source['extract_code'] ?? '')); return new WP_REST_Response([ 'success' => true, 'token' => $token, 'dl_url' => $dl_url, 'expires_in' => self::TOKEN_TTL, 'source_label' => $source['label'] ?? '', 'source_type' => $source['type'] ?? 'direct', 'has_extract' => $extract_code !== '', 'extract_code' => $extract_code, 'balance' => $uid > 0 && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0, ], 200); } public function maybe_serve_download(): void { $token = get_query_var(self::QUERY_VAR); if (!$token) return; $token = (string) $token; if (strlen($token) > 64 || !preg_match('/^[A-Za-z0-9]+$/', $token)) { $this->fail_410(); } global $wpdb; $table = AKSRT_DB_Migrator::table('dl_token'); if (!$table) $this->fail_410(); $row = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE token = %s LIMIT 1", $token ), ARRAY_A); if (!$row) $this->fail_410(); $expires_at = strtotime($row['expires_at'] . ' UTC'); if ($expires_at !== false && time() > $expires_at) $this->fail_410(); if ((int) $row['used_count'] >= (int) $row['max_uses']) $this->fail_410(); $ip = function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''); $ua_hash = sha1((string) ($_SERVER['HTTP_USER_AGENT'] ?? '')); if (!empty($row['ip_bind']) && $row['ip_bind'] !== $ip) $this->fail_410(); if (!empty($row['ua_hash']) && $row['ua_hash'] !== $ua_hash) $this->fail_410(); $post = get_post((int) $row['post_id']); if (!$post || !aksrt_is_resource_post($post)) $this->fail_410(); $meta = aksrt_get_resource_meta($post); $idx = (int) $row['source_index']; if (empty($meta['downloads'][$idx]['url'])) $this->fail_410(); $wpdb->update( $table, ['used_count' => (int) $row['used_count'] + 1], ['token' => $token], ['%d'], ['%s'] ); // Count on actual token consumption. if (function_exists('aksrt_update_post_counter')) { aksrt_update_post_counter((int) $row['post_id'], '_aksrt_res_download_count', 1); } else { $count = (int) get_post_meta((int) $row['post_id'], '_aksrt_res_download_count', true); update_post_meta((int) $row['post_id'], '_aksrt_res_download_count', $count + 1); } nocache_headers(); wp_redirect(esc_url_raw((string) $meta['downloads'][$idx]['url']), 302); exit; } private function fail_410(): void { status_header(410); nocache_headers(); wp_die(esc_html__('下载链接已过期或无效,请回到资源页重新获取。', 'aksrt'), 410); } private function issue_token(int $user_id, int $post_id, int $source_index, int $cost) { global $wpdb; $table = AKSRT_DB_Migrator::table('dl_token'); if (!$table) { return new WP_Error('aksrt_no_table', __('下载令牌表未初始化', 'aksrt'), ['status' => 500]); } $token = $this->random_token(self::TOKEN_LEN); $ip = function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''); $ua_hash = sha1((string) ($_SERVER['HTTP_USER_AGENT'] ?? '')); $now_utc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'); $expires_utc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->modify('+' . self::TOKEN_TTL . ' seconds')->format('Y-m-d H:i:s'); $ok = $wpdb->insert( $table, [ 'token' => $token, 'user_id' => $user_id, 'post_id' => $post_id, 'source_index' => $source_index, 'cost' => $cost, 'ip_bind' => $ip, 'ua_hash' => $ua_hash, 'used_count' => 0, 'max_uses' => 1, 'expires_at' => $expires_utc, 'created_at' => $now_utc, ], ['%s', '%d', '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%s', '%s'] ); if ($ok === false) { return new WP_Error('aksrt_token_insert', __('生成下载令牌失败', 'aksrt'), ['status' => 500]); } return $token; } private function random_token(int $len): string { $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $max = strlen($alphabet) - 1; $out = ''; for ($i = 0; $i < $len; $i++) { $out .= $alphabet[random_int(0, $max)]; } return $out; } }