Files
wordpress-theme-aksrt/src/AKSRT/Content/Visibility.php
T
kate522 aaf458af1c refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
2026-07-17 13:29:22 +08:00

410 lines
17 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
/**
* @package AKSRT
*/
namespace AKSRT\Content;
defined('ABSPATH') || exit;
class 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\Core\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\Core\DomainModel::default_pay_meta();
$pay_type = \AKSRT\Core\DomainModel::normalize_pay_type((string) ($pay_meta['pay_type'] ?? \AKSRT\Core\DomainModel::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\Settings')
? \AKSRT\Settings\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\Core\DomainModel::PAY_TYPE_NO) {
$state['has_paid_access'] = true;
return $state;
}
$has_free_access = $price <= 0 && $pay_type !== \AKSRT\Core\DomainModel::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\Payment\Order')) {
$has_paid_order = \AKSRT\Payment\Order::get_instance()->has_paid_access(
$uid,
\AKSRT\Core\DomainModel::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\Core\DomainModel::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\Core\DomainModel::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\Core\DomainModel::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\Core\DomainModel::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\Core\DomainModel::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\Core\DomainModel::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\Core\DomainModel::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\Settings')
? \AKSRT\Settings\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')]);
}
}