Files
wordpress-theme-aksrt/src/Attachment/PendingUploads.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

190 lines
7.1 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
/**
* 前台「选图即上传」的附件生命周期(对齐大厂:先传后认领,未认领兜底清理)。
*
* - 上传成功即打「待认领」标记(时间戳),返回 id + url;
* - 用户点 × 移除:DELETE /upload/pending/{id},仅能删自己的、仍待认领的附件;
* - 表单提交成功(评论 / 工单 / 举报)后按引用的 URL 认领,去掉标记;
* - 定时任务删除超过 TTL 仍未认领的附件。
*
* @package Jio
*/
namespace Jio\Attachment;
defined('ABSPATH') || exit;
final class PendingUploads {
public const META = '_jio_pending_upload';
public const CRON_HOOK = 'jio_pending_upload_cleanup';
private const TTL = DAY_IN_SECONDS;
private const BATCH_SIZE = 100;
/** 单次认领最多解析的 URL 数,防止超长文本拖慢请求。 */
private const CLAIM_MAX = 20;
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 {
add_action('rest_api_init', [$this, 'register_routes']);
add_action(self::CRON_HOOK, [$this, 'cleanup']);
add_action('init', [$this, 'maybe_schedule'], 20);
// 评论走 WP 原生管线(REST 提交 / 编辑均触发),在钩子上认领,避免侵入评论控制器。
add_action('comment_post', [$this, 'claim_comment'], 10, 3);
add_action('edit_comment', [$this, 'claim_edited_comment'], 10, 2);
}
public function maybe_schedule(): void {
if (!wp_next_scheduled(self::CRON_HOOK)) {
wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', self::CRON_HOOK);
}
}
public function register_routes(): void {
register_rest_route('jio/v1', '/upload/pending/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'rest_discard'],
'permission_callback' => static function (): bool {
return is_user_logged_in();
},
'args' => [
'id' => ['sanitize_callback' => 'absint'],
],
]);
}
/** 上传成功后调用:标记为待认领。 */
public static function mark(int $attachment_id): void {
if ($attachment_id > 0) {
update_post_meta($attachment_id, self::META, time());
}
}
public function rest_discard(\WP_REST_Request $request): \WP_REST_Response {
$id = absint($request->get_param('id'));
if (!self::discard($id, get_current_user_id())) {
return new \WP_REST_Response([
'success' => false,
'message' => __('图片不存在或已被使用', 'jio'),
], 404);
}
return new \WP_REST_Response(['success' => true]);
}
/** 删除用户自己仍待认领的附件;已被引用的附件绝不删除。 */
public static function discard(int $attachment_id, int $user_id): bool {
if (!self::is_owned_pending($attachment_id, $user_id, true)) {
return false;
}
return (bool) wp_delete_attachment($attachment_id, true);
}
/** @param list<int> $ids */
public static function claim_ids(array $ids, int $user_id): void {
if ($user_id <= 0) {
return;
}
foreach (array_unique(array_map('absint', $ids)) as $id) {
if (self::is_owned_pending($id, $user_id)) {
delete_post_meta($id, self::META);
}
}
}
/**
* 按 URL 列表认领;已保存文章可传 null 认领全部引用。
*
* @param list<string> $urls
*/
public static function claim_urls(array $urls, int $user_id, ?int $limit = self::CLAIM_MAX): void {
if ($user_id <= 0) {
return;
}
$urls = array_values(array_unique($urls));
if ($limit !== null) {
$urls = array_slice($urls, 0, max(0, $limit));
}
$ids = [];
foreach ($urls as $url) {
if (is_string($url) && $url !== '') {
$ids[] = (int) attachment_url_to_postid($url);
}
}
self::claim_ids($ids, $user_id);
}
/** 从正文中提取本站上传目录下的 URL 并认领(评论 [img=…]、举报举证、已保存文章)。 */
public static function claim_text(string $text, int $user_id, ?int $limit = self::CLAIM_MAX): void {
if ($user_id <= 0 || $text === '') {
return;
}
$base = (string) (wp_get_upload_dir()['baseurl'] ?? '');
if ($base === '' || strpos($text, wp_parse_url($base, PHP_URL_PATH) ?: $base) === false) {
return;
}
if (!preg_match_all('#https?://[^\s\[\]"\'<>]+#i', $text, $matches)) {
return;
}
$urls = array_filter($matches[0], static function (string $url) use ($base): bool {
return strpos(set_url_scheme($url), set_url_scheme($base)) === 0;
});
self::claim_urls(array_values($urls), $user_id, $limit);
}
/** @param int|string $approved */
public function claim_comment($comment_id, $approved, $commentdata): void {
$data = is_array($commentdata) ? $commentdata : [];
self::claim_text((string) ($data['comment_content'] ?? ''), (int) ($data['user_id'] ?? 0));
}
public function claim_edited_comment($comment_id, $data = []): void {
$comment = get_comment((int) $comment_id);
if ($comment) {
self::claim_text((string) $comment->comment_content, (int) $comment->user_id);
}
}
/** 定时清理:超过 TTL 仍未认领的附件连同文件一起删除。 */
public function cleanup(): void {
global $wpdb;
$ids = $wpdb->get_col($wpdb->prepare(
"SELECT p.ID FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = %s
WHERE p.post_type = 'attachment' AND p.post_parent = 0 AND CAST(m.meta_value AS UNSIGNED) < %d
ORDER BY p.ID ASC LIMIT %d",
self::META,
time() - self::TTL,
self::BATCH_SIZE
));
foreach ((array) $ids as $id) {
$id = (int) $id;
$post = get_post($id);
$pending_at = (int) get_post_meta($id, self::META, true);
if ($post && $post->post_type === 'attachment' && (int) $post->post_parent === 0
&& $pending_at > 0 && $pending_at < time() - self::TTL
) {
wp_delete_attachment($id, true);
}
}
}
private static function is_owned_pending(int $attachment_id, int $user_id, bool $unattached = false): bool {
if ($attachment_id <= 0 || $user_id <= 0) {
return false;
}
$post = get_post($attachment_id);
return $post
&& $post->post_type === 'attachment'
&& (int) $post->post_author === $user_id
&& (!$unattached || (int) $post->post_parent === 0)
&& get_post_meta($attachment_id, self::META, true) !== '';
}
}