Files
wordpress-theme-aksrt/inc/classes/core/class-aksrt-front-submit.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +08:00

392 lines
13 KiB
PHP

<?php
/**
* AKSRT Front Submit — 前台投稿系统骨架
*
* 提供前台投稿、草稿保存、编辑回退能力。
* 当前为骨架实现,后续 Phase 补充完整投稿产品线。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Front_Submit {
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('wp_ajax_aksrt_submit_post', [$this, 'ajax_submit_post']);
add_action('wp_ajax_aksrt_save_draft', [$this, 'ajax_save_draft']);
add_action('wp_ajax_aksrt_update_post', [$this, 'ajax_update_post']);
add_action('wp_ajax_aksrt_delete_draft', [$this, 'ajax_delete_draft']);
}
/**
* 检查用户是否可以投稿
*
* @param int|null $user_id User ID (null = current user).
* @return bool
*/
public function can_submit(?int $user_id = null): bool {
if ($user_id === null) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) return false;
// Read submission settings
$submit_settings = [];
if (class_exists('AKSRT_Settings')) {
$submit_settings = AKSRT_Settings::get_value('post_list.submit', []);
}
// Check if front submission is enabled (fix: was checking !empty && empty which is always false)
if (isset($submit_settings['submit_enabled']) && !$submit_settings['submit_enabled']) {
return false;
}
// Role whitelist: if configured, user must have one of the allowed roles
$allowed_roles = $submit_settings['submit_roles'] ?? [];
if (!empty($allowed_roles)) {
$user = get_userdata($user_id);
if (!$user) return false;
$user_roles = $user->roles;
if (empty(array_intersect($user_roles, (array) $allowed_roles))) {
// Also allow administrators
if (!in_array('administrator', $user_roles, true)) {
return false;
}
}
}
// Credits threshold: if configured, user must have enough credits
$min_credits = (int) ($submit_settings['submit_min_credits'] ?? 0);
if ($min_credits > 0 && class_exists('AKSRT_Credits')) {
$balance = AKSRT_Credits::balance($user_id);
if ($balance < $min_credits) {
return false;
}
}
// Default: allow users with edit_posts capability (or any logged-in user if submission is enabled)
return user_can($user_id, 'edit_posts') || !empty($submit_settings['submit_enabled']);
}
/**
* 提交文章
*
* @param array $data 投稿数据
* @return array{success: bool, post_id?: int, message: string}
*/
public function submit(array $data): array {
$user_id = get_current_user_id();
if (!$this->can_submit($user_id)) {
return ['success' => false, 'message' => __('没有投稿权限', 'aksrt')];
}
$title = sanitize_text_field((string) ($data['title'] ?? ''));
$content = wp_kses_post((string) ($data['content'] ?? ''));
$category = (int) ($data['category'] ?? 0);
$tags = sanitize_text_field((string) ($data['tags'] ?? ''));
$status = sanitize_key((string) ($data['status'] ?? 'pending'));
if ($title === '') {
return ['success' => false, 'message' => __('标题不能为空', 'aksrt')];
}
$valid_statuses = ['pending', 'draft', 'publish'];
if (!in_array($status, $valid_statuses, true)) {
$status = 'pending';
}
// Determine review policy: if review is not required and user can publish, auto-publish
if ($status === 'pending') {
$submit_settings = [];
if (class_exists('AKSRT_Settings')) {
$submit_settings = AKSRT_Settings::get_value('post_list.submit', []);
}
$review_required = $submit_settings['review_required'] ?? true;
$skip_moderation = false;
if (class_exists('AKSRT_Settings')) {
$all = AKSRT_Settings::get_all();
$skip_moderation = !empty($all['skip_submit_moderation']);
}
if (!$review_required || $skip_moderation) {
// Auto-publish if user has publish_posts capability
if (user_can($user_id, 'publish_posts')) {
$status = 'publish';
}
}
}
$post_data = [
'post_title' => $title,
'post_content' => $content,
'post_status' => $status,
'post_author' => $user_id,
'post_type' => 'post',
];
$post_id = wp_insert_post($post_data, true);
if (is_wp_error($post_id)) {
return ['success' => false, 'message' => $post_id->get_error_message()];
}
// 设置分类
if ($category > 0) {
wp_set_post_categories($post_id, [$category]);
}
// 设置标签
if ($tags !== '') {
$tag_names = array_map('trim', explode(',', $tags));
wp_set_post_tags($post_id, $tag_names);
}
// 设置封面图
$thumbnail_url = sanitize_url((string) ($data['thumbnail'] ?? ''));
if ($thumbnail_url !== '') {
$this->set_thumbnail_from_url($post_id, $thumbnail_url);
}
// 设置专题 (topics taxonomy)
$this->apply_topics_and_filters($post_id, $data);
return [
'success' => true,
'post_id' => $post_id,
'message' => $status === 'draft' ? __('草稿已保存', 'aksrt') : __('投稿已提交,等待审核', 'aksrt'),
];
}
/**
* 更新文章
*/
public function update(int $post_id, array $data): array {
$user_id = get_current_user_id();
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
$post = get_post($post_id);
if (!$post || (int) $post->post_author !== $user_id) {
return ['success' => false, 'message' => __('没有编辑权限', 'aksrt')];
}
$update_data = ['ID' => $post_id];
if (isset($data['title'])) {
$update_data['post_title'] = sanitize_text_field($data['title']);
}
if (isset($data['content'])) {
$update_data['post_content'] = wp_kses_post($data['content']);
}
if (isset($data['status'])) {
$update_data['post_status'] = sanitize_key($data['status']);
}
$result = wp_update_post($update_data, true);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message()];
}
// Update category
$category = (int) ($data['category'] ?? 0);
if ($category > 0) {
wp_set_post_categories($post_id, [$category]);
}
// Update tags
if (isset($data['tags'])) {
$tags = sanitize_text_field((string) $data['tags']);
if ($tags !== '') {
$tag_names = array_map('trim', explode(',', $tags));
wp_set_post_tags($post_id, $tag_names);
} else {
wp_set_post_tags($post_id, []);
}
}
// Update thumbnail
if (isset($data['thumbnail'])) {
$thumbnail_url = sanitize_url((string) $data['thumbnail']);
if ($thumbnail_url !== '') {
$this->set_thumbnail_from_url($post_id, $thumbnail_url);
}
}
// Update topics + custom filter meta
$this->apply_topics_and_filters($post_id, $data);
return ['success' => true, 'post_id' => $post_id, 'message' => __('已更新', 'aksrt')];
}
/**
* 从 URL 设置封面图
*
* Downloads the image from the given URL, sideloads it into the media library,
* and sets it as the post's featured image.
*
* @param int $post_id Post ID.
* @param string $url Image URL.
*/
private function set_thumbnail_from_url(int $post_id, string $url): void {
if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
return;
}
// Require media handling functions
if (!function_exists('media_sideload_image')) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
}
// Check if URL is an image
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION));
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
if ($ext !== '' && !in_array($ext, $allowed_exts, true)) {
return;
}
// Sideload the image
$attachment_id = media_sideload_image($url, $post_id, '', 'id');
if (!is_wp_error($attachment_id)) {
set_post_thumbnail($post_id, $attachment_id);
} elseif (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf('[AKSRT Front Submit] Failed to set thumbnail for post %d: %s — %s', $post_id, $url, $attachment_id->get_error_message()));
}
}
/**
* AJAX 提交投稿
*/
public function ajax_submit_post(): void {
check_ajax_referer('aksrt_submit', '_nonce');
$data = $_POST;
$result = $this->submit($data);
if ($result['success']) {
wp_send_json_success($result);
}
wp_send_json_error($result);
}
/**
* AJAX 保存草稿
*/
public function ajax_save_draft(): void {
check_ajax_referer('aksrt_submit', '_nonce');
$_POST['status'] = 'draft';
$data = $_POST;
$result = $this->submit($data);
if ($result['success']) {
wp_send_json_success($result);
}
wp_send_json_error($result);
}
/**
* AJAX 更新文章
*/
public function ajax_update_post(): void {
check_ajax_referer('aksrt_submit', '_nonce');
$post_id = (int) ($_POST['post_id'] ?? 0);
if ($post_id <= 0) {
wp_send_json_error(['message' => __('文章 ID 无效', 'aksrt')]);
}
$result = $this->update($post_id, $_POST);
if ($result['success']) {
wp_send_json_success($result);
}
wp_send_json_error($result);
}
/**
* AJAX handler: delete draft
*/
public function ajax_delete_draft(): void {
check_ajax_referer('aksrt_submit', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
// Fix: JS sends 'draft_id', not 'post_id'
$post_id = (int) (($_POST['draft_id'] ?? $_POST['post_id']) ?? 0);
if ($post_id <= 0) {
wp_send_json_error(['message' => __('草稿 ID 无效', 'aksrt')]);
}
$post = get_post($post_id);
if (!$post || (int) $post->post_author !== $user_id) {
wp_send_json_error(['message' => __('没有删除权限', 'aksrt')]);
}
if ($post->post_status !== 'draft') {
wp_send_json_error(['message' => __('只能删除草稿', 'aksrt')]);
}
$result = wp_delete_post($post_id, true);
if ($result) {
wp_send_json_success(['message' => __('草稿已删除', 'aksrt')]);
}
wp_send_json_error(['message' => __('删除失败', 'aksrt')]);
}
/**
* Apply topics taxonomy terms and custom filter meta values to a post.
*
* Reads 'topics' array and 'cf_{dim_key}' scalar values from $data.
* Custom filter keys are validated against the configured dimension definitions.
*
* @param int $post_id Post ID.
* @param array $data Submission data ($_POST).
*/
private function apply_topics_and_filters(int $post_id, array $data): void {
// Topics taxonomy
if (isset($data['topics']) && is_array($data['topics']) && taxonomy_exists('topics')) {
$topic_ids = array_map('absint', $data['topics']);
$topic_ids = array_filter($topic_ids);
wp_set_object_terms($post_id, array_values($topic_ids), 'topics');
}
// Custom filter meta values (_aksrt_cf_{dim_key})
foreach ($data as $key => $val) {
if (strpos($key, 'cf_') !== 0) {
continue;
}
$dim_key = sanitize_key(substr($key, 3));
if ($dim_key === '') {
continue;
}
$cf_val = sanitize_key((string) $val);
$meta_key = '_aksrt_cf_' . $dim_key;
if ($cf_val !== '') {
update_post_meta($post_id, $meta_key, $cf_val);
} else {
delete_post_meta($post_id, $meta_key);
}
}
}
}