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>
190 lines
7.1 KiB
PHP
190 lines
7.1 KiB
PHP
<?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) !== '';
|
||
}
|
||
}
|