Files
wordpress-theme-aksrt/inc/classes/class-aksrt-content-visibility.php
T
kate522 1c8e4d3919 feat(pay): zibll-style pay_modo/points/pay_limit, VIP quota, and local speed stream
Expand post pay meta with cash/points modes and member purchase limits,
enforce free-download daily quotas, and stream local files with VIP speed
caps while keeping remote links on redirect.
2026-07-14 01:11:33 +08:00

349 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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));
$price = (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,
'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,
];
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) {
return $state;
}
$has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP;
// Prefer points_price when pay_modo=points.
if (is_array($pay_meta) && (($pay_meta['pay_modo'] ?? '0') === 'points')) {
$price = (float) ($pay_meta['points_price'] ?? $price);
$state['price'] = $price;
$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;
}
$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
);
}
$has_vip_access = false;
$user_vip_level = 0;
if ($uid > 0) {
if (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 (function_exists('aksrt_is_vip') && ($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) {
$has_vip_access = $pay_limit <= 0 || $user_vip_level >= $pay_limit;
}
}
// Member-only purchase restriction (Zibll pay_limit).
if ($pay_limit > 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_NO) {
$state['need_vip'] = true;
$state['pay_limit'] = $pay_limit;
$allowed_member = $has_vip_access && $user_vip_level >= $pay_limit;
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
$download_cost = function_exists('aksrt_resource_download_cost')
? (int) aksrt_resource_download_cost($resource_post)
: (int) ceil($price);
$state['download_cost'] = max(0, $download_cost);
$state['need_login'] = $uid <= 0;
$state['need_purchase'] = $allowed_member && $state['download_cost'] > 0 && !$has_paid_order;
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $state['download_cost'] <= 0 || $has_vip_access);
$state['can_view'] = true;
$state['can_download'] = $allowed_member && ($uid > 0) && ($has_paid_order || $state['download_cost'] <= 0 || $has_vip_access);
if (!$allowed_member) {
$state['can_download'] = false;
$state['has_paid_access'] = false;
}
return $state;
}
$state['has_paid_access'] = $allowed_member || $has_paid_order;
$state['can_view'] = $state['has_paid_access'];
$state['can_download'] = false;
$state['need_purchase'] = !$state['has_paid_access'];
return $state;
}
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
$download_cost = function_exists('aksrt_resource_download_cost')
? (int) aksrt_resource_download_cost($resource_post)
: (int) ceil($price);
$state['download_cost'] = max(0, $download_cost);
// Zibll pay_free_logged_show: free download still requires login.
$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 && $state['download_cost'] <= 0);
$state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
$state['need_purchase'] = !$has_vip_access && $state['download_cost'] > 0;
$state['has_paid_access'] = $has_vip_access || $has_paid_order || !$state['need_purchase'];
$state['can_view'] = true;
$state['can_download'] = !$state['need_login'] || $uid > 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;
$state['has_paid_access'] = $has_free_access || $has_paid_order || $has_vip_access;
$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)),
]);
}
// Zibll daily free-download quota.
if ($cost <= 0 && function_exists('aksrt_check_free_download_quota')) {
$quota = aksrt_check_free_download_quota($uid, $cost);
if (is_wp_error($quota)) {
return $quota;
}
}
if (!empty($paid_settings['download_credit']) && $cost > 0) {
$balance = class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0;
if ($balance < $cost) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足,无法下载。', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $cost,
]);
}
}
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')]);
}
}