\WP_REST_Server::READABLE, 'callback' => [$this, 'rest_bootstrap'], 'permission_callback' => [$this, 'can_use_studio'], 'args' => [ 'post_id' => ['default' => 0, 'sanitize_callback' => 'absint'], ], ]); register_rest_route(self::NS, '/creator/save', [ 'methods' => 'POST', 'callback' => [$this, 'rest_save'], 'permission_callback' => [$this, 'can_use_studio'], ]); register_rest_route(self::NS, '/creator/submit', [ 'methods' => 'POST', 'callback' => [$this, 'rest_submit'], 'permission_callback' => [$this, 'can_use_studio'], ]); register_rest_route(self::NS, '/creator/preview', [ 'methods' => 'POST', 'callback' => [$this, 'rest_preview'], 'permission_callback' => [$this, 'can_use_studio'], ]); register_rest_route(self::NS, '/creator/private-upload', [ 'methods' => 'POST', 'callback' => [$this, 'rest_private_upload'], 'permission_callback' => [$this, 'can_upload_private'], ]); register_rest_route(self::NS, '/creator/private-uploads', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'rest_list_private_uploads'], 'permission_callback' => [$this, 'can_upload_private'], ]); } public function can_use_studio($request = null): bool { $user_id = get_current_user_id(); if ($user_id <= 0) { return false; } $front = FrontSubmit::get_instance(); if (empty($front->settings()['enabled'])) { return false; } $post_id = 0; if ($request instanceof \WP_REST_Request) { $post_id = absint($request->get_param('post_id')); } elseif (isset($_REQUEST['post_id'])) { $post_id = absint(wp_unslash($_REQUEST['post_id'])); } if ($post_id > 0) { return $front->can_edit_post($post_id, $user_id); } return $front->can_submit($user_id); } public function can_upload_private(): bool { $user_id = get_current_user_id(); if ($user_id <= 0) { return false; } return FrontSubmit::get_instance()->user_can_cap('new_post_pay_download', $user_id); } /** * 编辑上下文:作者打开创作页时的一切初始数据。 */ public function rest_bootstrap(\WP_REST_Request $request): \WP_REST_Response { $post_id = absint($request->get_param('post_id')); $user_id = get_current_user_id(); return new \WP_REST_Response($this->bootstrap_payload($post_id, $user_id), 200); } /** * @return array */ public function bootstrap_payload(int $post_id, int $user_id): array { $front = FrontSubmit::get_instance(); $settings = $front->settings(); $source = $post_id > 0 ? get_post($post_id) : null; $version = null; $version_id = 0; if ($source instanceof \WP_Post && SubmissionVersion::requires_version($post_id)) { $version = SubmissionVersion::get_working($post_id, $user_id); $version_id = $version instanceof \WP_Post ? (int) $version->ID : 0; } $editable = $version instanceof \WP_Post ? $version : ($source instanceof \WP_Post ? $source : null); $pay_meta = $editable instanceof \WP_Post && function_exists('jio_get_pay_meta') ? jio_get_pay_meta($editable->ID) : DomainModel::default_pay_meta(); $snapshot = $version_id > 0 ? SubmissionVersion::snapshot($version_id) : []; $post_meta = $source instanceof \WP_Post ? (array) get_post_meta($source->ID, DomainModel::POST_META_KEY, true) : []; $terms = $this->term_payload($settings, $source, $snapshot); return [ 'sourceId' => $post_id, 'versionId' => $version_id, 'versionRev' => $version_id > 0 ? SubmissionVersion::revision($version_id) : 0, 'status' => $version_id > 0 ? SubmissionVersion::status($version_id) : ($source->post_status ?? ''), 'reviewNote' => $version_id > 0 ? (string) get_post_meta($version_id, SubmissionVersion::META_NOTE, true) : '', 'title' => $editable instanceof \WP_Post ? $editable->post_title : '', 'excerpt' => $editable instanceof \WP_Post ? $editable->post_excerpt : '', 'content' => $editable instanceof \WP_Post ? $editable->post_content : '', 'permalink' => $source instanceof \WP_Post ? get_permalink($source) : '', 'isPublished'=> $source instanceof \WP_Post && $source->post_status === 'publish', 'categories' => $terms['categories'], 'tags' => $terms['tags'], 'custom' => $terms['custom'], 'customDefs' => $terms['customDefs'], 'featured' => $this->creator_featured_payload($source, $post_meta), 'pay' => $this->pay_payload((array) $pay_meta, $snapshot, $settings), 'capabilities' => $this->capability_payload($front, $user_id, $post_id), 'limits' => [ 'titleMin' => (int) $settings['title_min'], 'titleMax' => (int) $settings['title_max'], 'titlePlaceholder' => (string) $settings['title_placeholder'], 'contentPlaceholder' => (string) $settings['content_placeholder'], ], 'privateUploads' => $this->private_upload_rows($user_id, $post_id), 'privateStorage' => PrivateAssets::is_available(), 'media' => [ 'listUrl' => esc_url_raw(rest_url(self::NS . '/front-submit/media')), 'uploadUrl' => [ 'image' => esc_url_raw(rest_url(self::NS . '/front-submit/upload/image')), ], 'maxBytes' => FrontSubmit::upload_limit_bytes('image'), 'batch' => class_exists('\Jio\Attachment\UploadLimits') ? \Jio\Attachment\UploadLimits::image_batch_limit() : 6, ], 'rest' => [ 'root' => esc_url_raw(rest_url()), 'nonce' => wp_create_nonce('wp_rest'), ], 'urls' => [ 'submit' => function_exists('jio_get_new_post_url') ? jio_get_new_post_url() : '', 'newPost' => function_exists('jio_get_new_post_url') ? jio_get_new_post_url() : '', 'center' => function_exists('jio_get_user_center_url') ? jio_get_user_center_url('posts') : '', ], ]; } /** * 保存或自动保存。已发布文章写入版本记录;新文章走既有 persist()。 */ public function rest_save(\WP_REST_Request $request) { $user_id = get_current_user_id(); $post_id = absint($request->get_param('post_id')); $is_auto = (bool) $request->get_param('autosave'); $payload = [ 'title' => (string) $request->get_param('title'), 'content' => (string) $request->get_param('content'), 'categories' => (array) $request->get_param('categories'), 'tags' => (array) $request->get_param('tags'), 'custom' => (array) $request->get_param('custom'), 'featured' => (array) $request->get_param('featured'), 'schedule' => (array) $request->get_param('schedule'), 'pay' => (array) $request->get_param('pay'), ]; if ($post_id > 0 && SubmissionVersion::requires_version($post_id)) { $result = SubmissionVersion::get_instance()->save_working($post_id, $user_id, [ 'title' => $payload['title'] !== '' ? $payload['title'] : null, 'content' => $payload['content'], 'base_rev' => absint($request->get_param('rev')), 'snapshot' => $this->snapshot_from_payload($payload), ], false); if (is_wp_error($result)) { return $result; } $result['autosave'] = $is_auto; return new \WP_REST_Response($result, 200); } $front = FrontSubmit::get_instance(); $data = [ 'posts_id' => $post_id, 'post_title' => $payload['title'], 'post_content' => $payload['content'], 'category' => $payload['categories'], 'tags' => implode(',', array_map('strval', $payload['tags'])), 'featured_data'=> $payload['featured'], 'schedule' => $payload['schedule'], ]; foreach ($payload['custom'] as $key => $value) { $data['cf_' . sanitize_key((string) $key)] = $value; } if ($payload['pay'] !== []) { $data['pay_enabled'] = !empty($payload['pay']['enabled']) ? '1' : ''; $data['jio_pay'] = (array) ($payload['pay']['fields'] ?? []); } $result = $front->persist($data, 'posts_draft'); if (empty($result['success'])) { return new \WP_Error('jio_creator_save_failed', (string) ($result['message'] ?? __('保存失败', 'jio')), ['status' => 400]); } $result['autosave'] = $is_auto; $result['versionRev'] = max(1, (int) ($result['rev'] ?? 1)); return new \WP_REST_Response($result, 200); } /** * 提交审核 / 直接发布。 */ public function rest_submit(\WP_REST_Request $request) { $user_id = get_current_user_id(); $post_id = absint($request->get_param('post_id')); $front = FrontSubmit::get_instance(); // 与 admin-ajax 投稿入口保持同一道人机验证闸门(启用 front_submit 场景时)。 if (function_exists('jio_require_captcha')) { $captcha = jio_require_captcha('front_submit', [ 'jio_captcha_proof' => sanitize_text_field((string) $request->get_param('captcha_proof')), ]); if ($captcha !== true) { $message = is_wp_error($captcha) ? $captcha->get_error_message() : __('人机验证失败,请重试', 'jio'); return new \WP_Error('jio_captcha_failed', $message, ['status' => 400]); } } $payload = [ 'title' => (string) $request->get_param('title'), 'content' => (string) $request->get_param('content'), 'categories' => (array) $request->get_param('categories'), 'tags' => (array) $request->get_param('tags'), 'custom' => (array) $request->get_param('custom'), 'featured' => (array) $request->get_param('featured'), 'schedule' => (array) $request->get_param('schedule'), 'pay' => (array) $request->get_param('pay'), ]; if ($post_id > 0 && SubmissionVersion::requires_version($post_id)) { $invalid = $this->validate_submission($payload, $user_id); if ($invalid !== '') { return new \WP_Error('jio_creator_invalid', $invalid, ['status' => 400]); } $version = SubmissionVersion::get_instance()->save_working($post_id, $user_id, [ 'title' => $payload['title'], 'content' => $payload['content'], 'base_rev' => absint($request->get_param('rev')), 'snapshot' => $this->snapshot_from_payload($payload), ], true); if (is_wp_error($version)) { return $version; } // 具备免审发布权限且明确选择立即发布时,才直接应用到线上文章。 if ($request->get_param('publish_now') && $front->user_can_cap('new_post_audit_no', $user_id)) { $applied = SubmissionVersion::get_instance()->publish_working($post_id, $user_id); if (empty($applied['success'])) { return new \WP_Error('jio_creator_publish_failed', (string) ($applied['message'] ?? __('发布失败', 'jio')), ['status' => 400]); } return new \WP_REST_Response($applied, 200); } return new \WP_REST_Response($version, 200); } $data = [ 'posts_id' => $post_id, 'post_title' => $payload['title'], 'post_content' => $payload['content'], 'category' => $payload['categories'], 'tags' => implode(',', array_map('strval', $payload['tags'])), 'featured_data' => $payload['featured'], 'schedule' => $payload['schedule'], ]; foreach ($payload['custom'] as $key => $value) { $data['cf_' . sanitize_key((string) $key)] = $value; } if ($payload['pay'] !== []) { $data['pay_enabled'] = !empty($payload['pay']['enabled']) ? '1' : ''; $data['jio_pay'] = (array) ($payload['pay']['fields'] ?? []); } $result = $front->persist($data, 'posts_save'); if (empty($result['success'])) { return new \WP_Error('jio_creator_submit_failed', (string) ($result['message'] ?? __('提交失败', 'jio')), ['status' => 400]); } $this->claim_private_assets($payload, (int) ($result['post_id'] ?? 0), $user_id); return new \WP_REST_Response($result, 200); } /** * 预览链接:已发布文章的改稿版本使用一次性签名地址,不公开版本内容。 */ public function rest_preview(\WP_REST_Request $request) { $user_id = get_current_user_id(); $post_id = absint($request->get_param('post_id')); $version_id = absint($request->get_param('version_id')); if ($version_id > 0) { $version = get_post($version_id); if (!$version instanceof \WP_Post || $version->post_type !== SubmissionVersion::POST_TYPE) { return new \WP_Error('jio_review_preview_missing', __('改稿不存在。', 'jio'), ['status' => 404]); } if ((int) $version->post_author !== $user_id && !SubmissionVersion::can_review($user_id)) { return new \WP_Error('jio_review_preview_forbidden', __('没有预览权限。', 'jio'), ['status' => 403]); } $token = PreviewToken::issue($version_id, $user_id); $source_id = SubmissionVersion::source_id($version_id); $source = $source_id > 0 ? get_post($source_id) : null; $base = $source instanceof \WP_Post ? (string) get_permalink($source) : home_url('/'); return new \WP_REST_Response([ 'url' => add_query_arg('jio_studio_preview', $token, $base), ], 200); } if ($post_id <= 0) { return new \WP_Error('jio_review_preview_missing', __('尚未保存文章。', 'jio'), ['status' => 400]); } $post = get_post($post_id); if (!$post instanceof \WP_Post) { return new \WP_Error('jio_review_preview_missing', __('文章不存在。', 'jio'), ['status' => 404]); } return new \WP_REST_Response([ 'url' => (string) get_preview_post_link($post), ], 200); } /** * 私有资料上传(付费附件)。 */ public function rest_private_upload(\WP_REST_Request $request) { $file = $request->get_file_params()['file'] ?? null; if (!is_array($file)) { return new \WP_Error('jio_private_upload_failed', __('请选择要上传的资料文件。', 'jio'), ['status' => 400]); } $post_id = absint($request->get_param('post_id')); $result = PrivateAssets::get_instance()->store_upload($file, get_current_user_id(), $post_id); if (is_wp_error($result)) { return $result; } return new \WP_REST_Response($result, 201); } public function rest_list_private_uploads(\WP_REST_Request $request): \WP_REST_Response { return new \WP_REST_Response([ 'items' => $this->private_upload_rows(get_current_user_id(), absint($request->get_param('post_id'))), ], 200); } /** * @return list> */ private function private_upload_rows(int $user_id, int $post_id): array { global $wpdb; $table = DBMigrator::table('private_asset'); if ($table === '' || $user_id <= 0) { return []; } $rows = $wpdb->get_results($wpdb->prepare( "SELECT asset_key, post_id, file_name, mime, size_bytes, created_at FROM {$table} WHERE owner_id = %d AND (post_id = %d OR post_id = 0) ORDER BY id DESC LIMIT 50", $user_id, $post_id ), ARRAY_A); $items = []; foreach (is_array($rows) ? $rows : [] as $row) { $items[] = [ 'assetKey' => (string) $row['asset_key'], 'postId' => (int) $row['post_id'], 'fileName' => (string) $row['file_name'], 'mime' => (string) $row['mime'], 'sizeBytes' => (int) $row['size_bytes'], 'sizeLabel' => size_format((int) $row['size_bytes']), ]; } return $items; } /** * 提交前的必填校验,与 FrontSubmitSave 保持同一口径。 * * @param array $payload */ private function validate_submission(array $payload, int $user_id): string { $settings = FrontSubmit::get_instance()->settings(); $title = trim(wp_strip_all_tags((string) ($payload['title'] ?? ''))); $content = trim(wp_strip_all_tags((string) ($payload['content'] ?? ''))); if ($title === '') { return __('请填写文章标题', 'jio'); } $length = function_exists('mb_strlen') ? mb_strlen($title) : strlen($title); if ($length < (int) $settings['title_min']) { return __('标题太短!', 'jio'); } if ($length > (int) $settings['title_max']) { return sprintf(__('标题太长了,不能超过%d个字', 'jio'), (int) $settings['title_max']); } if ((function_exists('mb_strlen') ? mb_strlen($content) : strlen($content)) < 10) { return __('文章内容过少', 'jio'); } if (empty($payload['categories'])) { return __('请选择文章分类', 'jio'); } return ''; } /** * 组合提交时的版本快照(分类、标签、封面、付费)。 * * @param array $payload * @return array */ private function snapshot_from_payload(array $payload): array { $downloads = []; foreach ((array) ($payload['pay']['fields']['downloads'] ?? []) as $row) { if (!is_array($row)) { continue; } $key = PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? '')); if ($key !== '') { $asset = PrivateAssets::get($key); if ($asset === null || $asset['owner_id'] !== get_current_user_id()) { continue; } $downloads[] = [ 'type' => 'protected', 'asset_key' => $key, 'file_name' => $asset['file_name'], 'size_bytes' => $asset['size_bytes'], 'label' => (string) ($row['label'] ?? ''), ]; continue; } $url = esc_url_raw((string) ($row['url'] ?? '')); if ($url !== '') { $downloads[] = [ 'type' => 'direct', 'url' => $url, 'label' => (string) ($row['label'] ?? ''), 'extract_code' => (string) ($row['extract_code'] ?? ''), ]; } } return [ 'categories' => array_values(array_filter(array_map('absint', (array) $payload['categories']))), 'tags' => array_values(array_filter(array_map('sanitize_text_field', (array) $payload['tags']))), 'custom' => is_array($payload['custom']) ? $payload['custom'] : [], 'featured' => is_array($payload['featured']) ? $payload['featured'] : [], 'pay' => [ 'enabled' => !empty($payload['pay']['enabled']), 'fields' => array_merge( (array) ($payload['pay']['fields'] ?? []), ['downloads' => $downloads] ), ], ]; } private function claim_private_assets(array $payload, int $post_id, int $user_id): void { if ($post_id <= 0) { return; } $keys = []; foreach ((array) ($payload['pay']['fields']['downloads'] ?? []) as $row) { if (is_array($row)) { $key = PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? '')); if ($key !== '') { $keys[] = $key; } } } PrivateAssets::claim_many($keys, $post_id, $user_id); } /** * @param array $settings * @return array */ private function term_payload(array $settings, ?\WP_Post $source, array $snapshot): array { $cat_args = [ 'taxonomy' => 'category', 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC', ]; if (!empty($settings['allowed_cat_ids'])) { $cat_args['include'] = $settings['allowed_cat_ids']; } else { $cat_args['parent'] = 0; } $terms = get_terms($cat_args); $categories = []; foreach (is_array($terms) ? $terms : [] as $term) { $categories[] = ['id' => (int) $term->term_id, 'name' => (string) $term->name]; } $current = $source instanceof \WP_Post && $snapshot === [] ? array_map('intval', wp_get_post_categories($source->ID)) : array_map('intval', (array) ($snapshot['categories'] ?? [])); $tags = $source instanceof \WP_Post && $snapshot === [] ? wp_get_post_tags($source->ID, ['fields' => 'names']) : array_map('strval', (array) ($snapshot['tags'] ?? [])); $custom_defs = []; $custom = []; if (!empty($settings['custom_filter_s']) && class_exists('\Jio\Settings\Settings')) { $filter_all = \Jio\Settings\Settings::get_post_filter_settings(); $dims = is_array($filter_all['custom_filter'] ?? null) ? $filter_all['custom_filter'] : []; foreach ($dims as $dim) { $key = sanitize_key((string) ($dim['key'] ?? '')); if ($key === '') { continue; } $vals = []; foreach ((array) ($dim['vals'] ?? []) as $value) { $vkey = (string) ($value['key'] ?? ''); if ($vkey === '') { continue; } $vals[] = ['key' => $vkey, 'name' => (string) ($value['name'] ?? $vkey)]; } $custom_defs[] = ['key' => $key, 'name' => (string) ($dim['name'] ?? $key), 'values' => $vals]; $saved = $snapshot !== [] ? ($snapshot['custom'][$key] ?? []) : ($source instanceof \WP_Post ? get_post_meta($source->ID, '_jio_cf_' . $key, true) : []); $custom[$key] = is_array($saved) ? array_values(array_map('strval', $saved)) : ($saved !== '' ? [(string) $saved] : []); } } return [ 'categories' => ['items' => $categories, 'selected' => $current], 'tags' => ['items' => is_array($tags) ? array_values($tags) : [], 'enabled' => !empty($settings['allow_tags'])], 'custom' => $custom, 'customDefs' => $custom_defs, ]; } /** * @param array $post_meta * @return array */ private function creator_featured_payload(?\WP_Post $source, array $post_meta): array { $type = ''; $data = []; $id = 0; if ($source instanceof \WP_Post) { $thumb_id = (int) get_post_thumbnail_id($source->ID); $url = $thumb_id > 0 ? (string) wp_get_attachment_image_url($thumb_id, 'full') : (string) ($post_meta['thumbnail_url'] ?? ''); if ($url !== '') { $type = 'image'; $id = $thumb_id; $data = ['url' => $url, 'id' => $thumb_id]; } } $front = FrontSubmit::get_instance(); $user = get_current_user_id(); return [ 'type' => $type, 'data' => $data, 'mediaId' => $id, 'canImage' => $front->user_can_cap('new_post_image_cover', $user), 'canSlide' => $front->user_can_cap('new_post_slide_cover', $user), 'canVideo' => $front->user_can_cap('new_post_video_cover', $user), ]; } /** * @param array $pay_meta * @param array $settings * @return array */ private function pay_payload(array $pay_meta, array $snapshot, array $settings): array { $front = FrontSubmit::get_instance(); $user = get_current_user_id(); $pay = $snapshot['pay']['fields'] ?? $pay_meta; $enabled = $snapshot !== [] ? !empty($snapshot['pay']['enabled']) : (($pay_meta['pay_type'] ?? 'no') !== 'no'); $downloads = []; foreach ((array) ($pay['downloads'] ?? []) as $row) { if (!is_array($row)) { continue; } $key = PrivateAssets::sanitize_asset_key((string) ($row['asset_key'] ?? '')); $downloads[] = [ 'type' => $key !== '' ? 'protected' : (string) ($row['type'] ?? 'direct'), 'assetKey' => $key, 'url' => $key !== '' ? '' : (string) ($row['url'] ?? ''), 'label' => (string) ($row['label'] ?? ''), 'fileName' => (string) ($row['file_name'] ?? ''), 'sizeBytes' => (int) ($row['size_bytes'] ?? 0), ]; } return [ 'enabled' => (bool) $enabled, 'canPay' => $front->user_can_cap('new_post_pay', $user), 'canDownload' => $front->user_can_cap('new_post_pay_download', $user), 'payModo' => (($pay['pay_modo'] ?? '0') === 'points') ? 'points' : 'cash', 'price' => (string) ($pay['price'] ?? '0.00'), 'points' => (int) ($pay['points_price'] ?? 0), 'vip1Price' => (string) ($pay['vip1_price'] ?? '0.00'), 'vip2Price' => (string) ($pay['vip2_price'] ?? '0.00'), 'payLimit' => (int) ($pay['pay_limit'] ?? 0), 'downloads' => $downloads, 'freeAllowed' => !empty($settings['pay_free_s']), 'priceMin' => (int) ($settings['pay_price_min'] ?? 0), 'priceMax' => (int) ($settings['pay_price_max'] ?? 0), 'pointsMin' => (int) ($settings['pay_points_min'] ?? 0), 'pointsMax' => (int) ($settings['pay_points_max'] ?? 0), 'privateStorage' => PrivateAssets::is_available(), ]; } /** * @return array */ private function capability_payload(FrontSubmit $front, int $user_id, int $post_id): array { $cap = static function (string $name) use ($front, $user_id, $post_id): bool { return $front->user_can_cap($name, $user_id, $post_id > 0 ? $post_id : null); }; return [ 'canUploadImage' => $cap('upload_submit_image'), 'canUploadVideo' => $cap('new_post_upload_video'), 'canUploadFile' => $cap('upload_resource_attachment'), 'canEmbed' => $cap('new_post_iframe_video'), 'canHide' => $cap('new_post_hide'), 'canPay' => $cap('new_post_pay'), 'canPayDownload' => $cap('new_post_pay_download'), 'canPublishNow' => $cap('new_post_audit_no'), // 定时发布只有能直接发布的账号可用;其余账号提交后仍走审核队列。 'canSchedule' => $cap('new_post_audit_no'), 'canEdit' => $post_id > 0 ? $front->can_edit_post($post_id, $user_id) : $front->can_submit($user_id), 'isLoggedIn' => $user_id > 0, ]; } }