Bump content sales on resource order paid; show order number/time in download box; poll pending gateway payments; support download_limit_over_price overbuy.
408 lines
17 KiB
PHP
408 lines
17 KiB
PHP
<?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
|
||
);
|
||
}
|
||
|
||
$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')]);
|
||
}
|
||
}
|