feat(download): switch download buttons to direct source links
The token relay still stood between the button and the file: every click issued a short-lived token URL, so downloads never reached the real source address directly. - Render direct anchors (target=_blank) to the raw source URL in the download box and the paid card, with inline extract-code buttons - Retire the relay: delete Content\Download plus its init wiring, header query-var branch and SEO noindex branch, and drop the dead download-request flow from cms-download.js (cashier, scan-pay and error views stay) - Keep the dl_token table and the download logs page as read-only history; unscheduling of the orphaned cleanup cron is preserved
This commit is contained in:
+3
-111
@@ -2,9 +2,9 @@
|
||||
* AKSRT CMS Download
|
||||
*
|
||||
* 功能:
|
||||
* - 监听 [data-action="aksrt-download"] 按钮点击
|
||||
* - 调 POST /aksrt/v1/download/request
|
||||
* - 成功后直接打开生成的下载链接;失败/购买等场景仍走弹窗
|
||||
* - 监听购买入口(data-aksrt-pay-content / data-action="aksrt-need-purchase")
|
||||
* - 内容购买收银台、扫码支付与错误提示弹窗
|
||||
* - 下载已改为直连:按钮为源地址锚点,不再经过本站中转请求
|
||||
*
|
||||
* 弹窗视觉走主题 design token(见 assets/css/frontend/cms-download.css),
|
||||
* 付费收银台沿用参考实现的 colorful header 结构。
|
||||
@@ -140,7 +140,6 @@
|
||||
// ===== 统一视图壳(下载 / 错误 / 扫码共用主题化外观) =====
|
||||
// 头部与按钮统一继承运行时主题色(--focus-color),状态用图标 + 标题区分。
|
||||
const DL_VIEWS = {
|
||||
success: { icon: 'fa-cloud-download-alt', title: '下载已就绪' },
|
||||
login: { icon: 'fa-user-lock', title: '请先登录' },
|
||||
purchase: { icon: 'fa-shopping-cart', title: '请先购买' },
|
||||
vip: { icon: 'fa-gem', title: '会员专属' },
|
||||
@@ -251,34 +250,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 成功:直接打开生成的下载链接,不再弹窗 =====
|
||||
function startDownload(payload, returnFocusEl) {
|
||||
const url = payload && payload.dl_url ? String(payload.dl_url) : '';
|
||||
if (!url) {
|
||||
renderError('下载链接生成失败,请稍后重试', {}, returnFocusEl);
|
||||
return;
|
||||
}
|
||||
// 与旧版弹窗内「立即下载」一致:新标签页打开本站中转链接。
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
// ===== 主流程 =====
|
||||
function captchaScope(sourceEl) {
|
||||
const box = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
|
||||
return box ? (box.querySelector('[data-aksrt-download-captcha]') || box) : sourceEl;
|
||||
}
|
||||
|
||||
function isCaptchaActive(sourceEl) {
|
||||
const card = sourceEl ? sourceEl.closest('[data-aksrt-download-box]') : null;
|
||||
if (!card) return false;
|
||||
return card.getAttribute('data-captcha') === '1';
|
||||
}
|
||||
|
||||
function cashierHeader() {
|
||||
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
|
||||
@@ -299,70 +272,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
function ensureCaptchaThen(sourceEl, callback) {
|
||||
if (!isCaptchaActive(sourceEl)) { callback({}); return; }
|
||||
const captcha = window.AKSRTCaptcha;
|
||||
if (!captcha) {
|
||||
if (window.console && console.warn) console.warn('[AKSRT] download captcha not ready');
|
||||
alert('验证码组件加载中,请稍候再试');
|
||||
return;
|
||||
}
|
||||
captcha.run('download', captchaScope(sourceEl)).then(function (payload) {
|
||||
if (payload !== null) callback(payload);
|
||||
});
|
||||
}
|
||||
|
||||
function performDownloadRequest(button, captchaPayload) {
|
||||
const card = button.closest('[data-aksrt-download-box]');
|
||||
if (!card) return;
|
||||
const postId = parseInt(card.getAttribute('data-post-id'), 10);
|
||||
const sourceIndex = parseInt(button.getAttribute('data-source-index'), 10);
|
||||
if (!postId || isNaN(sourceIndex)) return;
|
||||
|
||||
button.disabled = true;
|
||||
const original = button.innerHTML;
|
||||
button.innerHTML = '<i class="fa fas fa-spinner fa-spin" aria-hidden="true"></i> 处理中…';
|
||||
|
||||
const restNonce = window.aksrtData.restNonce || '';
|
||||
|
||||
fetch(REST_BASE + '/download/request', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': restNonce,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_id: postId,
|
||||
source_index: sourceIndex,
|
||||
captcha: captchaPayload,
|
||||
}),
|
||||
})
|
||||
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, json: j }; }); })
|
||||
.then(function (r) {
|
||||
button.disabled = false;
|
||||
button.innerHTML = original;
|
||||
if (!r.ok || (r.json && r.json.code)) {
|
||||
renderError(r.json.message || '请稍后重试', r.json.data || {}, button, r.json.code);
|
||||
return;
|
||||
}
|
||||
startDownload(r.json, button);
|
||||
})
|
||||
.catch(function () {
|
||||
button.disabled = false;
|
||||
button.innerHTML = original;
|
||||
renderError('网络错误,请稍后重试', {}, button);
|
||||
})
|
||||
.finally(function () {
|
||||
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('download', captchaScope(button));
|
||||
});
|
||||
}
|
||||
|
||||
function requestDownload(button) {
|
||||
ensureCaptchaThen(button, function (captchaPayload) {
|
||||
performDownloadRequest(button, captchaPayload);
|
||||
});
|
||||
}
|
||||
|
||||
function pollOrderPaid(orderNumber, returnFocusEl) {
|
||||
if (!orderNumber) return;
|
||||
@@ -387,12 +298,6 @@
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ===== 积分不足按钮(前置拦截,不发请求) =====
|
||||
function showLowCreditModal(button) {
|
||||
const balance = parseInt(button.getAttribute('data-balance'), 10) || 0;
|
||||
const cost = parseInt(button.getAttribute('data-cost'), 10) || 0;
|
||||
renderError('', { balance: balance, cost: cost }, button, 'aksrt_points_insufficient');
|
||||
}
|
||||
|
||||
// ===== 收银台(内容购买) =====
|
||||
function restHeaders(idempotencyKey) {
|
||||
@@ -807,13 +712,6 @@
|
||||
|
||||
// ===== 事件绑定 =====
|
||||
document.addEventListener('click', function (e) {
|
||||
const lowBtn = e.target.closest('[data-action="aksrt-need-credits"]');
|
||||
if (lowBtn) {
|
||||
e.preventDefault();
|
||||
showLowCreditModal(lowBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
const buyBtn = e.target.closest('[data-action="aksrt-need-purchase"], [data-aksrt-pay-content]');
|
||||
if (buyBtn) {
|
||||
e.preventDefault();
|
||||
@@ -828,12 +726,6 @@
|
||||
if (postId) openCashier(postId, buyBtn, priceType);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = e.target.closest('[data-action="aksrt-download"]');
|
||||
if (!btn) return;
|
||||
if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return;
|
||||
e.preventDefault();
|
||||
requestDownload(btn);
|
||||
});
|
||||
|
||||
// Public cashier bridge (content pay).
|
||||
|
||||
@@ -218,9 +218,6 @@ if (class_exists('AKSRT\\Payment\\CommissionLedger')) {
|
||||
if (class_exists('AKSRT\\Payment\\InviteCode')) {
|
||||
\AKSRT\Payment\InviteCode::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Content\Download')) {
|
||||
\AKSRT\Content\Download::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Content\Pay')) {
|
||||
\AKSRT\Content\Pay::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -1463,6 +1463,10 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
|
||||
continue;
|
||||
}
|
||||
$url = (string) ($download['url'] ?? '');
|
||||
if ($url === '') {
|
||||
// 直连模式没有可用的原始地址,空源不渲染。
|
||||
continue;
|
||||
}
|
||||
$label = trim((string) ($download['label'] ?? ''));
|
||||
$service = function_exists('aksrt_detect_cloud_service')
|
||||
? aksrt_detect_cloud_service($url)
|
||||
@@ -1470,9 +1474,7 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
|
||||
if ($label === '') {
|
||||
$label = (string) ($service['name'] ?? __('本地下载', 'aksrt'));
|
||||
}
|
||||
$type = sanitize_key((string) ($download['type'] ?? 'direct'));
|
||||
$extract_code = trim((string) ($download['extract_code'] ?? ''));
|
||||
$has_extract = $extract_code !== '';
|
||||
$service_key = sanitize_html_class((string) ($service['key'] ?? 'other')) ?: 'other';
|
||||
$button_class = trim((string) ($download['class'] ?? ''));
|
||||
$button_class = $button_class !== '' ? $button_class : 'b-theme ' . $service_key;
|
||||
@@ -1481,8 +1483,8 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
|
||||
$copy_label = $extract_code !== '' ? $extract_code : ($copy_key !== '' ? $copy_key : __('更多内容', 'aksrt'));
|
||||
|
||||
$buttons .= '<div class="but-download flex ac">';
|
||||
$buttons .= '<button type="button" class="mr10 but ' . esc_attr($button_class) . '" data-action="aksrt-download" data-source-index="' . esc_attr((string) $index) . '" data-source-type="' . esc_attr($type) . '" data-has-extract="' . ($has_extract ? '1' : '0') . '">';
|
||||
$buttons .= '<i class="fa fas fa-download" aria-hidden="true"></i>' . esc_html($label) . '</button>';
|
||||
$buttons .= '<a class="mr10 but ' . esc_attr($button_class) . '" href="' . esc_url($url) . '" target="_blank" rel="nofollow noopener">';
|
||||
$buttons .= '<i class="fa fas fa-download" aria-hidden="true"></i>' . esc_html($label) . '</a>';
|
||||
if ($copy_value !== '') {
|
||||
$buttons .= '<button type="button" class="but" data-clipboard-tag="' . esc_attr($copy_key !== '' ? $copy_key : __('内容', 'aksrt')) . '" data-clipboard-text="' . esc_attr($copy_value) . '">' . esc_html($copy_label) . '</button>';
|
||||
} elseif ($extract_code !== '') {
|
||||
@@ -1577,21 +1579,14 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
||||
|
||||
$download_box = '';
|
||||
$root_data = '';
|
||||
$captcha = '';
|
||||
if ($pay_type === AKSRT_PAY_TYPE_DOWNLOAD) {
|
||||
$captcha_active = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
|
||||
$root_data = ' data-aksrt-download-box data-post-id="' . esc_attr((string) $post->ID) . '" data-captcha="' . ($captcha_active ? '1' : '0') . '"';
|
||||
$root_data = ' data-aksrt-download-box data-post-id="' . esc_attr((string) $post->ID) . '"';
|
||||
$download_box = '<div class="hidden-box show"><div class="hidden-text"><i class="fa fas fa-download mr6" aria-hidden="true"></i>' . esc_html__('资源下载', 'aksrt') . '</div>' . aksrt_get_paid_download_actions_html($meta) . '</div>';
|
||||
$demo = aksrt_get_demo_link_html($post, 'but jb-yellow padding-lg');
|
||||
if ($demo !== '') {
|
||||
$download_box .= '<div class="mt10">' . $demo . '</div>';
|
||||
}
|
||||
$download_box .= aksrt_get_pay_attributes_html($post, 'pay-attr');
|
||||
if ($captcha_active && function_exists('aksrt_render_captcha')) {
|
||||
ob_start();
|
||||
aksrt_render_captcha('download');
|
||||
$captcha = '<div class="resource-download-box__captcha resource-download-box__captcha--hidden" data-aksrt-download-captcha>' . (string) ob_get_clean() . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$service = aksrt_get_pay_service_html('inline-block ml10');
|
||||
@@ -1607,7 +1602,6 @@ if (!function_exists('aksrt_get_paid_box_html')) {
|
||||
. '<div' . $title_style . '><span class="badg c-red hollow badg-sm mr6">' . esc_html($type_label) . '</span><b>' . esc_html($title) . '</b></div>'
|
||||
. $download_box
|
||||
. $service
|
||||
. $captcha
|
||||
. '</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +49,10 @@ if (!function_exists('aksrt_header_slide_current_page_type')) {
|
||||
*
|
||||
* Custom rewrite requests can retain WordPress's is_home flag, so the
|
||||
* explicit theme surfaces must win before the home fallback is evaluated.
|
||||
* Utility rewrites (download token hand-off, go-link confirm) are error /
|
||||
* hand-off surfaces and opt out entirely, like the excluded templates.
|
||||
* Utility rewrites (go-link confirm) are error / hand-off surfaces and
|
||||
* opt out entirely, like the excluded templates.
|
||||
*/
|
||||
function aksrt_header_slide_current_page_type(): string {
|
||||
if (class_exists('\AKSRT\Content\Download') && (string) get_query_var(\AKSRT\Content\Download::QUERY_VAR) !== '') {
|
||||
return '';
|
||||
}
|
||||
if ((string) get_query_var('aksrt_go_redirect') !== '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class PayDownloadLogs {
|
||||
$total = $result['total'];
|
||||
$pages = max(1, (int) ceil($total / $per_page));
|
||||
|
||||
echo '<p class="description">' . esc_html__('基于下载令牌表(aksrt_dl_token)。已使用记录会保留一段时间后清理;未使用且过期的令牌会被定期删除。', 'aksrt') . '</p>';
|
||||
echo '<p class="description">' . esc_html__('基于下载令牌表(aksrt_dl_token)。下载已改为直连模式,不再签发新令牌,以下为历史记录。', 'aksrt') . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
|
||||
@@ -1,538 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Content;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Download {
|
||||
|
||||
const TOKEN_LEN = 48;
|
||||
const QUERY_VAR = 'aksrt_dl_token';
|
||||
|
||||
// Fixed relay configuration. The admin download-config settings page was
|
||||
// removed, so these constants are the single source of truth for the
|
||||
// site-routed download system (endpoint, per-token uses, TTL).
|
||||
const ENDPOINT = 'download';
|
||||
const MAX_USES = 1;
|
||||
const TTL_MINUTES = 10;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('init', [$this, 'register_rewrite']);
|
||||
add_filter('query_vars', [$this, 'add_query_var']);
|
||||
add_action('template_redirect', [$this, 'maybe_serve_download'], 0);
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
|
||||
add_action('init', [$this, 'maybe_flush_rewrite'], 99);
|
||||
add_action('wp', [$this, 'maybe_schedule_cleanup']);
|
||||
add_action('aksrt_dl_token_cleanup', [$this, 'cleanup_expired_tokens']);
|
||||
}
|
||||
|
||||
public function maybe_schedule_cleanup(): void {
|
||||
if (!wp_next_scheduled('aksrt_dl_token_cleanup')) {
|
||||
$next = (new \DateTimeImmutable('tomorrow 03:30', wp_timezone()))->getTimestamp();
|
||||
wp_schedule_event($next, 'daily', 'aksrt_dl_token_cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
public function cleanup_expired_tokens(): void {
|
||||
global $wpdb;
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) return;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::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
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective download settings for the fixed relay configuration.
|
||||
*/
|
||||
private function config(): array {
|
||||
return [
|
||||
'endpoint' => self::ENDPOINT,
|
||||
'max_uses' => self::MAX_USES,
|
||||
'ttl' => self::TTL_MINUTES * MINUTE_IN_SECONDS,
|
||||
];
|
||||
}
|
||||
|
||||
public function register_rewrite(): void {
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($this->config()['endpoint'], '/') . '/([A-Za-z0-9]{1,64})/?$',
|
||||
'index.php?' . self::QUERY_VAR . '=$matches[1]',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-flush rewrite rules on theme version changes, using the same
|
||||
* version-marker approach as Auth\Router.
|
||||
*/
|
||||
public function maybe_flush_rewrite(): void {
|
||||
$version = AKSRT_VERSION . ':download-endpoint:' . self::ENDPOINT;
|
||||
if ((string) get_option('aksrt_dl_rewrite_version') !== $version) {
|
||||
flush_rewrite_rules(false);
|
||||
update_option('aksrt_dl_rewrite_version', $version);
|
||||
}
|
||||
}
|
||||
|
||||
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'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_rest_nonce(\WP_REST_Request $request) {
|
||||
// The capability matrix can explicitly allow resource downloads for
|
||||
// everyone. A WordPress REST nonce only exists for authenticated
|
||||
// sessions, so requiring one from visitors would make the "all"
|
||||
// column ineffective. Anonymous requests still pass the unified
|
||||
// download gate (capability, payment and captcha) in rest_request().
|
||||
if (get_current_user_id() <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$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_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 || !function_exists('aksrt_is_downloadable_content') || !aksrt_is_downloadable_content($post)) {
|
||||
return new \WP_Error('aksrt_invalid', __('内容不存在或未启用下载能力', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$meta = function_exists('aksrt_get_download_box_meta') ? aksrt_get_download_box_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;
|
||||
|
||||
$uid = get_current_user_id();
|
||||
$cost = aksrt_resource_download_cost($post);
|
||||
|
||||
// Daily free-download quota (legacy).
|
||||
if ($cost <= 0 && $uid > 0 && function_exists('aksrt_check_free_download_quota')) {
|
||||
$quota = aksrt_check_free_download_quota($uid, $cost);
|
||||
if (is_wp_error($quota)) {
|
||||
return $quota;
|
||||
}
|
||||
}
|
||||
|
||||
$cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : [];
|
||||
$verify = aksrt_require_captcha('download', $cap_payload);
|
||||
if ($verify !== true) {
|
||||
$code = is_wp_error($verify) ? $verify->get_error_code() : 'aksrt_captcha_failed';
|
||||
$msg = is_wp_error($verify) ? $verify->get_error_message() : __('人机验证失败', 'aksrt');
|
||||
return new \WP_Error($code ?: 'aksrt_captcha_failed', $msg, ['status' => 400]);
|
||||
}
|
||||
|
||||
// Purchase is one-shot via cashier (resource order). Token issue does not re-deduct.
|
||||
$config = $this->config();
|
||||
$token = $this->issue_token($uid, $post_id, $source_index, 0, $config);
|
||||
if (is_wp_error($token)) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
// Record free download against daily quota after token issued.
|
||||
if ($cost <= 0 && $uid > 0 && function_exists('aksrt_record_user_free_download')) {
|
||||
aksrt_record_user_free_download($uid, $post_id);
|
||||
}
|
||||
|
||||
$dl_url = home_url('/' . $config['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' => $config['ttl'],
|
||||
'source_label' => $source['label'] ?? '',
|
||||
'source_type' => $source['type'] ?? 'direct',
|
||||
'has_extract' => $extract_code !== '',
|
||||
'extract_code' => $extract_code,
|
||||
'balance' => $uid > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($uid, \AKSRT\Ledger\AssetType::POINTS) : 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\Core\DBMigrator::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 || !function_exists('aksrt_is_downloadable_content') || !aksrt_is_downloadable_content($post)) $this->fail_410();
|
||||
$meta = function_exists('aksrt_get_download_box_meta') ? aksrt_get_download_box_meta($post) : [];
|
||||
$idx = (int) $row['source_index'];
|
||||
if (empty($meta['downloads'][$idx]['url'])) $this->fail_410();
|
||||
|
||||
// Probes (HEAD requests and Speculative-Loading prefetches) validate
|
||||
// availability but must never consume a use nor waste bandwidth.
|
||||
$is_probe = $this->is_probe_request();
|
||||
|
||||
$target_url = esc_url_raw((string) $meta['downloads'][$idx]['url']);
|
||||
|
||||
// Local attachment / uploads file: stream without artificial throttling.
|
||||
$local_path = $this->resolve_local_download_path($target_url);
|
||||
if ($local_path !== '' && is_readable($local_path)) {
|
||||
$this->send_download_headers($local_path);
|
||||
if (!$is_probe) {
|
||||
// Count only fully delivered bodies; aborted transfers stay reusable.
|
||||
if ($this->stream_body($local_path)) {
|
||||
$this->consume_token($row, $token);
|
||||
}
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
nocache_headers();
|
||||
wp_redirect($target_url, 302);
|
||||
// Remote hosts give no completion signal, so the redirect hand-off
|
||||
// counts as delivery; probes still never consume.
|
||||
if (!$is_probe) {
|
||||
$this->consume_token($row, $token);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that inspect the link instead of taking the download: plain
|
||||
* HEAD plus browser speculative prefetches (Sec-Purpose: prefetch).
|
||||
*/
|
||||
private function is_probe_request(): bool {
|
||||
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'HEAD') {
|
||||
return true;
|
||||
}
|
||||
$purpose = strtolower((string) ($_SERVER['HTTP_SEC_PURPOSE'] ?? ($_SERVER['HTTP_PURPOSE'] ?? '')));
|
||||
return $purpose !== '' && str_contains($purpose, 'prefetch');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the attachment headers for a local download.
|
||||
*/
|
||||
private function send_download_headers(string $path): void {
|
||||
$filename = basename($path);
|
||||
$size = filesize($path);
|
||||
if ($size === false) {
|
||||
$this->fail_410();
|
||||
}
|
||||
|
||||
nocache_headers();
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Content-Length: ' . (string) $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the local file body after its headers. Returns true only when
|
||||
* every byte was flushed to the client.
|
||||
*/
|
||||
private function stream_body(string $path): bool {
|
||||
$size = filesize($path);
|
||||
if ($size === false) {
|
||||
$this->fail_410();
|
||||
}
|
||||
|
||||
$chunk = 1024 * 64; // 64KB
|
||||
|
||||
$fh = fopen($path, 'rb');
|
||||
if (!$fh) {
|
||||
$this->fail_410();
|
||||
}
|
||||
// Disable output compression so Content-Length remains accurate.
|
||||
if (function_exists('apache_setenv')) {
|
||||
@apache_setenv('no-gzip', '1');
|
||||
}
|
||||
@ini_set('zlib.output_compression', 'Off');
|
||||
while (!feof($fh)) {
|
||||
if (connection_aborted()) {
|
||||
fclose($fh);
|
||||
return false;
|
||||
}
|
||||
$buf = fread($fh, $chunk);
|
||||
if ($buf === false) {
|
||||
fclose($fh);
|
||||
return false;
|
||||
}
|
||||
echo $buf;
|
||||
if (function_exists('flush')) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
fclose($fh);
|
||||
|
||||
return !connection_aborted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a site-local URL to a filesystem path under uploads (or ABSPATH).
|
||||
*/
|
||||
private function resolve_local_download_path(string $url): string {
|
||||
if ($url === '') {
|
||||
return '';
|
||||
}
|
||||
$uploads = wp_upload_dir();
|
||||
$baseurl = trailingslashit((string) ($uploads['baseurl'] ?? ''));
|
||||
$basedir = trailingslashit((string) ($uploads['basedir'] ?? ''));
|
||||
if ($baseurl !== '' && str_starts_with($url, $baseurl)) {
|
||||
$rel = substr($url, strlen($baseurl));
|
||||
$path = $basedir . ltrim(rawurldecode($rel), '/\\');
|
||||
$real = realpath($path);
|
||||
$root = realpath($basedir);
|
||||
if ($real && $root && str_starts_with($real, $root) && is_file($real)) {
|
||||
return $real;
|
||||
}
|
||||
}
|
||||
// Same-host path under ABSPATH.
|
||||
$home = trailingslashit(home_url('/'));
|
||||
if (str_starts_with($url, $home)) {
|
||||
$rel = substr($url, strlen($home));
|
||||
$path = ABSPATH . ltrim(rawurldecode($rel), '/\\');
|
||||
$real = realpath($path);
|
||||
$root = realpath(ABSPATH);
|
||||
if ($real && $root && str_starts_with($real, $root) && is_file($real)) {
|
||||
return $real;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count a delivered download. The guard caps concurrent finishers at
|
||||
* max_uses instead of overshooting the counter.
|
||||
*/
|
||||
private function consume_token(array $row, string $token): void {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
||||
if (!$table) return;
|
||||
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$table} SET used_count = used_count + 1 WHERE token = %s AND used_count < max_uses",
|
||||
$token
|
||||
));
|
||||
|
||||
if (!$updated) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private function fail_410(): void {
|
||||
status_header(410);
|
||||
nocache_headers();
|
||||
|
||||
// Themed expiry page; wp_die stays only as the safety net for the
|
||||
// paths that already streamed headers/body (send_download_headers,
|
||||
// stream_body) where the theme chrome can no longer be emitted.
|
||||
if (function_exists('get_header') && !headers_sent()) {
|
||||
$this->render_expired_page();
|
||||
exit;
|
||||
}
|
||||
|
||||
wp_die(
|
||||
esc_html__('下载链接已过期或无效,请回到资源页重新获取。', 'aksrt'),
|
||||
esc_html__('下载链接已失效', 'aksrt'),
|
||||
['response' => 410]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat, theme-native 410 page. All colors resolve from the runtime CSS
|
||||
* variables (--color-primary plus its html.dark override, legacy ink
|
||||
* vars flipped by body.dark-theme), so the admin-configured primary
|
||||
* color and the light/dark mode apply with no extra scripts.
|
||||
*/
|
||||
private function render_expired_page(): void {
|
||||
add_action('wp_head', static function (): void {
|
||||
$css = <<<'CSS'
|
||||
.aksrt-410{display:flex;align-items:center;justify-content:center;min-height:72vh;padding:48px 20px;text-align:center}
|
||||
.aksrt-410__card{max-width:460px}
|
||||
.aksrt-410__icon{width:92px;height:92px;margin:0 auto 28px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:2.25rem;background:var(--color-primary-soft,rgba(0,0,0,.06));color:var(--color-primary,#f04494)}
|
||||
.aksrt-410__code{font-family:var(--font-display,var(--font-body,inherit));font-size:clamp(4rem,10vw,5.5rem);font-weight:800;line-height:1;letter-spacing:.02em;color:var(--muted-4-color,#d2d2d2);margin:0 0 10px}
|
||||
.aksrt-410__title{font-family:var(--font-display,inherit);font-size:clamp(20px,3vw,26px);font-weight:700;line-height:1.3;color:var(--key-color,#333);margin:0 0 12px}
|
||||
.aksrt-410__desc{font-size:15px;line-height:1.7;color:var(--muted-color,#777);margin:0 0 32px}
|
||||
.aksrt-410__actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.aksrt-410__actions .btn-primary,.aksrt-410__actions .btn-outline{--btn-h:42px;--btn-px:22px}
|
||||
CSS;
|
||||
echo '<style id="aksrt-410-page">' . $css . '</style>' . "\n";
|
||||
}, 99);
|
||||
|
||||
$title_filter = static function (array $parts): array {
|
||||
$parts['title'] = __('下载链接已失效', 'aksrt');
|
||||
return $parts;
|
||||
};
|
||||
add_filter('document_title_parts', $title_filter);
|
||||
|
||||
get_header();
|
||||
remove_filter('document_title_parts', $title_filter);
|
||||
?>
|
||||
<main id="main-content" class="aksrt-410" tabindex="-1">
|
||||
<div class="aksrt-410__card">
|
||||
<span class="aksrt-410__icon" aria-hidden="true"><i class="fa fas fa-hourglass-end"></i></span>
|
||||
<p class="aksrt-410__code">410</p>
|
||||
<h1 class="aksrt-410__title"><?php esc_html_e('下载链接已失效', 'aksrt'); ?></h1>
|
||||
<p class="aksrt-410__desc"><?php esc_html_e('下载链接已过期或无效,请回到资源页重新获取。', 'aksrt'); ?></p>
|
||||
<div class="aksrt-410__actions">
|
||||
<a href="javascript:history.back()" class="btn-primary">
|
||||
<i class="fa fas fa-arrow-left" aria-hidden="true"></i>
|
||||
<span><?php esc_html_e('返回上一页', 'aksrt'); ?></span>
|
||||
</a>
|
||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-outline">
|
||||
<i class="fa fas fa-home" aria-hidden="true"></i>
|
||||
<span><?php esc_html_e('返回首页', 'aksrt'); ?></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php
|
||||
get_footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
private function issue_token(int $user_id, int $post_id, int $source_index, int $cost, array $config = []) {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
||||
if (!$table) {
|
||||
return new \WP_Error('aksrt_no_table', __('下载令牌表未初始化', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
if (empty($config)) {
|
||||
$config = $this->config();
|
||||
}
|
||||
|
||||
$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('+' . (int) $config['ttl'] . ' seconds')
|
||||
->format('Y-m-d H:i:s');
|
||||
|
||||
// PRIMARY KEY(token) already guarantees uniqueness; retry on the
|
||||
// astronomically unlikely collision instead of failing the request.
|
||||
for ($attempt = 0; $attempt < 3; $attempt++) {
|
||||
$token = $this->random_token(self::TOKEN_LEN);
|
||||
|
||||
$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' => (int) $config['max_uses'],
|
||||
'expires_at' => $expires_utc,
|
||||
'created_at' => $now_utc,
|
||||
],
|
||||
['%s', '%d', '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%s', '%s']
|
||||
);
|
||||
|
||||
if ($ok !== false) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
return new \WP_Error('aksrt_token_insert', __('生成下载令牌失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,6 @@ class SEOHead {
|
||||
}
|
||||
// 单一出口:所有 SEO/OG/JSON-LD 输出集中在 priority 2。
|
||||
add_action('wp_head', [$this, 'render'], 2);
|
||||
// robots:dl token 页面 noindex(独立、最早输出)。
|
||||
add_action('wp_head', [$this, 'render_robots'], 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,15 +227,6 @@ class SEOHead {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* dl token 页面 noindex。
|
||||
*/
|
||||
public function render_robots(): void {
|
||||
if (function_exists('get_query_var') && get_query_var('aksrt_dl_token')) {
|
||||
echo '<meta name="robots" content="noindex,nofollow">' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 head(favicon + canonical + description/keywords + OG/Twitter + JSON-LD)。
|
||||
*/
|
||||
|
||||
@@ -55,11 +55,6 @@ $pay_modo = is_array($priced)
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: ((($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
|
||||
$user_amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 0);
|
||||
$cost = function_exists('aksrt_resource_download_cost') ? (int) aksrt_resource_download_cost($post_id) : 0;
|
||||
|
||||
$captcha_active = function_exists('aksrt_is_captcha_enabled_for')
|
||||
&& aksrt_is_captcha_enabled_for('download');
|
||||
$balance = (int) ($gate_data['balance'] ?? (is_user_logged_in() ? \AKSRT\Ledger\LedgerService::get_instance()->balance(get_current_user_id(), \AKSRT\Ledger\AssetType::POINTS) : 0));
|
||||
$login_url = (string) ($gate_data['login_url'] ?? wp_login_url(get_permalink($post_id)));
|
||||
$vip_url = function_exists('aksrt_get_user_center_tab_url')
|
||||
? aksrt_get_user_center_tab_url('credits')
|
||||
@@ -160,7 +155,6 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
data-pay-limit="<?php echo esc_attr((string) $pay_limit); ?>"
|
||||
data-has-paid="<?php echo $has_paid_access ? '1' : '0'; ?>"
|
||||
data-credit="0"
|
||||
data-captcha="<?php echo $captcha_active ? '1' : '0'; ?>"
|
||||
aria-label="<?php esc_attr_e('下载区', 'aksrt'); ?>">
|
||||
|
||||
<?php if ($has_paid_access && $user_amount > 0) : ?>
|
||||
@@ -345,6 +339,10 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
}
|
||||
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
|
||||
$url = isset($row['url']) ? (string) $row['url'] : '';
|
||||
if ($url === '') {
|
||||
// 直连模式没有可用的原始地址,空源不渲染。
|
||||
continue;
|
||||
}
|
||||
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
|
||||
$has_extract = $extract_code !== '';
|
||||
|
||||
@@ -354,27 +352,22 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
}
|
||||
|
||||
if ($is_need_login) {
|
||||
$btn_tag = 'a';
|
||||
$btn_href = $login_url;
|
||||
$btn_text = __('登录后下载', 'aksrt');
|
||||
$btn_icon = 'fa-sign-in-alt';
|
||||
$btn_modifier = ' resource-download-box__btn--login';
|
||||
$btn_action = '';
|
||||
$btn_target = '';
|
||||
$btn_rel = '';
|
||||
} else {
|
||||
$btn_tag = 'button';
|
||||
$btn_href = '';
|
||||
$btn_href = $url;
|
||||
$btn_text = __('立即下载', 'aksrt');
|
||||
$btn_icon = 'fa-download';
|
||||
$btn_modifier = $has_paid_access ? ' resource-download-box__btn--owned' : '';
|
||||
$btn_action = 'aksrt-download';
|
||||
$btn_target = '';
|
||||
$btn_rel = '';
|
||||
$btn_target = ' target="_blank"';
|
||||
$btn_rel = ' rel="nofollow noopener"';
|
||||
}
|
||||
?>
|
||||
<li class="resource-download-box__item">
|
||||
<?php if ($btn_tag === 'a') : ?>
|
||||
<a
|
||||
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?>"
|
||||
href="<?php echo esc_url($btn_href); ?>"<?php echo $btn_target; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $btn_rel; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
@@ -388,25 +381,12 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
<?php echo esc_html($service['name']); ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php else : ?>
|
||||
<button
|
||||
type="button"
|
||||
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?>"
|
||||
data-action="<?php echo esc_attr($btn_action); ?>"
|
||||
data-source-index="<?php echo esc_attr((string) $idx); ?>"
|
||||
data-source-type="<?php echo esc_attr($type); ?>"
|
||||
data-has-extract="<?php echo $has_extract ? '1' : '0'; ?>"
|
||||
data-balance="<?php echo esc_attr((string) $balance); ?>"
|
||||
data-cost="<?php echo esc_attr((string) $cost); ?>"
|
||||
style="--service-color: <?php echo esc_attr($service['color']); ?>"
|
||||
>
|
||||
<i class="fa <?php echo esc_attr($btn_icon); ?>" aria-hidden="true"></i>
|
||||
<span><?php echo esc_html($btn_text); ?></span>
|
||||
<span class="resource-download-box__service-badge" style="background: <?php echo esc_attr($service['color']); ?>">
|
||||
<?php echo esc_html($service['name']); ?>
|
||||
</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($has_extract) : ?>
|
||||
<button type="button" class="resource-download-box__btn resource-download-box__btn--extract"
|
||||
data-clipboard-tag="<?php esc_attr_e('提取码', 'aksrt'); ?>"
|
||||
data-clipboard-text="<?php echo esc_attr($extract_code); ?>"
|
||||
><?php echo esc_html($extract_code); ?></button>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
@@ -471,12 +451,6 @@ if ($is_need_purchase && $user_amount > 0) {
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($captcha_active && function_exists('aksrt_render_captcha')) : ?>
|
||||
<div class="resource-download-box__captcha resource-download-box__captcha--hidden" data-aksrt-download-captcha>
|
||||
<?php aksrt_render_captcha('download'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="resource-download-box__footer">
|
||||
<p class="resource-download-box__notice-text">
|
||||
<i class="fa fas fa-info-circle" aria-hidden="true"></i>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
test('download relay configuration is pinned to built-in constants', () => {
|
||||
const dl = read('src/Content/Download.php');
|
||||
|
||||
// The admin settings page is gone; the option must never be read again.
|
||||
assert.doesNotMatch(dl, /aksrt_download_config/);
|
||||
|
||||
// The retired master switch is gone from the runtime.
|
||||
assert.doesNotMatch(dl, /function is_enabled/);
|
||||
|
||||
// Single source of truth: fixed endpoint, per-token uses and TTL.
|
||||
assert.match(dl, /const ENDPOINT\s*=\s*'download'/);
|
||||
assert.match(dl, /const MAX_USES\s*=\s*1/);
|
||||
assert.match(dl, /const TTL_MINUTES\s*=\s*10/);
|
||||
|
||||
// Rewrite regex and URL building share the configured alias.
|
||||
assert.match(dl, /preg_quote\(\$this->config\(\)\['endpoint'\], '\/'\)/);
|
||||
assert.match(dl, /home_url\('\/' \. \$config\['endpoint'\] \. '\/' \. \$token\)/);
|
||||
assert.match(dl, /add_action\('init', \[\$this, 'maybe_flush_rewrite'\], 99\)/);
|
||||
assert.match(dl, /flush_rewrite_rules\(false\)/);
|
||||
assert.match(dl, /aksrt_dl_rewrite_version/);
|
||||
|
||||
// TTL and per-token max uses come from the fixed configuration.
|
||||
assert.match(dl, /'expires_in'\s+=> \$config\['ttl'\]/);
|
||||
assert.match(dl, /'max_uses'\s+=> \(int\) \$config\['max_uses'\]/);
|
||||
});
|
||||
|
||||
test('the retired master switch leaves no direct-download branches behind', () => {
|
||||
const dl = read('src/Content/Download.php');
|
||||
const box = read('templates/parts/resource-download-box.php');
|
||||
const pay = read('inc/helpers/aksrt-pay.php');
|
||||
|
||||
// No runtime switch and no option reads remain.
|
||||
assert.doesNotMatch(dl, /function is_enabled/);
|
||||
assert.doesNotMatch(dl, /aksrt_download_config/);
|
||||
|
||||
// Direct mode was only reachable through the removed switch.
|
||||
assert.doesNotMatch(box, /direct_download/);
|
||||
assert.doesNotMatch(pay, /direct_download/);
|
||||
|
||||
// Relay buttons stay the only download path on both surfaces; the login
|
||||
// anchor and the paid-card relay button keep their original markup.
|
||||
assert.match(box, /data-action="<\?php echo esc_attr\(\$btn_action\); \?>/);
|
||||
assert.match(box, /href="<\?php echo esc_url\(\$btn_href\); \?>/);
|
||||
assert.match(pay, /data-action="aksrt-download"/);
|
||||
|
||||
// Dead standalone-page helper stays removed.
|
||||
assert.doesNotMatch(pay, /aksrt_is_download_alone_page_enabled/);
|
||||
});
|
||||
|
||||
test('usage is counted only after full delivery, never for probes', () => {
|
||||
const dl = read('src/Content/Download.php');
|
||||
|
||||
// Probe detection covers HEAD requests and speculative prefetches, which
|
||||
// validate availability but consume nothing.
|
||||
assert.match(dl, /private function is_probe_request\(\): bool/);
|
||||
assert.match(dl, /REQUEST_METHOD.*=== 'HEAD'/);
|
||||
assert.match(dl, /HTTP_SEC_PURPOSE/);
|
||||
assert.match(dl, /'prefetch'/);
|
||||
assert.match(dl, /!\$is_probe\) \{\s*\$this->consume_token/);
|
||||
|
||||
// Local streaming splits headers from body and aborts stop consumption.
|
||||
assert.match(dl, /private function send_download_headers\(string \$path\): void/);
|
||||
assert.match(dl, /private function stream_body\(string \$path\): bool/);
|
||||
assert.match(dl, /if \(connection_aborted\(\)\) \{/);
|
||||
assert.match(dl, /return !connection_aborted\(\);/);
|
||||
|
||||
// Atomic cap so concurrent finishes can not overshoot max_uses.
|
||||
assert.match(dl, /SET used_count = used_count \+ 1 WHERE token = %s AND used_count < max_uses/);
|
||||
|
||||
// Expired / exhausted links stay a dead end instead of re-issuing silently.
|
||||
assert.match(dl, /status_header\(410\)/);
|
||||
assert.match(dl, /请回到资源页重新获取/);
|
||||
assert.doesNotMatch(dl, /fail_410\(\);\s*issue_token/);
|
||||
});
|
||||
|
||||
test('expired links render a themed flat 410 page that follows the theme color', () => {
|
||||
const dl = read('src/Content/Download.php');
|
||||
|
||||
// Full theme chrome instead of a bare wp_die page.
|
||||
assert.match(dl, /private function render_expired_page\(\): void/);
|
||||
assert.match(dl, /get_header\(\)/);
|
||||
assert.match(dl, /get_footer\(\)/);
|
||||
|
||||
// wp_die remains only as the safety net when output already started.
|
||||
assert.match(dl, /headers_sent\(\)/);
|
||||
|
||||
// Flat hero keyed to the runtime theme variables (light + html.dark).
|
||||
assert.match(dl, /var\(--color-primary-soft/);
|
||||
assert.match(dl, /var\(--color-primary/);
|
||||
assert.match(dl, /var\(--key-color/);
|
||||
assert.match(dl, /var\(--muted-4-color/);
|
||||
|
||||
// Theme button system and FA5 icon form only (never "fa fa-").
|
||||
assert.match(dl, /class="btn-primary"/);
|
||||
assert.match(dl, /class="btn-outline"/);
|
||||
assert.match(dl, /fa fas fa-hourglass-end/);
|
||||
assert.doesNotMatch(dl, /\bfa fa-/);
|
||||
|
||||
// The 410 page is a utility rewrite; the header component must never match
|
||||
// a page type for it (custom rewrites retain the is_home flag).
|
||||
const slider = read('inc/helpers/homepage/header-components.php');
|
||||
assert.match(slider, /class_exists\('\\AKSRT\\Content\\Download'\)/);
|
||||
assert.match(slider, /get_query_var\(\\AKSRT\\Content\\Download::QUERY_VAR\)/);
|
||||
});
|
||||
|
||||
test('token generation retries on primary-key collisions', () => {
|
||||
const dl = read('src/Content/Download.php');
|
||||
assert.match(dl, /PRIMARY KEY\(token\) already guarantees uniqueness/);
|
||||
assert.match(dl, /for \(\$attempt = 0; \$attempt < 3; \$attempt\+\+\)/);
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const exists = (file) => fs.existsSync(path.join(root, file));
|
||||
|
||||
test('the token relay is fully retired', () => {
|
||||
// The relay class itself is gone.
|
||||
assert.equal(exists('src/Content/Download.php'), false);
|
||||
|
||||
// No init wiring remains (DownloadState is a different class and stays).
|
||||
assert.doesNotMatch(read('functions.php'), /Content\\Download'/);
|
||||
|
||||
// The orphaned cleanup cron is still unscheduled on theme switch.
|
||||
assert.match(read('functions.php'), /aksrt_dl_token_cleanup/);
|
||||
|
||||
// Header query-var branch and SEO noindex branch are gone with the route.
|
||||
assert.doesNotMatch(read('inc/helpers/homepage/header-components.php'), /aksrt_dl_token/);
|
||||
assert.doesNotMatch(read('src/Core/SEOHead.php'), /render_robots/);
|
||||
assert.doesNotMatch(read('src/Core/SEOHead.php'), /aksrt_dl_token/);
|
||||
});
|
||||
|
||||
test('the download box renders direct anchors with inline extract codes', () => {
|
||||
const box = read('templates/parts/resource-download-box.php');
|
||||
|
||||
// No relay button, captcha surface or low-credit data attributes.
|
||||
assert.doesNotMatch(box, /data-action="aksrt-download"/);
|
||||
assert.doesNotMatch(box, /data-captcha/);
|
||||
assert.doesNotMatch(box, /data-aksrt-download-captcha/);
|
||||
assert.doesNotMatch(box, /captcha_active/);
|
||||
assert.doesNotMatch(box, /data-balance|data-cost/);
|
||||
|
||||
// Sources without a raw URL are skipped; the button is a direct anchor.
|
||||
assert.match(box, /if \(\$url === ''\) \{[\s\S]*?continue;/);
|
||||
assert.match(box, /\$btn_href = \$url;/);
|
||||
assert.match(box, /\$btn_target = ' target="_blank"'/);
|
||||
assert.match(box, /\$btn_rel = ' rel="nofollow noopener"'/);
|
||||
|
||||
// Extract codes surface inline next to the anchor, no popup involved.
|
||||
assert.match(box, /if \(\$has_extract\) : \?>/);
|
||||
assert.match(box, /data-clipboard-text="<\?php echo esc_attr\(\$extract_code\); \?>/);
|
||||
|
||||
// The login gate keeps its own anchor.
|
||||
assert.match(box, /\$btn_href = \$login_url;/);
|
||||
});
|
||||
|
||||
test('the paid card download actions render direct anchors', () => {
|
||||
const pay = read('inc/helpers/aksrt-pay.php');
|
||||
|
||||
assert.doesNotMatch(pay, /data-action="aksrt-download"/);
|
||||
assert.doesNotMatch(pay, /data-captcha/);
|
||||
assert.doesNotMatch(pay, /data-aksrt-download-captcha/);
|
||||
assert.doesNotMatch(pay, /captcha_active/);
|
||||
|
||||
// Direct anchor to the raw source URL, empty sources skipped.
|
||||
assert.match(pay, /if \(\$url === ''\) \{[\s\S]*?continue;/);
|
||||
assert.match(pay, /<a class="mr10 but ' \. esc_attr\(\$button_class\) \. '" href="' \. esc_url\(\$url\) \. '" target="_blank" rel="nofollow noopener">/);
|
||||
|
||||
// The extract-code badge next to the anchor stays.
|
||||
assert.match(pay, /<span class="badg">' \. esc_html\(\$extract_code\)/);
|
||||
});
|
||||
|
||||
test('cms-download.js only serves the cashier now', () => {
|
||||
const js = read('assets/js/cms-download.js');
|
||||
|
||||
// The download request flow is gone.
|
||||
assert.doesNotMatch(js, /download\/request/);
|
||||
assert.doesNotMatch(js, /data-action="aksrt-download"/);
|
||||
assert.doesNotMatch(js, /renderSuccess|startDownload|performDownloadRequest|requestDownload/);
|
||||
assert.doesNotMatch(js, /AKSRTCaptcha/);
|
||||
|
||||
// Cashier, scan-pay and error views remain.
|
||||
assert.match(js, /data-aksrt-pay-content/);
|
||||
assert.match(js, /window\.aksrtOpenCashier = function/);
|
||||
assert.match(js, /function renderCashier/);
|
||||
assert.match(js, /function renderScanPay/);
|
||||
assert.match(js, /function renderError/);
|
||||
|
||||
// The script is still enqueued for resource / paid singles.
|
||||
assert.match(read('src/Core/ScriptsPart1b.php'), /'aksrt-cms-download'/);
|
||||
});
|
||||
|
||||
test('the download logs page reports historical token records', () => {
|
||||
const page = read('src/Admin/Native/PayDownloadLogs.php');
|
||||
assert.match(page, /aksrt_dl_token/);
|
||||
assert.match(page, /不再签发新令牌/);
|
||||
});
|
||||
Reference in New Issue
Block a user