feat(links): move friend links to native WordPress links with review flow
- enable the native Links manager and drop the custom friend-link option/REST path - add per-page link settings for the friends template - render link categories from get_bookmarks() with search and page-content options - add a validated public link submission endpoint with logo upload, captcha, rate limiting and duplicate checks - create pending links for review and notify administrators by email and in-app message - add native Links admin pending count, columns and edit notes - cover the new submission and cleanup contracts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ef7b5939ba
commit
4c562428b4
24 files changed
+792
-273
No files matched your search
@@ -48,23 +48,6 @@ class GlobalController extends ControllerBase {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Friend Links
|
||||
register_rest_route(self::API_NAMESPACE, '/friend-links', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_friend_links'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/friend-links/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'rest_delete_friend_link'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/friend-links/reorder', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_reorder_friend_links'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// CMS Features
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/cms-features', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
@@ -167,63 +150,6 @@ class GlobalController extends ControllerBase {
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_handle_friend_links(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$links = jio_get_friend_links();
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $links]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$links = get_option('jio_friend_links', []);
|
||||
$new_link = [
|
||||
'id' => uniqid('fl_'),
|
||||
'name' => sanitize_text_field($params['name'] ?? ''),
|
||||
'url' => esc_url_raw($params['url'] ?? ''),
|
||||
'logo' => esc_url_raw($params['logo'] ?? ''),
|
||||
'description' => sanitize_textarea_field($params['description'] ?? ''),
|
||||
'sort_order' => (int) ($params['sort_order'] ?? 0),
|
||||
'active' => 1,
|
||||
];
|
||||
$links[] = $new_link;
|
||||
usort($links, function ($a, $b) {
|
||||
return ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0);
|
||||
});
|
||||
update_option('jio_friend_links', $links);
|
||||
do_action('jio_friend_link_submitted', $new_link);
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $new_link, 'message' => __('Friend link added.', 'jio')]);
|
||||
}
|
||||
|
||||
public function rest_delete_friend_link(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = $request->get_param('id');
|
||||
$links = get_option('jio_friend_links', []);
|
||||
$links = array_filter($links, function ($link) use ($id) {
|
||||
return ($link['id'] ?? '') !== $id;
|
||||
});
|
||||
$links = array_values($links);
|
||||
update_option('jio_friend_links', $links);
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Friend link deleted.', 'jio')]);
|
||||
}
|
||||
|
||||
public function rest_reorder_friend_links(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$ordered_ids = $request->get_param('ids');
|
||||
if (!is_array($ordered_ids)) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'jio')], 400);
|
||||
}
|
||||
$links = get_option('jio_friend_links', []);
|
||||
$order_map = array_flip($ordered_ids);
|
||||
usort($links, function ($a, $b) use ($order_map) {
|
||||
$idx_a = $order_map[($a['id'] ?? '')] ?? 999;
|
||||
$idx_b = $order_map[($b['id'] ?? '')] ?? 999;
|
||||
return $idx_a <=> $idx_b;
|
||||
});
|
||||
foreach ($links as $index => &$link) {
|
||||
$link['sort_order'] = $index;
|
||||
}
|
||||
update_option('jio_friend_links', $links);
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'jio')]);
|
||||
}
|
||||
|
||||
public function rest_handle_footer_tabbar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$settings = class_exists('\Jio\Settings\Settings')
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Native WordPress link administration enhancements.
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
namespace Jio\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class LinkManager {
|
||||
public static function init(): void {
|
||||
add_action('admin_bar_menu', [self::class, 'add_toolbar_node'], 999);
|
||||
add_filter('manage_link-manager_columns', [self::class, 'filter_columns']);
|
||||
add_action('manage_link_custom_column', [self::class, 'render_column'], 10, 2);
|
||||
add_action('add_meta_boxes_link', [self::class, 'add_edit_help']);
|
||||
}
|
||||
|
||||
public static function add_toolbar_node(\WP_Admin_Bar $admin_bar): void {
|
||||
if (!current_user_can('manage_links')) {
|
||||
return;
|
||||
}
|
||||
$count = self::pending_count();
|
||||
$title = '<span class="ab-icon dashicons-before dashicons-admin-links"></span>' . ($count > 0 ? (string) $count : '');
|
||||
$admin_bar->add_node([
|
||||
'id' => 'jio-links',
|
||||
'title' => $title,
|
||||
'href' => admin_url('link-manager.php' . ($count > 0 ? '?orderby=visible&order=asc' : '')),
|
||||
'meta' => ['title' => $count > 0 ? sprintf(__('有 %d 个链接待审核', 'jio'), $count) : __('链接管理', 'jio')],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<string,string> $columns @return array<string,string> */
|
||||
public static function filter_columns(array $columns): array {
|
||||
$columns['visible'] = sprintf(
|
||||
__('可见性(待审 %d)', 'jio'),
|
||||
self::pending_count()
|
||||
);
|
||||
unset($columns['rel']);
|
||||
$columns['link_description'] = __('简介', 'jio');
|
||||
$columns['link_image'] = __('Logo', 'jio');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public static function render_column(string $column, int $link_id): void {
|
||||
$link = get_bookmark($link_id);
|
||||
if (!$link) {
|
||||
return;
|
||||
}
|
||||
switch ($column) {
|
||||
case 'visible':
|
||||
echo !empty($link->link_visible)
|
||||
? esc_html__('已公开', 'jio')
|
||||
: '<strong>' . esc_html__('待审核', 'jio') . '</strong>';
|
||||
break;
|
||||
case 'link_description':
|
||||
echo esc_html((string) $link->link_description);
|
||||
break;
|
||||
case 'link_image':
|
||||
$image = esc_url((string) $link->link_image);
|
||||
if ($image !== '') {
|
||||
printf('<img src="%s" alt="" width="50" height="50" style="object-fit:cover">', $image);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static function add_edit_help(): void {
|
||||
add_meta_box(
|
||||
'jio-link-fields-help',
|
||||
__('链接资料说明', 'jio'),
|
||||
static function (): void {
|
||||
echo '<p>' . esc_html__('链接图像、RSS、备注和评分可在链接编辑页的“高级”区域设置;将链接设为私密即可保持待审核状态。', 'jio') . '</p>';
|
||||
},
|
||||
'link',
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
private static function pending_count(): int {
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->links} WHERE link_visible != %s",
|
||||
'Y'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
/**
|
||||
* Page configuration for native link listings.
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
namespace Jio\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class PageLinksMetaBox {
|
||||
private const NONCE_ACTION = 'jio_page_links_save';
|
||||
private const NONCE_NAME = 'jio_page_links_nonce';
|
||||
|
||||
public static function init(): void {
|
||||
add_action('add_meta_boxes_page', [self::class, 'register']);
|
||||
add_action('save_post_page', [self::class, 'save'], 10, 3);
|
||||
}
|
||||
|
||||
public static function register(\WP_Post $post): void {
|
||||
if (!self::is_links_page($post)) {
|
||||
return;
|
||||
}
|
||||
add_meta_box(
|
||||
'jio-page-links',
|
||||
__('链接页设置', 'jio'),
|
||||
[self::class, 'render'],
|
||||
'page',
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
public static function render(\WP_Post $post): void {
|
||||
$settings = function_exists('jio_get_page_links_settings')
|
||||
? jio_get_page_links_settings((int) $post->ID)
|
||||
: [];
|
||||
if ($settings === [] && !metadata_exists('post', (int) $post->ID, 'page_links_nofollow_s')) {
|
||||
$settings['nofollow_s'] = true;
|
||||
$settings['submit_sign_s'] = true;
|
||||
}
|
||||
wp_nonce_field(self::NONCE_ACTION, self::NONCE_NAME);
|
||||
$terms = get_terms(['taxonomy' => 'link_category', 'hide_empty' => false]);
|
||||
$terms = is_wp_error($terms) ? [] : $terms;
|
||||
$selected_categories = array_map('absint', (array) ($settings['category'] ?? []));
|
||||
$submit_categories = array_map('absint', (array) ($settings['submit_cats'] ?? []));
|
||||
$search_types = (array) ($settings['search_types'] ?? []);
|
||||
?>
|
||||
<table class="form-table" role="presentation">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('页面正文', 'jio'); ?></th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="page_links_content_s" value="1" <?php checked(!empty($settings['content_s'])); ?>> <?php esc_html_e('显示页面正文', 'jio'); ?></label>
|
||||
<p><select name="page_links_content_position">
|
||||
<option value="top" <?php selected($settings['content_position'] ?? 'top', 'top'); ?>><?php esc_html_e('正文在链接列表上方', 'jio'); ?></option>
|
||||
<option value="bottom" <?php selected($settings['content_position'] ?? 'top', 'bottom'); ?>><?php esc_html_e('正文在链接列表下方', 'jio'); ?></option>
|
||||
</select></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('搜索模块', 'jio'); ?></th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="page_links_search_s" value="1" <?php checked(!empty($settings['search_s'])); ?>> <?php esc_html_e('显示搜索引擎模块', 'jio'); ?></label>
|
||||
<p><?php foreach (self::search_types() as $key => $label) : ?>
|
||||
<label style="margin-right:12px"><input type="checkbox" name="page_links_search_types[]" value="<?php echo esc_attr($key); ?>" <?php checked(in_array($key, $search_types, true)); ?>> <?php echo esc_html($label); ?></label>
|
||||
<?php endforeach; ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="jio-page-links-categories"><?php esc_html_e('显示链接分类', 'jio'); ?></label></th>
|
||||
<td>
|
||||
<select id="jio-page-links-categories" name="page_links_category[]" multiple size="6" style="min-width:280px">
|
||||
<?php foreach ($terms as $term) : ?>
|
||||
<option value="<?php echo (int) $term->term_id; ?>" <?php selected(in_array((int) $term->term_id, $selected_categories, true)); ?>><?php echo esc_html($term->name); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e('可以多选并拖动排序;未选择分类时不显示链接列表。', 'jio'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('显示方式与排序', 'jio'); ?></th>
|
||||
<td>
|
||||
<label><?php esc_html_e('样式', 'jio'); ?>
|
||||
<select name="page_links_style">
|
||||
<option value="card" <?php selected($settings['style'] ?? 'card', 'card'); ?>><?php esc_html_e('图文列表', 'jio'); ?></option>
|
||||
<option value="bigcard" <?php selected($settings['style'] ?? 'card', 'bigcard'); ?>><?php esc_html_e('大卡片', 'jio'); ?></option>
|
||||
<option value="image" <?php selected($settings['style'] ?? 'card', 'image'); ?>><?php esc_html_e('纯图片', 'jio'); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="margin-left:12px"><?php esc_html_e('每类最多显示', 'jio'); ?>
|
||||
<input type="number" min="0" step="1" name="page_links_limit" value="<?php echo (int) ($settings['limit'] ?? 0); ?>" style="width:90px"> <?php esc_html_e('条(0 为不限)', 'jio'); ?>
|
||||
</label>
|
||||
<p>
|
||||
<label><?php esc_html_e('排序依据', 'jio'); ?>
|
||||
<select name="page_links_orderby">
|
||||
<option value="name" <?php selected($settings['orderby'] ?? 'name', 'name'); ?>><?php esc_html_e('名称', 'jio'); ?></option>
|
||||
<option value="updated" <?php selected($settings['orderby'] ?? 'name', 'updated'); ?>><?php esc_html_e('更新时间', 'jio'); ?></option>
|
||||
<option value="rating" <?php selected($settings['orderby'] ?? 'name', 'rating'); ?>><?php esc_html_e('评分', 'jio'); ?></option>
|
||||
<option value="rand" <?php selected($settings['orderby'] ?? 'name', 'rand'); ?>><?php esc_html_e('随机', 'jio'); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="margin-left:12px"><input type="radio" name="page_links_order" value="ASC" <?php checked($settings['order'] ?? 'ASC', 'ASC'); ?>> <?php esc_html_e('升序', 'jio'); ?></label>
|
||||
<label><input type="radio" name="page_links_order" value="DESC" <?php checked($settings['order'] ?? 'ASC', 'DESC'); ?>> <?php esc_html_e('降序', 'jio'); ?></label>
|
||||
</p>
|
||||
<label><input type="checkbox" name="page_links_go_s" value="1" <?php checked(!empty($settings['go_s'])); ?>> <?php esc_html_e('外链使用中转页', 'jio'); ?></label>
|
||||
<label style="margin-left:12px"><input type="checkbox" name="page_links_blank_s" value="1" <?php checked(!empty($settings['blank_s'])); ?>> <?php esc_html_e('新标签页打开', 'jio'); ?></label>
|
||||
<label style="margin-left:12px"><input type="checkbox" name="page_links_nofollow_s" value="1" <?php checked(!empty($settings['nofollow_s'])); ?>> <?php esc_html_e('添加 nofollow', 'jio'); ?></label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('链接申请', 'jio'); ?></th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="page_links_submit_s" value="1" <?php checked(!empty($settings['submit_s'])); ?>> <?php esc_html_e('显示申请链接入口', 'jio'); ?></label>
|
||||
<p><label><input type="checkbox" name="page_links_submit_sign_s" value="1" <?php checked(!empty($settings['submit_sign_s'])); ?>> <?php esc_html_e('登录后才能申请', 'jio'); ?></label></p>
|
||||
<label for="jio-page-links-submit-cats"><?php esc_html_e('允许选择的申请分类', 'jio'); ?></label><br>
|
||||
<select id="jio-page-links-submit-cats" name="page_links_submit_cats[]" multiple size="5" style="min-width:280px">
|
||||
<?php foreach ($terms as $term) : ?>
|
||||
<option value="<?php echo (int) $term->term_id; ?>" <?php selected(in_array((int) $term->term_id, $submit_categories, true)); ?>><?php echo esc_html($term->name); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e('留空表示允许选择所有链接分类。', 'jio'); ?></p>
|
||||
<p><label><?php esc_html_e('申请入口标题', 'jio'); ?><br><input type="text" name="page_links_submit_title" value="<?php echo esc_attr((string) ($settings['submit_title'] ?? '')); ?>" class="regular-text"></label></p>
|
||||
<label for="jio-page-links-submit-dec"><?php esc_html_e('申请说明', 'jio'); ?></label>
|
||||
<textarea id="jio-page-links-submit-dec" name="page_links_submit_dec" rows="6" class="large-text code"><?php echo esc_textarea((string) ($settings['submit_dec'] ?? '')); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
public static function save(int $post_id, \WP_Post $post, bool $update): void {
|
||||
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST[self::NONCE_NAME]) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST[self::NONCE_NAME])), self::NONCE_ACTION)) {
|
||||
return;
|
||||
}
|
||||
if (!self::is_links_page($post) || !current_user_can('edit_page', $post_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = wp_unslash($_POST);
|
||||
$term_ids = self::sanitize_term_ids($raw['page_links_category'] ?? []);
|
||||
$submit_term_ids = self::sanitize_term_ids($raw['page_links_submit_cats'] ?? []);
|
||||
$search_types = array_values(array_intersect(array_map('sanitize_key', (array) ($raw['page_links_search_types'] ?? [])), array_keys(self::search_types())));
|
||||
$style = sanitize_key((string) ($raw['page_links_style'] ?? 'card'));
|
||||
$orderby = sanitize_key((string) ($raw['page_links_orderby'] ?? 'name'));
|
||||
$order = strtoupper(sanitize_text_field((string) ($raw['page_links_order'] ?? 'ASC')));
|
||||
$position = sanitize_key((string) ($raw['page_links_content_position'] ?? 'top'));
|
||||
|
||||
$values = [
|
||||
'page_links_content_s' => !empty($raw['page_links_content_s']),
|
||||
'page_links_content_position' => in_array($position, ['top', 'bottom'], true) ? $position : 'top',
|
||||
'page_links_search_s' => !empty($raw['page_links_search_s']),
|
||||
'page_links_search_types' => $search_types,
|
||||
'page_links_category' => $term_ids,
|
||||
'page_links_style' => in_array($style, ['card', 'bigcard', 'image'], true) ? $style : 'card',
|
||||
'page_links_limit' => max(0, min(500, (int) ($raw['page_links_limit'] ?? 0))),
|
||||
'page_links_orderby' => in_array($orderby, ['name', 'updated', 'rating', 'rand'], true) ? $orderby : 'name',
|
||||
'page_links_order' => in_array($order, ['ASC', 'DESC'], true) ? $order : 'ASC',
|
||||
'page_links_go_s' => !empty($raw['page_links_go_s']),
|
||||
'page_links_blank_s' => !empty($raw['page_links_blank_s']),
|
||||
'page_links_nofollow_s' => !empty($raw['page_links_nofollow_s']),
|
||||
'page_links_submit_s' => !empty($raw['page_links_submit_s']),
|
||||
'page_links_submit_sign_s' => !empty($raw['page_links_submit_sign_s']),
|
||||
'page_links_submit_cats' => $submit_term_ids,
|
||||
'page_links_submit_title' => sanitize_text_field((string) ($raw['page_links_submit_title'] ?? '')),
|
||||
'page_links_submit_dec' => wp_kses_post((string) ($raw['page_links_submit_dec'] ?? '')),
|
||||
];
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
private static function is_links_page(\WP_Post $post): bool {
|
||||
return $post->post_name === 'friends' || get_page_template_slug($post->ID) === 'templates/pages/page-friends.php';
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private static function search_types(): array {
|
||||
return [
|
||||
'self' => __('站内', 'jio'),
|
||||
'baidu' => __('百度', 'jio'),
|
||||
'google' => __('谷歌', 'jio'),
|
||||
'bing' => __('必应', 'jio'),
|
||||
'sogou' => __('搜狗', 'jio'),
|
||||
'360' => '360',
|
||||
];
|
||||
}
|
||||
|
||||
/** @param mixed $raw @return list<int> */
|
||||
private static function sanitize_term_ids($raw): array {
|
||||
$ids = array_values(array_unique(array_filter(array_map('absint', (array) $raw))));
|
||||
return array_values(array_filter($ids, static function (int $id): bool {
|
||||
$term = get_term($id, 'link_category');
|
||||
return $term && !is_wp_error($term);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,7 @@ class SchemaPermissions {
|
||||
['label' => '打赏用户', 'value' => 'tip_user'],
|
||||
['label' => '提交工单', 'value' => 'ticket_create'],
|
||||
['label' => '回复工单', 'value' => 'ticket_reply'],
|
||||
['label' => '友情链接申请', 'value' => 'friend_link'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],
|
||||
|
||||
Reference in new issue
Block a user