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.
392 lines
13 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|
|
}
|