refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories (core, auth, user, credits, content, payment/gateways, forum, shop, sms, ai, messages, extensions, widgets, settings, admin). Update bootstrap require paths, co-locate traits with owners, fix forum/user relative requires, drop duplicate shop controller requires, and wire home-modules load. Path-only; class names and init order unchanged.
This commit is contained in:
1 parent
2a8fe68048
commit
ce1c13b947
115 files changed
+171
-224
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Content Visibility
|
||||
*
|
||||
* Central access policy for shortcode-gated content and paid-content runtime.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Content_Visibility {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// 短代码注册统一在 AKSRT_Shortcodes(registry);本类只提供渲染实现。
|
||||
add_action('wp_ajax_aksrt_check_visibility', [$this, 'ajax_check_visibility']);
|
||||
add_action('wp_ajax_nopriv_aksrt_check_visibility', [$this, 'ajax_check_visibility']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|WP_Post|null $post
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function get_post_access_state($post = null, int $user_id = 0): array {
|
||||
$resource_post = get_post($post);
|
||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
$pay_meta = function_exists('aksrt_get_pay_meta')
|
||||
? aksrt_get_pay_meta($resource_post)
|
||||
: AKSRT_Domain_Model::default_pay_meta();
|
||||
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($pay_meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
|
||||
$priced = function_exists('aksrt_get_user_pay_price')
|
||||
? aksrt_get_user_pay_price($resource_post, $uid)
|
||||
: null;
|
||||
$pay_modo = is_array($priced)
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: ((($pay_meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
|
||||
$price = is_array($priced)
|
||||
? (float) ($priced['amount'] ?? 0)
|
||||
: (($pay_modo === 'points')
|
||||
? (float) ($pay_meta['points_price'] ?? 0)
|
||||
: (float) ($pay_meta['price'] ?? 0));
|
||||
$paid_settings = class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::get_paid_content_settings()
|
||||
: [
|
||||
'download_login' => true,
|
||||
'download_credit' => true,
|
||||
'download_captcha' => false,
|
||||
];
|
||||
|
||||
$state = [
|
||||
'post_id' => $resource_post instanceof WP_Post ? (int) $resource_post->ID : 0,
|
||||
'pay_type' => $pay_type,
|
||||
'pay_modo' => $pay_modo,
|
||||
'price' => $price,
|
||||
'need_login' => false,
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'require_captcha' => false,
|
||||
'has_paid_access' => false,
|
||||
'can_view' => true,
|
||||
'can_download' => true,
|
||||
'download_cost' => 0,
|
||||
'pay_limit' => 0,
|
||||
'user_vip_level' => 0,
|
||||
];
|
||||
|
||||
if (!($resource_post instanceof WP_Post)) {
|
||||
$state['can_view'] = false;
|
||||
$state['can_download'] = false;
|
||||
return $state;
|
||||
}
|
||||
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
|
||||
$state['has_paid_access'] = true;
|
||||
return $state;
|
||||
}
|
||||
|
||||
$has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP;
|
||||
$pay_limit = (int) ($pay_meta['pay_limit'] ?? 0);
|
||||
if (!in_array($pay_limit, [0, 1, 2], true)) {
|
||||
$pay_limit = 0;
|
||||
}
|
||||
$state['pay_limit'] = $pay_limit;
|
||||
$has_paid_order = false;
|
||||
|
||||
if ($uid > 0 && class_exists('AKSRT_Order')) {
|
||||
$has_paid_order = AKSRT_Order::get_instance()->has_paid_access(
|
||||
$uid,
|
||||
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
|
||||
(int) $resource_post->ID
|
||||
);
|
||||
}
|
||||
// Guest cookie paid unlock (Zibll pay_no_logged_in).
|
||||
if (!$has_paid_order && $uid <= 0 && function_exists('aksrt_guest_has_paid_access')) {
|
||||
$has_paid_order = aksrt_guest_has_paid_access((int) $resource_post->ID);
|
||||
}
|
||||
|
||||
$has_vip_access = false;
|
||||
$user_vip_level = is_array($priced) ? (int) ($priced['vip_level'] ?? 0) : 0;
|
||||
if ($uid > 0) {
|
||||
if ($user_vip_level <= 0 && function_exists('aksrt_get_user_vip_level')) {
|
||||
$user_vip_level = aksrt_get_user_vip_level($uid);
|
||||
}
|
||||
if (function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) {
|
||||
$has_vip_access = true;
|
||||
$user_vip_level = max($user_vip_level, 2);
|
||||
} elseif ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP || $pay_limit > 0) {
|
||||
$has_vip_access = $user_vip_level >= max(1, $pay_limit);
|
||||
} elseif ($user_vip_level > 0) {
|
||||
// VIP tier pricing may zero cost; still track membership.
|
||||
$has_vip_access = $pay_limit <= 0 || $user_vip_level >= $pay_limit;
|
||||
}
|
||||
}
|
||||
$state['user_vip_level'] = $user_vip_level;
|
||||
|
||||
// Member-only purchase restriction (Zibll pay_limit).
|
||||
if ($pay_limit > 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_NO) {
|
||||
$state['need_vip'] = $uid <= 0 || $user_vip_level < $pay_limit;
|
||||
$allowed_member = $uid > 0 && $user_vip_level >= $pay_limit;
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
|
||||
// Cash: order gate only. Points: cashier once → paid order zeros cost.
|
||||
$download_cost = 0;
|
||||
if ($pay_modo === 'points' && $price > 0 && !$has_paid_order) {
|
||||
$download_cost = (int) ceil($price);
|
||||
}
|
||||
if ($allowed_member && $price <= 0) {
|
||||
$download_cost = 0;
|
||||
}
|
||||
$state['download_cost'] = max(0, $download_cost);
|
||||
$state['need_login'] = $uid <= 0;
|
||||
$state['need_purchase'] = $allowed_member && $price > 0 && !$has_paid_order;
|
||||
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $price <= 0);
|
||||
$state['can_view'] = true;
|
||||
$state['can_download'] = $allowed_member && ($uid > 0) && ($has_paid_order || $price <= 0);
|
||||
if (!$allowed_member) {
|
||||
$state['can_download'] = false;
|
||||
$state['has_paid_access'] = false;
|
||||
$state['need_purchase'] = false;
|
||||
$state['need_vip'] = true;
|
||||
}
|
||||
return $state;
|
||||
}
|
||||
|
||||
// reading/gallery/video with pay_limit
|
||||
if ($allowed_member) {
|
||||
$state['has_paid_access'] = $has_paid_order || $price <= 0;
|
||||
$state['need_purchase'] = !$state['has_paid_access'];
|
||||
$state['need_vip'] = false;
|
||||
} else {
|
||||
$state['has_paid_access'] = false;
|
||||
$state['need_purchase'] = false;
|
||||
$state['need_vip'] = true;
|
||||
}
|
||||
$state['can_view'] = $state['has_paid_access'];
|
||||
$state['can_download'] = false;
|
||||
return $state;
|
||||
}
|
||||
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
|
||||
// Zibll: cash = order unlock; points = one-shot cashier order (not per-download deduct).
|
||||
$download_cost = 0;
|
||||
if ($pay_modo === 'points' && $price > 0 && !$has_paid_order) {
|
||||
$download_cost = (int) ceil($price);
|
||||
}
|
||||
$state['download_cost'] = max(0, $download_cost);
|
||||
|
||||
$free_logged_show = !empty($paid_settings['free_logged_show']);
|
||||
$state['need_login'] = !empty($paid_settings['download_login'])
|
||||
|| !empty($pay_meta['require_login'])
|
||||
|| ($free_logged_show && $price <= 0);
|
||||
$state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
|
||||
|
||||
$state['need_purchase'] = !$has_paid_order && $price > 0;
|
||||
$state['has_paid_access'] = $has_paid_order || $price <= 0 || $has_free_access;
|
||||
$state['can_view'] = true;
|
||||
$state['can_download'] = (!$state['need_login'] || $uid > 0) && ($has_paid_order || $price <= 0);
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
|
||||
$state['need_vip'] = true;
|
||||
$state['has_paid_access'] = $has_vip_access;
|
||||
$state['can_view'] = $has_vip_access;
|
||||
$state['can_download'] = false;
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
$state['need_login'] = !empty($pay_meta['require_login']);
|
||||
$state['need_purchase'] = !$has_free_access && !$has_paid_order;
|
||||
$state['has_paid_access'] = $has_free_access || $has_paid_order;
|
||||
// VIP free tier for reading etc. via amount.
|
||||
if (!$state['has_paid_access'] && $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP) {
|
||||
$state['has_paid_access'] = true;
|
||||
$state['need_purchase'] = false;
|
||||
}
|
||||
$state['can_view'] = $state['has_paid_access'] && (!$state['need_login'] || $uid > 0);
|
||||
$state['can_download'] = false;
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
public static function can_view_paid_content($post = null, int $user_id = 0): bool {
|
||||
$state = self::get_post_access_state($post, $user_id);
|
||||
return !empty($state['can_view']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|WP_Post|null $post
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function can_download_resource($post = null, int $user_id = 0) {
|
||||
$resource_post = get_post($post);
|
||||
if (!($resource_post instanceof WP_Post) || !function_exists('aksrt_is_downloadable_content') || !aksrt_is_downloadable_content($resource_post)) {
|
||||
return new WP_Error('aksrt_not_downloadable', __('该内容未启用下载能力。', 'aksrt'));
|
||||
}
|
||||
|
||||
$state = self::get_post_access_state($resource_post, $user_id);
|
||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
|
||||
if (($state['pay_type'] ?? '') !== AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
|
||||
return new WP_Error('aksrt_not_downloadable', __('该内容未启用下载能力。', 'aksrt'));
|
||||
}
|
||||
|
||||
if (!empty($state['need_login']) && $uid <= 0) {
|
||||
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
|
||||
'status' => 401,
|
||||
'login_url' => wp_login_url(get_permalink($resource_post)),
|
||||
]);
|
||||
}
|
||||
|
||||
// Free download + pay_free_logged_show (belt-and-suspenders with state.need_login).
|
||||
$cost = (int) ($state['download_cost'] ?? 0);
|
||||
$paid_settings = class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::get_paid_content_settings()
|
||||
: ['download_credit' => true, 'free_logged_show' => false];
|
||||
|
||||
if ($cost <= 0 && !empty($paid_settings['free_logged_show']) && $uid <= 0) {
|
||||
return new WP_Error('aksrt_need_login', __('免费资源,请登录后下载。', 'aksrt'), [
|
||||
'status' => 401,
|
||||
'login_url' => wp_login_url(get_permalink($resource_post)),
|
||||
]);
|
||||
}
|
||||
|
||||
// Member-only (pay_limit).
|
||||
if (!empty($state['need_vip']) && empty($state['has_paid_access']) && empty($state['can_download'])) {
|
||||
$need_level = (int) ($state['pay_limit'] ?? 1);
|
||||
return new WP_Error('aksrt_need_vip', sprintf(
|
||||
/* translators: %d: required VIP level */
|
||||
__('该资源仅限 VIP%s 及以上会员购买/下载。', 'aksrt'),
|
||||
max(1, $need_level)
|
||||
), [
|
||||
'status' => 403,
|
||||
'pay_limit' => $need_level,
|
||||
]);
|
||||
}
|
||||
|
||||
// Cash or points: require cashier purchase (resource order) before download token.
|
||||
if (!empty($state['need_purchase']) && empty($state['has_paid_access'])) {
|
||||
$pay_modo = (string) ($state['pay_modo'] ?? '0');
|
||||
$price = (float) ($state['price'] ?? 0);
|
||||
return new WP_Error('aksrt_need_purchase', __('请先购买后再下载。', 'aksrt'), [
|
||||
'status' => 402,
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $pay_modo === 'points' ? (int) ceil($price) : $price,
|
||||
'post_id' => (int) ($state['post_id'] ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
// Zibll daily free-download quota (only when free / already unlocked).
|
||||
if ($cost <= 0 && function_exists('aksrt_check_free_download_quota')) {
|
||||
// Provide per-post overbuy price to quota error.
|
||||
$over_cb = static function ($over, $uid) use ($resource_post) {
|
||||
if (!($resource_post instanceof WP_Post) || !function_exists('aksrt_get_pay_meta')) {
|
||||
return $over;
|
||||
}
|
||||
$m = aksrt_get_pay_meta($resource_post);
|
||||
return max(0.0, (float) ($m['download_limit_over_price'] ?? 0));
|
||||
};
|
||||
add_filter('aksrt_download_limit_over_price', $over_cb, 10, 2);
|
||||
$quota = aksrt_check_free_download_quota($uid, $cost);
|
||||
remove_filter('aksrt_download_limit_over_price', $over_cb, 10);
|
||||
if (is_wp_error($quota)) {
|
||||
$data = (array) $quota->get_error_data();
|
||||
$data['post_id'] = (int) ($state['post_id'] ?? 0);
|
||||
$data['pay_modo'] = (string) ($state['pay_modo'] ?? '0');
|
||||
return new WP_Error($quota->get_error_code(), $quota->get_error_message(), $data);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render_hide_shortcode(array $atts, string $content = ''): string {
|
||||
$atts = shortcode_atts([
|
||||
'mode' => 'comment',
|
||||
'message' => '',
|
||||
'pid' => get_the_ID(),
|
||||
], $atts, 'aksrt_hide');
|
||||
|
||||
$mode = sanitize_key((string) $atts['mode']);
|
||||
$pid = (int) $atts['pid'];
|
||||
$message = (string) $atts['message'];
|
||||
|
||||
if ($pid <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->is_unlocked($mode, $pid)) {
|
||||
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked">' . do_shortcode($content) . '</div>';
|
||||
}
|
||||
|
||||
if ($message === '') {
|
||||
switch ($mode) {
|
||||
case 'comment':
|
||||
$message = __('评论后查看隐藏内容', 'aksrt');
|
||||
break;
|
||||
case 'login':
|
||||
$message = __('登录后查看隐藏内容', 'aksrt');
|
||||
break;
|
||||
case 'password':
|
||||
$message = __('输入密码后查看隐藏内容', 'aksrt');
|
||||
break;
|
||||
default:
|
||||
$message = __('此内容已隐藏', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
$wrapper_class = 'aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--' . esc_attr($mode);
|
||||
$html = '<div class="' . $wrapper_class . '" data-aksrt-visibility-mode="' . esc_attr($mode) . '" data-aksrt-visibility-pid="' . esc_attr((string) $pid) . '">';
|
||||
$html .= '<div class="aksrt-hidden-content__message">';
|
||||
$html .= '<i class="fas fa-lock" aria-hidden="true"></i> ';
|
||||
$html .= '<span>' . esc_html($message) . '</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
if ($mode === 'password') {
|
||||
$html .= '<div class="aksrt-hidden-content__form">';
|
||||
$html .= '<input type="password" class="aksrt-hidden-content__password-input" placeholder="' . esc_attr__('请输入密码', 'aksrt') . '">';
|
||||
$html .= '<button type="button" class="aksrt-hidden-content__submit-btn btn-primary btn-sm">' . esc_html__('验证', 'aksrt') . '</button>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function is_unlocked(string $mode, int $pid): bool {
|
||||
switch ($mode) {
|
||||
case 'comment':
|
||||
if (!is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return get_comments([
|
||||
'post_id' => $pid,
|
||||
'user_id' => get_current_user_id(),
|
||||
'count' => true,
|
||||
'status' => 'approve',
|
||||
]) > 0;
|
||||
|
||||
case 'login':
|
||||
return is_user_logged_in();
|
||||
|
||||
case 'password':
|
||||
return !empty($_COOKIE['aksrt_hide_pwd_' . $pid]);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function ajax_check_visibility(): void {
|
||||
check_ajax_referer('aksrt_visibility', '_nonce');
|
||||
|
||||
$mode = sanitize_key((string) ($_POST['mode'] ?? ''));
|
||||
$pid = (int) ($_POST['pid'] ?? 0);
|
||||
|
||||
if ($pid <= 0 || !in_array($mode, ['comment', 'login', 'password'], true)) {
|
||||
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
||||
}
|
||||
|
||||
if ($mode === 'password') {
|
||||
$password = sanitize_text_field((string) ($_POST['password'] ?? ''));
|
||||
$post_password = get_post_password($pid);
|
||||
if ($post_password === '' || $password !== $post_password) {
|
||||
wp_send_json_error(['message' => __('密码错误', 'aksrt')]);
|
||||
}
|
||||
|
||||
setcookie('aksrt_hide_pwd_' . $pid, '1', time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
|
||||
wp_send_json_success(['message' => __('验证成功', 'aksrt')]);
|
||||
}
|
||||
|
||||
if ($this->is_unlocked($mode, $pid)) {
|
||||
wp_send_json_success(['message' => __('已解锁', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('未满足条件', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Download State
|
||||
*
|
||||
* Unified download state resolver for resource pages.
|
||||
* Driven by AKSRT_Content_Visibility (cashier unlock), not per-download credits.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Download_State {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const STATE_NOT_LOGGED_IN = 'not_logged_in';
|
||||
const STATE_NO_PERMISSION = 'no_permission';
|
||||
const STATE_NEED_PURCHASE = 'need_purchase';
|
||||
const STATE_NEED_VIP = 'need_vip';
|
||||
const STATE_OWNED = 'owned';
|
||||
const STATE_AVAILABLE = 'available';
|
||||
const STATE_PENDING_DELIVERY = 'pending_delivery';
|
||||
/** @deprecated use STATE_NEED_PURCHASE — kept for old templates */
|
||||
const STATE_INSUFFICIENT_CREDITS = 'need_purchase';
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current user's download state for a given resource post.
|
||||
*
|
||||
* @param int $post_id Resource post ID.
|
||||
* @return array{
|
||||
* state:string,can_download:bool,message:string,action_label:string,
|
||||
* action_url:string,cost:int,balance:int,login_url:string,pay_modo:string,
|
||||
* amount:float,need_purchase:bool,need_vip:bool,has_paid_access:bool
|
||||
* }
|
||||
*/
|
||||
public function resolve(int $post_id): array {
|
||||
$login_url = wp_login_url(get_permalink($post_id));
|
||||
$is_logged_in = is_user_logged_in();
|
||||
$user_id = $is_logged_in ? get_current_user_id() : 0;
|
||||
|
||||
$meta = function_exists('aksrt_get_download_box_meta')
|
||||
? aksrt_get_download_box_meta($post_id)
|
||||
: (function_exists('aksrt_get_resource_meta') ? aksrt_get_resource_meta($post_id) : []);
|
||||
$downloads = is_array($meta['downloads'] ?? null) ? $meta['downloads'] : [];
|
||||
|
||||
if ($downloads === []) {
|
||||
return $this->build_result(
|
||||
self::STATE_NO_PERMISSION,
|
||||
false,
|
||||
__('该资源暂未提供下载链接。', 'aksrt'),
|
||||
__('暂无下载', 'aksrt'),
|
||||
'',
|
||||
0,
|
||||
0,
|
||||
$login_url
|
||||
);
|
||||
}
|
||||
|
||||
$access = class_exists('AKSRT_Content_Visibility')
|
||||
? AKSRT_Content_Visibility::get_post_access_state($post_id, $user_id)
|
||||
: [];
|
||||
$priced = function_exists('aksrt_get_user_pay_price')
|
||||
? aksrt_get_user_pay_price($post_id, $user_id)
|
||||
: null;
|
||||
$pay_modo = is_array($priced)
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: '0';
|
||||
$amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : 0.0;
|
||||
$cost = function_exists('aksrt_resource_download_cost')
|
||||
? (int) aksrt_resource_download_cost($post_id)
|
||||
: 0;
|
||||
$balance = ($user_id > 0 && class_exists('AKSRT_Credits'))
|
||||
? AKSRT_Credits::balance($user_id)
|
||||
: 0;
|
||||
|
||||
$gate = function_exists('aksrt_can_download') ? aksrt_can_download($post_id, $user_id) : true;
|
||||
if (is_wp_error($gate)) {
|
||||
$code = $gate->get_error_code();
|
||||
$data = (array) $gate->get_error_data();
|
||||
$msg = $gate->get_error_message() ?: __('无下载权限。', 'aksrt');
|
||||
|
||||
if ($code === 'aksrt_need_login') {
|
||||
return $this->build_result(
|
||||
self::STATE_NOT_LOGGED_IN,
|
||||
false,
|
||||
__('登录后可下载此资源。', 'aksrt'),
|
||||
__('登录下载', 'aksrt'),
|
||||
(string) ($data['login_url'] ?? $login_url),
|
||||
$cost,
|
||||
$balance,
|
||||
(string) ($data['login_url'] ?? $login_url),
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $amount,
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => !empty($access['has_paid_access']),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($code === 'aksrt_need_vip') {
|
||||
$vip_url = function_exists('aksrt_get_user_center_tab_url')
|
||||
? aksrt_get_user_center_tab_url('credits')
|
||||
: $login_url;
|
||||
return $this->build_result(
|
||||
self::STATE_NEED_VIP,
|
||||
false,
|
||||
$msg,
|
||||
__('开通会员', 'aksrt'),
|
||||
$vip_url,
|
||||
$cost,
|
||||
$balance,
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $amount,
|
||||
'need_purchase' => false,
|
||||
'need_vip' => true,
|
||||
'has_paid_access' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// need_purchase / legacy insufficient credits → cashier.
|
||||
if (in_array($code, ['aksrt_need_purchase', 'aksrt_credits_insufficient'], true)) {
|
||||
$label = $pay_modo === 'points'
|
||||
? sprintf(__('购买(%s 积分)', 'aksrt'), (string) (int) ceil($amount > 0 ? $amount : $cost))
|
||||
: __('去购买', 'aksrt');
|
||||
return $this->build_result(
|
||||
self::STATE_NEED_PURCHASE,
|
||||
false,
|
||||
$msg ?: __('请先购买后再下载。', 'aksrt'),
|
||||
$label,
|
||||
'',
|
||||
$cost,
|
||||
$balance,
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $amount > 0 ? $amount : (float) $cost,
|
||||
'need_purchase' => true,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $this->build_result(
|
||||
self::STATE_NO_PERMISSION,
|
||||
false,
|
||||
$msg,
|
||||
__('无权限', 'aksrt'),
|
||||
'',
|
||||
$cost,
|
||||
$balance,
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $amount,
|
||||
'need_purchase' => !empty($access['need_purchase']),
|
||||
'need_vip' => !empty($access['need_vip']),
|
||||
'has_paid_access' => !empty($access['has_paid_access']),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Paid / free unlock.
|
||||
$has_paid = !empty($access['has_paid_access']);
|
||||
if ($has_paid || $amount <= 0) {
|
||||
$owned = $has_paid && $amount > 0;
|
||||
return $this->build_result(
|
||||
$owned ? self::STATE_OWNED : self::STATE_AVAILABLE,
|
||||
true,
|
||||
$owned
|
||||
? __('已购买,可下载。', 'aksrt')
|
||||
: __('此资源可免费下载。', 'aksrt'),
|
||||
$owned ? __('立即下载', 'aksrt') : __('立即下载', 'aksrt'),
|
||||
'',
|
||||
0,
|
||||
$balance,
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => 0.0,
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Gate true but still priced unpaid (should be rare).
|
||||
return $this->build_result(
|
||||
self::STATE_NEED_PURCHASE,
|
||||
false,
|
||||
__('请先购买后再下载。', 'aksrt'),
|
||||
__('去购买', 'aksrt'),
|
||||
'',
|
||||
$cost,
|
||||
$balance,
|
||||
$login_url,
|
||||
[
|
||||
'pay_modo' => $pay_modo,
|
||||
'amount' => $amount,
|
||||
'need_purchase' => true,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $extra
|
||||
*/
|
||||
private function build_result(
|
||||
string $state,
|
||||
bool $can_download,
|
||||
string $message,
|
||||
string $action_label,
|
||||
string $action_url,
|
||||
int $cost,
|
||||
int $balance,
|
||||
string $login_url,
|
||||
array $extra = []
|
||||
): array {
|
||||
return array_merge([
|
||||
'state' => $state,
|
||||
'can_download' => $can_download,
|
||||
'message' => $message,
|
||||
'action_label' => $action_label,
|
||||
'action_url' => $action_url,
|
||||
'cost' => $cost,
|
||||
'balance' => $balance,
|
||||
'login_url' => $login_url,
|
||||
'pay_modo' => '0',
|
||||
'amount' => 0.0,
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'has_paid_access' => false,
|
||||
], $extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Download
|
||||
*
|
||||
* Resource download flow:
|
||||
* 1) Frontend calls POST /aksrt/v1/download/request to get a short-lived token link.
|
||||
* 2) User visits /aksrt-dl/{token}, token is validated, consumed, then redirected.
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Download {
|
||||
|
||||
const TOKEN_TTL = 600; // 10 minutes
|
||||
const TOKEN_LEN = 48;
|
||||
const QUERY_VAR = 'aksrt_dl_token';
|
||||
const ENDPOINT = 'aksrt-dl';
|
||||
|
||||
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('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_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<id>\\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 || !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) : [];
|
||||
$paid_settings = AKSRT_Settings::get_paid_content_settings();
|
||||
$cost = aksrt_resource_download_cost($post);
|
||||
$uid = get_current_user_id();
|
||||
$priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post, $uid) : null;
|
||||
$pay_modo = is_array($priced)
|
||||
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
|
||||
: ((($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
|
||||
$display_price = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 0);
|
||||
|
||||
$access = class_exists('AKSRT_Content_Visibility')
|
||||
? AKSRT_Content_Visibility::get_post_access_state($post, $uid)
|
||||
: [];
|
||||
|
||||
$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' => $display_price,
|
||||
'base_price' => is_array($priced) ? (float) ($priced['base'] ?? $display_price) : (float) ($meta['price'] ?? 0),
|
||||
'pay_modo' => $pay_modo,
|
||||
'pay_limit' => (int) ($meta['pay_limit'] ?? 0),
|
||||
'cost' => $cost,
|
||||
'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']) || (!empty($paid_settings['free_logged_show']) && $cost <= 0 && $display_price <= 0),
|
||||
'require_captcha' => $need_captcha,
|
||||
'logged_in' => $uid > 0,
|
||||
'balance' => $uid > 0 && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0,
|
||||
'has_paid_access' => !empty($access['has_paid_access']),
|
||||
'need_purchase' => !empty($access['need_purchase']),
|
||||
'need_vip' => !empty($access['need_vip']),
|
||||
'need_login' => !empty($access['need_login']) && $uid <= 0,
|
||||
'can_download' => !empty($access['can_download']),
|
||||
'login_url' => wp_login_url(get_permalink($post)),
|
||||
'vip_level' => (int) ($access['user_vip_level'] ?? 0),
|
||||
'free_limit' => $uid > 0 && function_exists('aksrt_get_user_free_download_limit') ? aksrt_get_user_free_download_limit($uid) : 0,
|
||||
'free_used' => $uid > 0 && function_exists('aksrt_get_user_free_downloaded_number') ? aksrt_get_user_free_downloaded_number($uid) : 0,
|
||||
'free_surplus' => $uid > 0 && function_exists('aksrt_get_free_download_surplus') ? aksrt_get_free_download_surplus($uid) : null,
|
||||
'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 || !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;
|
||||
|
||||
$paid_settings = AKSRT_Settings::get_paid_content_settings();
|
||||
$uid = get_current_user_id();
|
||||
$cost = aksrt_resource_download_cost($post);
|
||||
|
||||
// Daily free-download quota (Zibll).
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
|
||||
// 下载双开关:cms_features.download_captcha(或单资源强制)且场景未停用。
|
||||
if ($need_captcha && function_exists('aksrt_require_captcha')) {
|
||||
$cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : [];
|
||||
// 场景未启用时 require 直接放行;启用时强制校验 token。
|
||||
if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download')) {
|
||||
$verify = aksrt_require_captcha('download', $cap_payload);
|
||||
if ($verify !== true) {
|
||||
$msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt');
|
||||
return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Purchase is one-shot via cashier (resource order). Token issue does not re-deduct.
|
||||
$token = $this->issue_token($uid, $post_id, $source_index, 0);
|
||||
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('/' . 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 || !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();
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$target_url = esc_url_raw((string) $meta['downloads'][$idx]['url']);
|
||||
$speed_kb = 0;
|
||||
$uid_speed = (int) ($row['user_id'] ?? 0);
|
||||
if ($uid_speed > 0 && function_exists('aksrt_get_user_download_speed')) {
|
||||
$speed_kb = aksrt_get_user_download_speed($uid_speed);
|
||||
}
|
||||
|
||||
// Local attachment / uploads file: optional throttled stream (Zibll speed limit).
|
||||
$local_path = $this->resolve_local_download_path($target_url);
|
||||
if ($local_path !== '' && is_readable($local_path)) {
|
||||
$this->stream_local_file($local_path, $speed_kb);
|
||||
exit;
|
||||
}
|
||||
|
||||
nocache_headers();
|
||||
wp_redirect($target_url, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a local file with optional KB/s throttle.
|
||||
*/
|
||||
private function stream_local_file(string $path, int $speed_kb = 0): 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);
|
||||
|
||||
$chunk = 1024 * 64; // 64KB
|
||||
$sleep_us = 0;
|
||||
if ($speed_kb > 0) {
|
||||
// Emit ~speed_kb KB per second using fixed 64KB chunks when possible.
|
||||
$bytes_per_sec = max(1024, $speed_kb * 1024);
|
||||
$chunk = min(1024 * 64, $bytes_per_sec);
|
||||
$chunk = max(1024, $chunk);
|
||||
// Sleep so chunk_bytes / second ≈ bytes_per_sec.
|
||||
$sleep_us = (int) max(0, (int) round(($chunk / $bytes_per_sec) * 1000000) - 1000);
|
||||
}
|
||||
|
||||
$fh = fopen($path, 'rb');
|
||||
if (!$fh) {
|
||||
$this->fail_410();
|
||||
}
|
||||
// Disable output compression for accurate Content-Length / throttle.
|
||||
if (function_exists('apache_setenv')) {
|
||||
@apache_setenv('no-gzip', '1');
|
||||
}
|
||||
@ini_set('zlib.output_compression', 'Off');
|
||||
while (!feof($fh)) {
|
||||
$buf = fread($fh, $chunk);
|
||||
if ($buf === false) {
|
||||
break;
|
||||
}
|
||||
echo $buf;
|
||||
if (function_exists('flush')) {
|
||||
flush();
|
||||
}
|
||||
if ($sleep_us > 0) {
|
||||
usleep($sleep_us);
|
||||
}
|
||||
}
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user