fix(download): pin frontend link generation to fixed relay config
The runtime kept reading aksrt_download_config after the settings page was removed, so the frontend still issued download links per the stale option values instead of the built-in defaults. - Pin endpoint, per-token uses and TTL to built-in constants and drop all option reads plus the is_enabled() master switch - Remove the now-unreachable direct-download branches from the download box template and the paid-card download helper - Lock the pinned contract in the download contract tests
This commit is contained in:
@@ -1457,20 +1457,12 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
|
|||||||
return '<div class="muted-2-color text-center">' . esc_html__('暂无可下载资源', 'aksrt') . '</div>';
|
return '<div class="muted-2-color text-center">' . esc_html__('暂无可下载资源', 'aksrt') . '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载总开关关闭 = 直连模式:按钮直接输出原始地址,不走本站中转链接。
|
|
||||||
$direct_download = class_exists('\AKSRT\Content\Download')
|
|
||||||
&& !\AKSRT\Content\Download::get_instance()->is_enabled();
|
|
||||||
|
|
||||||
$buttons = '';
|
$buttons = '';
|
||||||
foreach ($downloads as $index => $download) {
|
foreach ($downloads as $index => $download) {
|
||||||
if (!is_array($download)) {
|
if (!is_array($download)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$url = (string) ($download['url'] ?? '');
|
$url = (string) ($download['url'] ?? '');
|
||||||
if ($direct_download && $url === '') {
|
|
||||||
// 直连模式没有可用的原始地址,空源不渲染。
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$label = trim((string) ($download['label'] ?? ''));
|
$label = trim((string) ($download['label'] ?? ''));
|
||||||
$service = function_exists('aksrt_detect_cloud_service')
|
$service = function_exists('aksrt_detect_cloud_service')
|
||||||
? aksrt_detect_cloud_service($url)
|
? aksrt_detect_cloud_service($url)
|
||||||
@@ -1489,13 +1481,8 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) {
|
|||||||
$copy_label = $extract_code !== '' ? $extract_code : ($copy_key !== '' ? $copy_key : __('更多内容', 'aksrt'));
|
$copy_label = $extract_code !== '' ? $extract_code : ($copy_key !== '' ? $copy_key : __('更多内容', 'aksrt'));
|
||||||
|
|
||||||
$buttons .= '<div class="but-download flex ac">';
|
$buttons .= '<div class="but-download flex ac">';
|
||||||
if ($direct_download) {
|
$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 .= '<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) . '</button>';
|
||||||
$buttons .= '<i class="fa fas fa-download" aria-hidden="true"></i>' . esc_html($label) . '</a>';
|
|
||||||
} else {
|
|
||||||
$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>';
|
|
||||||
}
|
|
||||||
if ($copy_value !== '') {
|
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>';
|
$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 !== '') {
|
} elseif ($extract_code !== '') {
|
||||||
|
|||||||
+13
-56
@@ -12,11 +12,12 @@ class Download {
|
|||||||
const TOKEN_LEN = 48;
|
const TOKEN_LEN = 48;
|
||||||
const QUERY_VAR = 'aksrt_dl_token';
|
const QUERY_VAR = 'aksrt_dl_token';
|
||||||
|
|
||||||
// Hardened fallbacks used when the aksrt_download_config option is
|
// Fixed relay configuration. The admin download-config settings page was
|
||||||
// missing or contains values the admin validation would reject.
|
// removed, so these constants are the single source of truth for the
|
||||||
const FALLBACK_ENDPOINT = 'download';
|
// site-routed download system (endpoint, per-token uses, TTL).
|
||||||
const FALLBACK_MAX_USES = 1;
|
const ENDPOINT = 'download';
|
||||||
const FALLBACK_TTL_MINUTES = 10;
|
const MAX_USES = 1;
|
||||||
|
const TTL_MINUTES = 10;
|
||||||
|
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
@@ -69,51 +70,17 @@ class Download {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master switch for the site-routed download system (aksrt_download_config
|
* Effective download settings for the fixed relay configuration.
|
||||||
* "enabled", default true). When off, download buttons render the raw
|
|
||||||
* source URLs directly and every token route/endpoint below stays dormant.
|
|
||||||
*/
|
|
||||||
public function is_enabled(): bool {
|
|
||||||
$cfg = get_option('aksrt_download_config', []);
|
|
||||||
if (!is_array($cfg)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !isset($cfg['enabled']) || !empty($cfg['enabled']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Effective download settings (aksrt_download_config), clamped so a
|
|
||||||
* corrupted option can never break routing or accounting.
|
|
||||||
*/
|
*/
|
||||||
private function config(): array {
|
private function config(): array {
|
||||||
$cfg = get_option('aksrt_download_config', []);
|
|
||||||
if (!is_array($cfg)) {
|
|
||||||
$cfg = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$alias = trim((string) ($cfg['url_alias'] ?? self::FALLBACK_ENDPOINT));
|
|
||||||
if ($alias === '' || strlen($alias) > 48 || !preg_match('/^[A-Za-z0-9_-]+$/', $alias)) {
|
|
||||||
$alias = self::FALLBACK_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
$max_uses = isset($cfg['default_max_uses']) ? (int) $cfg['default_max_uses'] : self::FALLBACK_MAX_USES;
|
|
||||||
$max_uses = min(100, max(1, $max_uses));
|
|
||||||
|
|
||||||
$ttl_minutes = isset($cfg['ttl_minutes']) ? (int) $cfg['ttl_minutes'] : self::FALLBACK_TTL_MINUTES;
|
|
||||||
$ttl_minutes = min(1440, max(1, $ttl_minutes));
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'endpoint' => $alias,
|
'endpoint' => self::ENDPOINT,
|
||||||
'max_uses' => $max_uses,
|
'max_uses' => self::MAX_USES,
|
||||||
'ttl' => $ttl_minutes * MINUTE_IN_SECONDS,
|
'ttl' => self::TTL_MINUTES * MINUTE_IN_SECONDS,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function register_rewrite(): void {
|
public function register_rewrite(): void {
|
||||||
if (!$this->is_enabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_rewrite_rule(
|
add_rewrite_rule(
|
||||||
'^' . preg_quote($this->config()['endpoint'], '/') . '/([A-Za-z0-9]{1,64})/?$',
|
'^' . preg_quote($this->config()['endpoint'], '/') . '/([A-Za-z0-9]{1,64})/?$',
|
||||||
'index.php?' . self::QUERY_VAR . '=$matches[1]',
|
'index.php?' . self::QUERY_VAR . '=$matches[1]',
|
||||||
@@ -122,12 +89,11 @@ class Download {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-flush rewrite rules whenever the configured alias or the master
|
* Re-flush rewrite rules on theme version changes, using the same
|
||||||
* switch changes, using the same version-marker approach as Auth\Router.
|
* version-marker approach as Auth\Router.
|
||||||
*/
|
*/
|
||||||
public function maybe_flush_rewrite(): void {
|
public function maybe_flush_rewrite(): void {
|
||||||
$endpoint = $this->is_enabled() ? $this->config()['endpoint'] : 'off';
|
$version = AKSRT_VERSION . ':download-endpoint:' . self::ENDPOINT;
|
||||||
$version = AKSRT_VERSION . ':download-endpoint:' . $endpoint;
|
|
||||||
if ((string) get_option('aksrt_dl_rewrite_version') !== $version) {
|
if ((string) get_option('aksrt_dl_rewrite_version') !== $version) {
|
||||||
flush_rewrite_rules(false);
|
flush_rewrite_rules(false);
|
||||||
update_option('aksrt_dl_rewrite_version', $version);
|
update_option('aksrt_dl_rewrite_version', $version);
|
||||||
@@ -140,10 +106,6 @@ class Download {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function register_routes(): void {
|
public function register_routes(): void {
|
||||||
if (!$this->is_enabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
register_rest_route('aksrt/v1', '/download/request', [
|
register_rest_route('aksrt/v1', '/download/request', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [$this, 'rest_request'],
|
'callback' => [$this, 'rest_request'],
|
||||||
@@ -257,11 +219,6 @@ class Download {
|
|||||||
$this->fail_410();
|
$this->fail_410();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Master switch off: the token system is retired, links are dead ends.
|
|
||||||
if (!$this->is_enabled()) {
|
|
||||||
$this->fail_410();
|
|
||||||
}
|
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
||||||
if (!$table) $this->fail_410();
|
if (!$table) $this->fail_410();
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ if ($pay_type !== 'download' || empty($downloads)) {
|
|||||||
$is_resource_context = function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id);
|
$is_resource_context = function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id);
|
||||||
$context_variant = $is_resource_context ? 'resource' : 'content';
|
$context_variant = $is_resource_context ? 'resource' : 'content';
|
||||||
|
|
||||||
// 下载总开关关闭 = 直连模式:下载按钮直接输出原始地址,不走本站中转链接。
|
|
||||||
$direct_download = class_exists('\AKSRT\Content\Download')
|
|
||||||
&& !\AKSRT\Content\Download::get_instance()->is_enabled();
|
|
||||||
$subject_label = trim((string) ($meta['subject_label'] ?? ''));
|
$subject_label = trim((string) ($meta['subject_label'] ?? ''));
|
||||||
if ($subject_label === '') {
|
if ($subject_label === '') {
|
||||||
$subject_label = $is_resource_context ? __('资源', 'aksrt') : __('内容', 'aksrt');
|
$subject_label = $is_resource_context ? __('资源', 'aksrt') : __('内容', 'aksrt');
|
||||||
@@ -60,9 +57,7 @@ $pay_modo = is_array($priced)
|
|||||||
$user_amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 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;
|
$cost = function_exists('aksrt_resource_download_cost') ? (int) aksrt_resource_download_cost($post_id) : 0;
|
||||||
|
|
||||||
// 直连模式下没有“获取链接”请求,验证码容器不会被打开,属死 UI 不渲染。
|
$captcha_active = function_exists('aksrt_is_captcha_enabled_for')
|
||||||
$captcha_active = !$direct_download
|
|
||||||
&& function_exists('aksrt_is_captcha_enabled_for')
|
|
||||||
&& aksrt_is_captcha_enabled_for('download');
|
&& 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));
|
$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)));
|
$login_url = (string) ($gate_data['login_url'] ?? wp_login_url(get_permalink($post_id)));
|
||||||
@@ -350,10 +345,6 @@ if ($is_need_purchase && $user_amount > 0) {
|
|||||||
}
|
}
|
||||||
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
|
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
|
||||||
$url = isset($row['url']) ? (string) $row['url'] : '';
|
$url = isset($row['url']) ? (string) $row['url'] : '';
|
||||||
if ($direct_download && $url === '') {
|
|
||||||
// 直连模式没有可用的原始地址,空源不渲染。
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
|
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
|
||||||
$has_extract = $extract_code !== '';
|
$has_extract = $extract_code !== '';
|
||||||
|
|
||||||
@@ -371,15 +362,6 @@ if ($is_need_purchase && $user_amount > 0) {
|
|||||||
$btn_action = '';
|
$btn_action = '';
|
||||||
$btn_target = '';
|
$btn_target = '';
|
||||||
$btn_rel = '';
|
$btn_rel = '';
|
||||||
} elseif ($direct_download) {
|
|
||||||
$btn_tag = 'a';
|
|
||||||
$btn_href = $url;
|
|
||||||
$btn_text = __('立即下载', 'aksrt');
|
|
||||||
$btn_icon = 'fa-download';
|
|
||||||
$btn_modifier = $has_paid_access ? ' resource-download-box__btn--owned' : '';
|
|
||||||
$btn_action = '';
|
|
||||||
$btn_target = ' target="_blank"';
|
|
||||||
$btn_rel = ' rel="nofollow noopener"';
|
|
||||||
} else {
|
} else {
|
||||||
$btn_tag = 'button';
|
$btn_tag = 'button';
|
||||||
$btn_href = '';
|
$btn_href = '';
|
||||||
@@ -406,12 +388,6 @@ if ($is_need_purchase && $user_amount > 0) {
|
|||||||
<?php echo esc_html($service['name']); ?>
|
<?php echo esc_html($service['name']); ?>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php if ($direct_download && $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; ?>
|
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -8,13 +8,19 @@ const test = require('node:test');
|
|||||||
const root = path.resolve(__dirname, '..', '..');
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||||
|
|
||||||
test('Download.php derives endpoint, ttl and max uses from configuration', () => {
|
test('download relay configuration is pinned to built-in constants', () => {
|
||||||
const dl = read('src/Content/Download.php');
|
const dl = read('src/Content/Download.php');
|
||||||
|
|
||||||
// Dead hard-coded values are gone.
|
// The admin settings page is gone; the option must never be read again.
|
||||||
assert.doesNotMatch(dl, /const ENDPOINT/);
|
assert.doesNotMatch(dl, /aksrt_download_config/);
|
||||||
assert.doesNotMatch(dl, /const TOKEN_TTL/);
|
|
||||||
assert.doesNotMatch(dl, /aksrt-dl/);
|
// 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.
|
// Rewrite regex and URL building share the configured alias.
|
||||||
assert.match(dl, /preg_quote\(\$this->config\(\)\['endpoint'\], '\/'\)/);
|
assert.match(dl, /preg_quote\(\$this->config\(\)\['endpoint'\], '\/'\)/);
|
||||||
@@ -23,11 +29,34 @@ test('Download.php derives endpoint, ttl and max uses from configuration', () =>
|
|||||||
assert.match(dl, /flush_rewrite_rules\(false\)/);
|
assert.match(dl, /flush_rewrite_rules\(false\)/);
|
||||||
assert.match(dl, /aksrt_dl_rewrite_version/);
|
assert.match(dl, /aksrt_dl_rewrite_version/);
|
||||||
|
|
||||||
// TTL and per-token max uses come from the settings section.
|
// TTL and per-token max uses come from the fixed configuration.
|
||||||
assert.match(dl, /'expires_in'\s+=> \$config\['ttl'\]/);
|
assert.match(dl, /'expires_in'\s+=> \$config\['ttl'\]/);
|
||||||
assert.match(dl, /'max_uses'\s+=> \(int\) \$config\['max_uses'\]/);
|
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', () => {
|
test('usage is counted only after full delivery, never for probes', () => {
|
||||||
const dl = read('src/Content/Download.php');
|
const dl = read('src/Content/Download.php');
|
||||||
|
|
||||||
@@ -89,31 +118,3 @@ test('token generation retries on primary-key collisions', () => {
|
|||||||
assert.match(dl, /PRIMARY KEY\(token\) already guarantees uniqueness/);
|
assert.match(dl, /PRIMARY KEY\(token\) already guarantees uniqueness/);
|
||||||
assert.match(dl, /for \(\$attempt = 0; \$attempt < 3; \$attempt\+\+\)/);
|
assert.match(dl, /for \(\$attempt = 0; \$attempt < 3; \$attempt\+\+\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('master switch off puts the token system to sleep and renders direct links', () => {
|
|
||||||
const dl = read('src/Content/Download.php');
|
|
||||||
const box = read('templates/parts/resource-download-box.php');
|
|
||||||
const pay = read('inc/helpers/aksrt-pay.php');
|
|
||||||
|
|
||||||
// The switch is a public runtime check; missing key means enabled.
|
|
||||||
assert.match(dl, /public function is_enabled\(\): bool/);
|
|
||||||
assert.match(dl, /!isset\(\$cfg\['enabled'\]\) \|\| !empty\(\$cfg\['enabled'\]\)/);
|
|
||||||
|
|
||||||
// Rewrite, REST routes and token serving all bail out when disabled.
|
|
||||||
assert.match(dl, /if \(!\$this->is_enabled\(\)\) \{\s*return;\s*\}/);
|
|
||||||
assert.match(dl, /Master switch off: the token system is retired/);
|
|
||||||
|
|
||||||
// The unused policy endpoint stays removed.
|
|
||||||
assert.doesNotMatch(dl, /download\/policy/);
|
|
||||||
assert.doesNotMatch(dl, /rest_policy/);
|
|
||||||
|
|
||||||
// Direct mode renders anchors to the raw source URL instead of token buttons.
|
|
||||||
assert.match(box, /\$direct_download = class_exists\('\\AKSRT\\Content\\Download'\)/);
|
|
||||||
assert.match(box, /elseif \(\$direct_download\) \{/);
|
|
||||||
assert.match(box, /href="<\?php echo esc_url\(\$btn_href\); \?>/);
|
|
||||||
assert.match(box, /data-clipboard-text="<\?php echo esc_attr\(\$extract_code\); \?>/);
|
|
||||||
assert.match(pay, /if \(\$direct_download\) \{\s*\$buttons \.= '<a class="mr10 but /);
|
|
||||||
|
|
||||||
// Dead standalone-page helper stays removed.
|
|
||||||
assert.doesNotMatch(pay, /aksrt_is_download_alone_page_enabled/);
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user