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
@@ -12,11 +12,6 @@ export const globalApiMethods = {
|
|||||||
getFloatingButtons() { return this.request('/settings/floating-buttons') },
|
getFloatingButtons() { return this.request('/settings/floating-buttons') },
|
||||||
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
|
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
|
||||||
|
|
||||||
getFriendLinks() { return this.request('/friend-links') },
|
|
||||||
addFriendLink(data) { return this.request('/friend-links', { method: 'POST', body: JSON.stringify(data) }) },
|
|
||||||
deleteFriendLink(id) { return this.request(`/friend-links/${id}`, { method: 'DELETE' }) },
|
|
||||||
reorderFriendLinks(ids) { return this.request('/friend-links/reorder', { method: 'POST', body: JSON.stringify({ ids }) }) },
|
|
||||||
|
|
||||||
getCmsFeatures() { return this.request('/settings/cms-features') },
|
getCmsFeatures() { return this.request('/settings/cms-features') },
|
||||||
saveCmsFeatures(cms_features) { return this.request('/settings/cms-features', { method: 'POST', body: JSON.stringify({ cms_features }) }) },
|
saveCmsFeatures(cms_features) { return this.request('/settings/cms-features', { method: 'POST', body: JSON.stringify({ cms_features }) }) },
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,7 @@ export const permissionsSchemas = {
|
|||||||
{ label: '打赏用户', value: 'tip_user' },
|
{ label: '打赏用户', value: 'tip_user' },
|
||||||
{ label: '提交工单', value: 'ticket_create' },
|
{ label: '提交工单', value: 'ticket_create' },
|
||||||
{ label: '回复工单', value: 'ticket_reply' },
|
{ label: '回复工单', value: 'ticket_reply' },
|
||||||
|
{ label: '友情链接申请', value: 'friend_link' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* Jio Friends Page — 友情链接页样式
|
|
||||||
*
|
|
||||||
* @package Jio
|
|
||||||
*/
|
|
||||||
|
|
||||||
.friends-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
||||||
gap: var(--space-4);
|
|
||||||
margin-top: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-4);
|
|
||||||
padding: var(--card-padding);
|
|
||||||
background: var(--surface-1);
|
|
||||||
border: 1px solid var(--color-line);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--color-ink-2);
|
|
||||||
transition: box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-card:hover {
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
border-color: var(--color-primary-soft);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-card__avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: var(--radius-avatar, 50%);
|
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: var(--surface-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-card__info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-card__name {
|
|
||||||
font-size: var(--text-base);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-ink);
|
|
||||||
margin: 0 0 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-card__desc {
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
color: var(--color-ink-3);
|
|
||||||
margin: 0;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: var(--leading-snug);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.friends-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -148,6 +148,17 @@
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jio-links-submit-status {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-links-logo-preview {
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- search cover ---- */
|
/* ---- search cover ---- */
|
||||||
.link-page-search-cover.page-cover {
|
.link-page-search-cover.page-cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Native link submission form.
|
||||||
|
*
|
||||||
|
* @package Jio
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var cfg = window.jioLinksSubmit || {};
|
||||||
|
var i18n = cfg.i18n || {};
|
||||||
|
var form = document.querySelector('.jio-links-submit-form');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
var button = form.querySelector('.jio-links-submit-btn');
|
||||||
|
var status = form.querySelector('.jio-links-submit-status');
|
||||||
|
var logoInput = form.querySelector('.jio-links-logo-input');
|
||||||
|
var preview = form.querySelector('.jio-links-logo-preview');
|
||||||
|
|
||||||
|
function setStatus(message, type) {
|
||||||
|
if (!status) return;
|
||||||
|
status.textContent = message || '';
|
||||||
|
status.className = 'jio-links-submit-status' + (type ? ' text-' + type : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logoInput && preview) {
|
||||||
|
logoInput.addEventListener('change', function () {
|
||||||
|
var file = logoInput.files && logoInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
preview.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (button && button.disabled) return;
|
||||||
|
|
||||||
|
var payload = new FormData(form);
|
||||||
|
var original = button ? button.innerHTML : '';
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<i class="fa fas fa-spinner fa-spin"></i> ' + (i18n.submitting || '正在提交…');
|
||||||
|
}
|
||||||
|
setStatus(i18n.submitting || '正在提交…', 'muted');
|
||||||
|
|
||||||
|
var captcha = window.JioCaptcha;
|
||||||
|
var captchaPromise = captcha ? captcha.run('friend_link', form) : Promise.resolve({});
|
||||||
|
captchaPromise.then(function (tokens) {
|
||||||
|
if (tokens === null) throw new Error(i18n.captcha || '请完成人机验证');
|
||||||
|
Object.keys(tokens || {}).forEach(function (key) {
|
||||||
|
payload.set(key, tokens[key]);
|
||||||
|
});
|
||||||
|
return fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}).then(function (response) {
|
||||||
|
return response.json().then(function (body) {
|
||||||
|
return { ok: response.ok, body: body };
|
||||||
|
});
|
||||||
|
}).then(function (result) {
|
||||||
|
var body = result.body || {};
|
||||||
|
var data = body.data || {};
|
||||||
|
if (!result.ok || !body.success) {
|
||||||
|
throw new Error(data.message || i18n.error || '提交失败,请重试');
|
||||||
|
}
|
||||||
|
setStatus(data.message || i18n.success || '提交成功', 'success');
|
||||||
|
form.reset();
|
||||||
|
if (preview) preview.src = '';
|
||||||
|
if (button) button.disabled = false;
|
||||||
|
if (button) button.innerHTML = original;
|
||||||
|
if (captcha) captcha.reset('friend_link', form);
|
||||||
|
window.setTimeout(function () {
|
||||||
|
var modal = document.getElementById('submit-links-modal');
|
||||||
|
if (modal) modal.classList.remove('show', 'is-open');
|
||||||
|
setStatus('', '');
|
||||||
|
}, 1800);
|
||||||
|
}).catch(function (error) {
|
||||||
|
setStatus(error.message || i18n.error || '提交失败,请重试', 'danger');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = original;
|
||||||
|
}
|
||||||
|
if (captcha) captcha.reset('friend_link', form);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -130,6 +130,15 @@ if (class_exists('\Jio\Admin\NavMenuOptions')) {
|
|||||||
if (class_exists('\Jio\Admin\PageConfigMetaBox')) {
|
if (class_exists('\Jio\Admin\PageConfigMetaBox')) {
|
||||||
\Jio\Admin\PageConfigMetaBox::init();
|
\Jio\Admin\PageConfigMetaBox::init();
|
||||||
}
|
}
|
||||||
|
if (class_exists('\Jio\Admin\PageLinksMetaBox')) {
|
||||||
|
\Jio\Admin\PageLinksMetaBox::init();
|
||||||
|
}
|
||||||
|
if (class_exists('\Jio\Admin\LinkManager')) {
|
||||||
|
\Jio\Admin\LinkManager::init();
|
||||||
|
}
|
||||||
|
if (class_exists('\Jio\Core\LinksSubmission')) {
|
||||||
|
\Jio\Core\LinksSubmission::init();
|
||||||
|
}
|
||||||
\Jio\Auth\OAuth::get_instance()->init();
|
\Jio\Auth\OAuth::get_instance()->init();
|
||||||
if (class_exists('\Jio\Auth\WeixinGzh')) {
|
if (class_exists('\Jio\Auth\WeixinGzh')) {
|
||||||
\Jio\Auth\WeixinGzh::get_instance()->init();
|
\Jio\Auth\WeixinGzh::get_instance()->init();
|
||||||
|
|||||||
+19
-11
@@ -84,14 +84,14 @@ if (!function_exists('jio_get_page_links_settings')) :
|
|||||||
'order' => $order,
|
'order' => $order,
|
||||||
'go_s' => (bool) get_post_meta($post_id, 'page_links_go_s', true),
|
'go_s' => (bool) get_post_meta($post_id, 'page_links_go_s', true),
|
||||||
'blank_s' => (bool) get_post_meta($post_id, 'page_links_blank_s', true),
|
'blank_s' => (bool) get_post_meta($post_id, 'page_links_blank_s', true),
|
||||||
// Default nofollow ON (legacy default true).
|
// Default nofollow ON unless the page meta explicitly turns it off.
|
||||||
'nofollow_s' => get_post_meta($post_id, 'page_links_nofollow_s', true) === ''
|
'nofollow_s' => metadata_exists('post', $post_id, 'page_links_nofollow_s')
|
||||||
? true
|
? (bool) get_post_meta($post_id, 'page_links_nofollow_s', true)
|
||||||
: (bool) get_post_meta($post_id, 'page_links_nofollow_s', true),
|
: true,
|
||||||
'submit_s' => (bool) get_post_meta($post_id, 'page_links_submit_s', true),
|
'submit_s' => (bool) get_post_meta($post_id, 'page_links_submit_s', true),
|
||||||
'submit_sign_s' => get_post_meta($post_id, 'page_links_submit_sign_s', true) === ''
|
'submit_sign_s' => metadata_exists('post', $post_id, 'page_links_submit_sign_s')
|
||||||
? true
|
? (bool) get_post_meta($post_id, 'page_links_submit_sign_s', true)
|
||||||
: (bool) get_post_meta($post_id, 'page_links_submit_sign_s', true),
|
: true,
|
||||||
'submit_cats' => $submit_cats,
|
'submit_cats' => $submit_cats,
|
||||||
'submit_title' => (string) (get_post_meta($post_id, 'page_links_submit_title', true) ?: __('申请入驻', 'jio')),
|
'submit_title' => (string) (get_post_meta($post_id, 'page_links_submit_title', true) ?: __('申请入驻', 'jio')),
|
||||||
'submit_dec' => (string) get_post_meta($post_id, 'page_links_submit_dec', true),
|
'submit_dec' => (string) get_post_meta($post_id, 'page_links_submit_dec', true),
|
||||||
@@ -393,11 +393,11 @@ endif;
|
|||||||
|
|
||||||
if (!function_exists('jio_submit_links_modal')) :
|
if (!function_exists('jio_submit_links_modal')) :
|
||||||
/**
|
/**
|
||||||
* Frontend submit modal markup (legacy).
|
* Frontend link submission modal.
|
||||||
*
|
*
|
||||||
* @param array{title?:string,dec?:string,sign?:bool,cats?:int[]} $args
|
* @param array{title?:string,dec?:string,sign?:bool,cats?:int[]} $args
|
||||||
*/
|
*/
|
||||||
function jio_submit_links_modal(array $args = []): string {
|
function jio_submit_links_modal(array $args = [], int $post_id = 0): string {
|
||||||
$defaults = [
|
$defaults = [
|
||||||
'title' => __('申请入驻', 'jio'),
|
'title' => __('申请入驻', 'jio'),
|
||||||
'dec' => '',
|
'dec' => '',
|
||||||
@@ -405,6 +405,7 @@ if (!function_exists('jio_submit_links_modal')) :
|
|||||||
'cats' => [],
|
'cats' => [],
|
||||||
];
|
];
|
||||||
$args = array_merge($defaults, $args);
|
$args = array_merge($defaults, $args);
|
||||||
|
$post_id = $post_id > 0 ? $post_id : (int) get_queried_object_id();
|
||||||
$title = (string) $args['title'];
|
$title = (string) $args['title'];
|
||||||
$header = $title !== ''
|
$header = $title !== ''
|
||||||
? '<div class="mb20"><button type="button" class="close" data-dismiss="modal" aria-label="' . esc_attr__('关闭', 'jio') . '">'
|
? '<div class="mb20"><button type="button" class="close" data-dismiss="modal" aria-label="' . esc_attr__('关闭', 'jio') . '">'
|
||||||
@@ -453,7 +454,7 @@ if (!function_exists('jio_submit_links_modal')) :
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
$placeholder_upload = get_template_directory_uri() . '/assets/img/placeholder.svg';
|
$placeholder_upload = get_template_directory_uri() . '/assets/img/placeholder.svg';
|
||||||
$body .= '<form class="form-horizontal mt10 form-upload jio-links-submit-form" enctype="multipart/form-data">';
|
$body .= '<form class="form-horizontal mt10 jio-links-submit-form" action="' . esc_url(admin_url('admin-ajax.php')) . '" method="post" enctype="multipart/form-data">';
|
||||||
$body .= '<div class="row gutters-5">';
|
$body .= '<div class="row gutters-5">';
|
||||||
$body .= '<div class="col-sm-6 mb10"><div class="em09 muted-2-color mb6">' . esc_html__('网站名称(必填)', 'jio') . '</div>'
|
$body .= '<div class="col-sm-6 mb10"><div class="em09 muted-2-color mb6">' . esc_html__('网站名称(必填)', 'jio') . '</div>'
|
||||||
. '<input type="text" class="form-control" name="link_name" required placeholder="' . esc_attr__('请输入网站名称', 'jio') . '"></div>';
|
. '<input type="text" class="form-control" name="link_name" required placeholder="' . esc_attr__('请输入网站名称', 'jio') . '"></div>';
|
||||||
@@ -468,11 +469,18 @@ if (!function_exists('jio_submit_links_modal')) :
|
|||||||
. '<input class="hide jio-links-logo-input" type="file" accept="image/gif,image/jpeg,image/jpg,image/png" name="image"></label>'
|
. '<input class="hide jio-links-logo-input" type="file" accept="image/gif,image/jpeg,image/jpg,image/png" name="image"></label>'
|
||||||
. '<div class="px12 muted-2-color mb6">' . esc_html__('请选择正方形 LOGO,支持 jpg/png/gif', 'jio') . '</div></div>';
|
. '<div class="px12 muted-2-color mb6">' . esc_html__('请选择正方形 LOGO,支持 jpg/png/gif', 'jio') . '</div></div>';
|
||||||
$body .= '</div>';
|
$body .= '</div>';
|
||||||
|
if (function_exists('jio_render_captcha')) {
|
||||||
|
ob_start();
|
||||||
|
jio_render_captcha('friend_link');
|
||||||
|
$body .= (string) ob_get_clean();
|
||||||
|
}
|
||||||
|
$body .= '<div class="jio-links-submit-status" role="status" aria-live="polite"></div>';
|
||||||
$body .= '<div class="text-right edit-footer">'
|
$body .= '<div class="text-right edit-footer">'
|
||||||
. '<button type="submit" class="but c-blue padding-lg jio-links-submit-btn"><i class="fa fas fa-check" aria-hidden="true"></i>'
|
. '<button type="submit" class="but c-blue padding-lg jio-links-submit-btn"><i class="fa fas fa-check" aria-hidden="true"></i>'
|
||||||
. esc_html__('确认提交', 'jio') . '</button></div>';
|
. esc_html__('确认提交', 'jio') . '</button></div>';
|
||||||
$body .= wp_nonce_field('jio_frontend_links_submit', '_wpnonce', false, false);
|
$body .= wp_nonce_field('jio_frontend_links_submit_' . $post_id, '_wpnonce', false, false);
|
||||||
$body .= '<input type="hidden" name="action" value="jio_frontend_links_submit">';
|
$body .= '<input type="hidden" name="action" value="jio_frontend_links_submit">';
|
||||||
|
$body .= '<input type="hidden" name="page_id" value="' . (int) $post_id . '">';
|
||||||
$body .= '</form>';
|
$body .= '</form>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,24 +10,6 @@ defined('ABSPATH') || exit;
|
|||||||
// 模板标签:友情链接
|
// 模板标签:友情链接
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
if (!function_exists('jio_get_friend_links')):
|
|
||||||
/**
|
|
||||||
* 获取友情链接列表(已激活的)
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
function jio_get_friend_links() {
|
|
||||||
if (class_exists('\Jio\Settings\Settings')) {
|
|
||||||
return \Jio\Settings\Settings::get_friend_links();
|
|
||||||
}
|
|
||||||
|
|
||||||
$links = get_option('jio_friend_links', array());
|
|
||||||
return array_values(array_filter($links, function($l) {
|
|
||||||
return !empty($l['active']);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
endif;
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 模板标签:站点 Logo / Favicon / About
|
// 模板标签:站点 Logo / Favicon / About
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -377,6 +377,7 @@ function jio_captcha_scenes(): array {
|
|||||||
'tip_user' => __('打赏用户', 'jio'),
|
'tip_user' => __('打赏用户', 'jio'),
|
||||||
'ticket_create' => __('提交工单', 'jio'),
|
'ticket_create' => __('提交工单', 'jio'),
|
||||||
'ticket_reply' => __('回复工单', 'jio'),
|
'ticket_reply' => __('回复工单', 'jio'),
|
||||||
|
'friend_link' => __('友情链接申请', 'jio'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,23 +48,6 @@ class GlobalController extends ControllerBase {
|
|||||||
'permission_callback' => [$this, 'check_permission'],
|
'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
|
// CMS Features
|
||||||
register_rest_route(self::API_NAMESPACE, '/settings/cms-features', [
|
register_rest_route(self::API_NAMESPACE, '/settings/cms-features', [
|
||||||
'methods' => ['GET', 'POST'],
|
'methods' => ['GET', 'POST'],
|
||||||
@@ -167,63 +150,6 @@ class GlobalController extends ControllerBase {
|
|||||||
return $this->success(['data' => $data]);
|
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 {
|
public function rest_handle_footer_tabbar(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
if ($request->get_method() === 'GET') {
|
if ($request->get_method() === 'GET') {
|
||||||
$settings = class_exists('\Jio\Settings\Settings')
|
$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' => 'tip_user'],
|
||||||
['label' => '提交工单', 'value' => 'ticket_create'],
|
['label' => '提交工单', 'value' => 'ticket_create'],
|
||||||
['label' => '回复工单', 'value' => 'ticket_reply'],
|
['label' => '回复工单', 'value' => 'ticket_reply'],
|
||||||
|
['label' => '友情链接申请', 'value' => 'friend_link'],
|
||||||
]],
|
]],
|
||||||
]],
|
]],
|
||||||
['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],
|
['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Public submission of native WordPress links.
|
||||||
|
*
|
||||||
|
* @package Jio
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Jio\Core;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
final class LinksSubmission {
|
||||||
|
private const ACTION = 'jio_frontend_links_submit';
|
||||||
|
private const NONCE_ACTION_PREFIX = 'jio_frontend_links_submit_';
|
||||||
|
private const RATE_LIMIT_SECONDS = 30;
|
||||||
|
|
||||||
|
public static function init(): void {
|
||||||
|
add_action('wp_ajax_' . self::ACTION, [self::class, 'handle']);
|
||||||
|
add_action('wp_ajax_nopriv_' . self::ACTION, [self::class, 'handle']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function handle(): void {
|
||||||
|
$page_id = absint($_POST['page_id'] ?? 0);
|
||||||
|
$page = get_post($page_id);
|
||||||
|
if (!$page instanceof \WP_Post || !self::is_links_page($page) || $page->post_status !== 'publish') {
|
||||||
|
self::fail(__('申请页面无效,请刷新后重试。', 'jio'), 400);
|
||||||
|
}
|
||||||
|
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(
|
||||||
|
sanitize_text_field(wp_unslash((string) $_POST['_wpnonce'])),
|
||||||
|
self::NONCE_ACTION_PREFIX . $page_id
|
||||||
|
)) {
|
||||||
|
self::fail(__('安全验证失败,请刷新页面后重试。', 'jio'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = jio_get_page_links_settings($page_id);
|
||||||
|
if (empty($settings['submit_s'])) {
|
||||||
|
self::fail(__('当前页面未开放链接申请。', 'jio'), 403);
|
||||||
|
}
|
||||||
|
if (!empty($settings['submit_sign_s']) && !is_user_logged_in()) {
|
||||||
|
self::fail(__('请先登录后再申请。', 'jio'), 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = wp_unslash($_POST);
|
||||||
|
if (function_exists('jio_require_captcha')) {
|
||||||
|
$captcha = jio_require_captcha('friend_link', is_array($payload) ? $payload : []);
|
||||||
|
if (is_wp_error($captcha)) {
|
||||||
|
self::fail($captcha->get_error_message(), 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ip = function_exists('jio_get_client_ip')
|
||||||
|
? (string) jio_get_client_ip()
|
||||||
|
: (string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
||||||
|
if (function_exists('jio_captcha_rate_limited') && jio_captcha_rate_limited('friend-link-submit', $ip, 1, self::RATE_LIMIT_SECONDS)) {
|
||||||
|
self::fail(__('操作过于频繁,请稍候再试。', 'jio'), 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = sanitize_text_field((string) ($payload['link_name'] ?? ''));
|
||||||
|
$name = function_exists('mb_substr') ? mb_substr($name, 0, 255) : substr($name, 0, 255);
|
||||||
|
$description = sanitize_text_field((string) ($payload['link_description'] ?? ''));
|
||||||
|
$description = function_exists('mb_substr') ? mb_substr($description, 0, 255) : substr($description, 0, 255);
|
||||||
|
$url = self::sanitize_url((string) ($payload['link_url'] ?? '')); if ($name === '') {
|
||||||
|
self::fail(__('请填写网站名称。', 'jio'));
|
||||||
|
}
|
||||||
|
if ($url === '') {
|
||||||
|
self::fail(__('请填写有效的网站地址。', 'jio'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$category_ids = self::allowed_categories($settings);
|
||||||
|
$submitted_category = absint($payload['link_category'] ?? 0);
|
||||||
|
$categories = [];
|
||||||
|
if ($category_ids === [] && $submitted_category > 0) {
|
||||||
|
self::fail(__('请选择有效的网站分类。', 'jio'));
|
||||||
|
}
|
||||||
|
if ($category_ids !== []) {
|
||||||
|
if ($submitted_category <= 0 || !in_array($submitted_category, $category_ids, true)) {
|
||||||
|
self::fail(__('请选择有效的网站分类。', 'jio'));
|
||||||
|
}
|
||||||
|
$categories[] = $submitted_category;
|
||||||
|
}
|
||||||
|
if (self::duplicate_url($url)) {
|
||||||
|
self::fail(__('该链接已存在,请勿重复提交。', 'jio'), 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$image_url = self::upload_image();
|
||||||
|
if (is_wp_error($image_url)) {
|
||||||
|
self::fail($image_url->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wp_insert_link')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/bookmark.php';
|
||||||
|
}
|
||||||
|
$link_data = [
|
||||||
|
'link_name' => $name,
|
||||||
|
'link_url' => $url,
|
||||||
|
'link_description' => $description,
|
||||||
|
'link_image' => $image_url,
|
||||||
|
'link_visible' => is_super_admin() ? 'Y' : 'N',
|
||||||
|
'link_category' => $categories,
|
||||||
|
];
|
||||||
|
$link_id = wp_insert_link($link_data, true);
|
||||||
|
if (is_wp_error($link_id) || (int) $link_id <= 0) {
|
||||||
|
self::fail(__('链接保存失败,请稍后重试。', 'jio'), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($link_data['link_visible'] !== 'Y') {
|
||||||
|
do_action('jio_friend_link_submitted', array_merge($link_data, [
|
||||||
|
'link_id' => (int) $link_id,
|
||||||
|
'username' => is_user_logged_in() ? wp_get_current_user()->display_name : __('访客', 'jio'),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
wp_send_json_success([
|
||||||
|
'message' => $link_data['link_visible'] === 'Y'
|
||||||
|
? __('链接提交成功。', 'jio')
|
||||||
|
: __('链接提交成功,等待管理员审核。', 'jio'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function is_links_page(\WP_Post $page): bool {
|
||||||
|
return $page->post_type === 'page'
|
||||||
|
&& ($page->post_name === 'friends' || get_page_template_slug($page->ID) === 'templates/pages/page-friends.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $settings @return list<int> */
|
||||||
|
private static function allowed_categories(array $settings): array {
|
||||||
|
$configured = array_values(array_filter(array_map('absint', (array) ($settings['submit_cats'] ?? []))));
|
||||||
|
if ($configured !== []) {
|
||||||
|
$ids = $configured;
|
||||||
|
} else {
|
||||||
|
$terms = get_terms(['taxonomy' => 'link_category', 'hide_empty' => false, 'fields' => 'ids']);
|
||||||
|
$ids = is_wp_error($terms) ? [] : array_map('absint', (array) $terms);
|
||||||
|
}
|
||||||
|
$ids = array_values(array_unique($ids));
|
||||||
|
return array_values(array_filter($ids, static function (int $id): bool {
|
||||||
|
$term = get_term($id, 'link_category');
|
||||||
|
return $term && !is_wp_error($term);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function sanitize_url(string $raw): string {
|
||||||
|
$url = esc_url_raw(trim($raw), ['http', 'https']);
|
||||||
|
$parts = wp_parse_url($url);
|
||||||
|
if (!is_array($parts) || empty($parts['host']) || !in_array(strtolower((string) ($parts['scheme'] ?? '')), ['http', 'https'], true)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (isset($parts['user']) || isset($parts['pass'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function duplicate_url(string $url): bool {
|
||||||
|
global $wpdb;
|
||||||
|
$target = self::normalize_url($url);
|
||||||
|
$urls = $wpdb->get_col($wpdb->prepare("SELECT link_url FROM {$wpdb->links} WHERE link_url <> %s", ''));
|
||||||
|
foreach ((array) $urls as $existing) {
|
||||||
|
if (self::normalize_url((string) $existing) === $target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalize_url(string $url): string {
|
||||||
|
$parts = wp_parse_url($url);
|
||||||
|
if (!is_array($parts) || empty($parts['host'])) {
|
||||||
|
return strtolower(trim($url));
|
||||||
|
}
|
||||||
|
$scheme = strtolower((string) ($parts['scheme'] ?? 'http'));
|
||||||
|
$host = strtolower((string) $parts['host']);
|
||||||
|
$port = isset($parts['port']) && !in_array([(string) $scheme, (int) $parts['port']], [['http', 80], ['https', 443]], true)
|
||||||
|
? ':' . (int) $parts['port']
|
||||||
|
: '';
|
||||||
|
$path = (string) ($parts['path'] ?? '/');
|
||||||
|
$path = $path === '/' ? '/' : rtrim($path, '/');
|
||||||
|
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||||
|
return $scheme . '://' . $host . $port . $path . $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return string|\WP_Error */
|
||||||
|
private static function upload_image() {
|
||||||
|
$file = $_FILES['image'] ?? null;
|
||||||
|
if (!is_array($file) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if ((int) ($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
|
||||||
|
return new \WP_Error('jio_link_upload', __('Logo 上传失败,请重试。', 'jio'));
|
||||||
|
}
|
||||||
|
if ((int) ($file['size'] ?? 0) > 3 * 1024 * 1024) {
|
||||||
|
return new \WP_Error('jio_link_upload', __('Logo 图片不能超过 3MB。', 'jio'));
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||||
|
$attachment_id = media_handle_upload('image', 0, [], [
|
||||||
|
'test_form' => false,
|
||||||
|
'mimes' => [
|
||||||
|
'jpg|jpeg|jpe' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
if (is_wp_error($attachment_id)) {
|
||||||
|
return new \WP_Error('jio_link_upload', __('Logo 上传失败,请检查图片格式和大小。', 'jio'));
|
||||||
|
}
|
||||||
|
$image_url = wp_get_attachment_image_url((int) $attachment_id, 'medium');
|
||||||
|
return $image_url ? esc_url_raw($image_url) : new \WP_Error('jio_link_upload', __('Logo 读取失败,请重试。', 'jio'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function fail(string $message, int $status = 400): void {
|
||||||
|
wp_send_json_error(['message' => $message], $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-11
@@ -536,20 +536,39 @@ trait ScriptsPart1b {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 友情链接页:路由专属 CSS
|
// 友情链接页:原生链接列表 + 申请表单
|
||||||
if (is_page_template('templates/pages/page-friends.php')) {
|
if (is_page_template('templates/pages/page-friends.php')) {
|
||||||
wp_enqueue_style(
|
$links_css = get_template_directory() . '/assets/css/routes/links.css';
|
||||||
'jio-route-friends',
|
if (file_exists($links_css)) {
|
||||||
$base_uri . '/assets/css/routes/friends.css',
|
wp_enqueue_style(
|
||||||
['jio-public'],
|
'jio-route-links',
|
||||||
file_exists(get_template_directory() . '/assets/css/routes/friends.css')
|
$base_uri . '/assets/css/routes/links.css',
|
||||||
? (string) filemtime(get_template_directory() . '/assets/css/routes/friends.css')
|
['jio-public'],
|
||||||
: $version
|
(string) filemtime($links_css)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
$links_js = get_template_directory() . '/assets/js/links-submit.js';
|
||||||
|
if (file_exists($links_js)) {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'jio-links-submit',
|
||||||
|
$base_uri . '/assets/js/links-submit.js',
|
||||||
|
['loader_js'],
|
||||||
|
(string) filemtime($links_js),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
wp_localize_script('jio-links-submit', 'jioLinksSubmit', [
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
'i18n' => [
|
||||||
|
'submitting' => __('正在提交…', 'jio'),
|
||||||
|
'success' => __('提交成功', 'jio'),
|
||||||
|
'error' => __('提交失败,请重试', 'jio'),
|
||||||
|
'captcha' => __('请完成人机验证', 'jio'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网址导航页(legacy links.php)
|
// 文章导航页(postsnavs.php)
|
||||||
// 文章导航页(legacy postsnavs.php)
|
|
||||||
if (is_page_template('templates/pages/page-posts-navs.php')) {
|
if (is_page_template('templates/pages/page-posts-navs.php')) {
|
||||||
$page_navs_js = get_template_directory() . '/assets/js/page-navs.js';
|
$page_navs_js = get_template_directory() . '/assets/js/page-navs.js';
|
||||||
if (file_exists($page_navs_js)) {
|
if (file_exists($page_navs_js)) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Setup {
|
|||||||
add_action('after_setup_theme', [$this, 'load_textdomain'], 1);
|
add_action('after_setup_theme', [$this, 'load_textdomain'], 1);
|
||||||
add_action('after_setup_theme', [$this, 'setup_theme']);
|
add_action('after_setup_theme', [$this, 'setup_theme']);
|
||||||
add_filter('dynamic_sidebar_params', [$this, 'filter_sidebar_widget_params']);
|
add_filter('dynamic_sidebar_params', [$this, 'filter_sidebar_widget_params']);
|
||||||
|
add_filter('pre_option_link_manager_enabled', '__return_true');
|
||||||
add_action('init', [$this, 'register_nav_menus'], 0);
|
add_action('init', [$this, 'register_nav_menus'], 0);
|
||||||
add_action('init', [$this, 'register_post_types'], 1);
|
add_action('init', [$this, 'register_post_types'], 1);
|
||||||
add_action('init', [$this, 'register_meta_fields'], 5);
|
add_action('init', [$this, 'register_meta_fields'], 5);
|
||||||
|
|||||||
@@ -189,7 +189,6 @@ trait DisplayPageGroups {
|
|||||||
'tabbar' => self::get_footer_tabbar_settings(),
|
'tabbar' => self::get_footer_tabbar_settings(),
|
||||||
'floating_buttons' => self::get_floating_buttons_settings(),
|
'floating_buttons' => self::get_floating_buttons_settings(),
|
||||||
'header_theme' => self::get_header_theme_config(),
|
'header_theme' => self::get_header_theme_config(),
|
||||||
'friend_links' => self::get_friend_links(),
|
|
||||||
'cms_features' => self::get_cms_features(),
|
'cms_features' => self::get_cms_features(),
|
||||||
'page_display' => self::get_page_display_settings(),
|
'page_display' => self::get_page_display_settings(),
|
||||||
'footer_theme' => self::get_footer_theme_config(),
|
'footer_theme' => self::get_footer_theme_config(),
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package Jio
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Jio\Settings;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
class DisplayRuntime {
|
|
||||||
|
|
||||||
/** 获取友链列表 */
|
|
||||||
public static function get_friend_links(): array {
|
|
||||||
$links = get_option('jio_friend_links', []) ?: [];
|
|
||||||
|
|
||||||
return array_values(array_filter($links, static function ($link) {
|
|
||||||
return !empty($link['active']);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 保存友链 */
|
|
||||||
public static function save_friend_links(array $links): void {
|
|
||||||
update_option('jio_friend_links', $links);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -220,7 +220,30 @@ final class MailNotifications {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function friend_link_submitted(array $link): void {
|
public static function friend_link_submitted(array $link): void {
|
||||||
self::send_admin('links_submit_admin', ['title' => (string) ($link['name'] ?? ''), 'username' => __('访客', 'jio'), 'url' => (string) ($link['url'] ?? '')]);
|
$url = admin_url('link-manager.php?orderby=visible&order=asc');
|
||||||
|
self::send_admin('links_submit_admin', [
|
||||||
|
'title' => (string) ($link['link_name'] ?? $link['name'] ?? ''),
|
||||||
|
'username' => (string) ($link['username'] ?? __('访客', 'jio')),
|
||||||
|
'url' => (string) ($link['link_url'] ?? $link['url'] ?? $url),
|
||||||
|
'review_url' => $url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!class_exists('\Jio\Messages\Message') || !class_exists('\Jio\Settings\Integration') || !\Jio\Settings\Integration::message_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$title = sprintf(__('新的链接待审核:%s', 'jio'), (string) ($link['link_name'] ?? $link['name'] ?? ''));
|
||||||
|
$content = sprintf(
|
||||||
|
__('网站有新的链接提交:%1$s(%2$s),提交人:%3$s。', 'jio'),
|
||||||
|
(string) ($link['link_name'] ?? $link['name'] ?? ''),
|
||||||
|
(string) ($link['link_url'] ?? $link['url'] ?? ''),
|
||||||
|
(string) ($link['username'] ?? __('访客', 'jio'))
|
||||||
|
);
|
||||||
|
foreach (get_users(['role' => 'administrator', 'fields' => 'ID', 'number' => 20]) as $admin_id) {
|
||||||
|
\Jio\Messages\Message::get_instance()->send((int) $admin_id, 'system', $content, [
|
||||||
|
'title' => $title,
|
||||||
|
'target_url' => $url,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed> $vars */
|
/** @param array<string,mixed> $vars */
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ final class MailTemplates {
|
|||||||
/** @return array<string, array<string, mixed>> */
|
/** @return array<string, array<string, mixed>> */
|
||||||
public static function definitions(): array {
|
public static function definitions(): array {
|
||||||
return [
|
return [
|
||||||
'links_submit_admin' => self::event('友情链接提交通知', '新友情链接提交:{title}\n提交人:{username}\n链接:{url}\n请登录后台审核。', ['title', 'username', 'url']),
|
'links_submit_admin' => self::event('友情链接提交通知', '新友情链接提交:{title}\n提交人:{username}\n链接:{url}\n请登录后台审核:{review_url}', ['title', 'username', 'url', 'review_url']),
|
||||||
'order_paid_admin' => self::event('新订单通知(管理员)', '收到一笔新订单。\n订单号:{order_number}\n用户:{username}\n商品:{product}\n金额:{amount} {currency}\n支付方式:{payment_method}', ['order_number', 'username', 'product', 'amount', 'currency', 'payment_method']),
|
'order_paid_admin' => self::event('新订单通知(管理员)', '收到一笔新订单。\n订单号:{order_number}\n用户:{username}\n商品:{product}\n金额:{amount} {currency}\n支付方式:{payment_method}', ['order_number', 'username', 'product', 'amount', 'currency', 'payment_method']),
|
||||||
'withdraw_submitted_admin' => self::event('提现申请通知(管理员)', '用户 {username} 提交了提现申请。\n订单号:{withdrawal_number}\n金额:{amount}\n方式:{method}', ['username', 'withdrawal_number', 'amount', 'method']),
|
'withdraw_submitted_admin' => self::event('提现申请通知(管理员)', '用户 {username} 提交了提现申请。\n订单号:{withdrawal_number}\n金额:{amount}\n方式:{method}', ['username', 'withdrawal_number', 'amount', 'method']),
|
||||||
'post_pending_admin' => self::event('投稿待审核通知', '用户 {username} 提交了待审核内容:{title}\n查看地址:{url}', ['username', 'title', 'url']),
|
'post_pending_admin' => self::event('投稿待审核通知', '用户 {username} 提交了待审核内容:{title}\n查看地址:{url}', ['username', 'title', 'url']),
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class Settings {
|
|||||||
'jio_floating_buttons',
|
'jio_floating_buttons',
|
||||||
'jio_credit_settings',
|
'jio_credit_settings',
|
||||||
'jio_notification_settings',
|
'jio_notification_settings',
|
||||||
'jio_friend_links',
|
|
||||||
'jio_category_recommend',
|
'jio_category_recommend',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -705,21 +704,6 @@ class Settings {
|
|||||||
public static function apply_list_per_page($query): void {
|
public static function apply_list_per_page($query): void {
|
||||||
PostDisplay::apply_list_per_page($query);
|
PostDisplay::apply_list_per_page($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** 获取友链列表 */
|
|
||||||
public static function get_friend_links(): array {
|
|
||||||
return DisplayRuntime::get_friend_links();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** 保存友链 */
|
|
||||||
public static function save_friend_links(array $links): void {
|
|
||||||
DisplayRuntime::save_friend_links($links);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取微信公众号登录完整配置。 */
|
/** 获取微信公众号登录完整配置。 */
|
||||||
public static function get_weixingzh_settings(): array {
|
public static function get_weixingzh_settings(): array {
|
||||||
return Integration::get_weixingzh_settings();
|
return Integration::get_weixingzh_settings();
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
get_header();
|
get_header();
|
||||||
|
|
||||||
$links = function_exists('jio_get_friend_links') ? jio_get_friend_links() : [];
|
$post_id = (int) get_queried_object_id();
|
||||||
|
$settings = function_exists('jio_get_page_links_settings') ? jio_get_page_links_settings($post_id) : [];
|
||||||
|
$links_html = function_exists('jio_page_links_html') ? jio_page_links_html($post_id) : '';
|
||||||
|
$search_html = function_exists('jio_link_get_search_cover') ? jio_link_get_search_cover($post_id) : '';
|
||||||
|
|
||||||
if (function_exists('jio_archive_open_shell')) {
|
if (function_exists('jio_archive_open_shell')) {
|
||||||
jio_archive_open_shell([
|
jio_archive_open_shell([
|
||||||
@@ -26,39 +29,40 @@ if (function_exists('jio_archive_open_shell')) {
|
|||||||
'title' => esc_html__('友情链接', 'jio'),
|
'title' => esc_html__('友情链接', 'jio'),
|
||||||
'desc' => __('与这些优秀的站点交换友链', 'jio'),
|
'desc' => __('与这些优秀的站点交换友链', 'jio'),
|
||||||
]);
|
]);
|
||||||
|
echo $search_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns escaped markup.
|
||||||
|
|
||||||
|
if (!empty($settings['content_s']) && ($settings['content_position'] ?? 'top') === 'top') {
|
||||||
|
echo '<div class="zib-widget mb20"><article class="article content-main__body">';
|
||||||
|
the_content();
|
||||||
|
echo '</article></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $links_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns escaped markup.
|
||||||
|
|
||||||
|
if (!empty($settings['content_s']) && ($settings['content_position'] ?? 'top') !== 'top') {
|
||||||
|
echo '<div class="zib-widget mb20"><article class="article content-main__body">';
|
||||||
|
the_content();
|
||||||
|
echo '</article></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($settings['submit_s']) && !empty($settings['submit_title'])) {
|
||||||
|
printf(
|
||||||
|
'<p class="text-center" style="margin-top:24px;"><a class="padding-h10 hollow but c-theme btn-block" href="#submit-links-modal" data-toggle="modal">%s</a></p>',
|
||||||
|
esc_html((string) $settings['submit_title'])
|
||||||
|
);
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if (!empty($links)) : ?>
|
|
||||||
<div class="friends-grid">
|
|
||||||
<?php foreach ($links as $link) : ?>
|
|
||||||
<a class="friend-card" href="<?php echo esc_url($link['url'] ?? '#'); ?>" target="_blank" rel="noopener noreferrer">
|
|
||||||
<?php
|
|
||||||
$logo = $link['logo'] ?? '';
|
|
||||||
if ($logo) {
|
|
||||||
echo '<img class="friend-card__avatar" src="' . esc_url($logo) . '" alt="">';
|
|
||||||
} else {
|
|
||||||
$initial = function_exists('mb_substr') ? mb_substr((string) ($link['name'] ?? 'A'), 0, 1, 'UTF-8') : substr((string) ($link['name'] ?? 'A'), 0, 1);
|
|
||||||
echo '<div class="friend-card__avatar" style="display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--color-primary);font-family:var(--font-heading);">' . esc_html($initial) . '</div>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="friend-card__info">
|
|
||||||
<h3 class="friend-card__name"><?php echo esc_html($link['name'] ?? ''); ?></h3>
|
|
||||||
<p class="friend-card__desc"><?php echo esc_html($link['description'] ?? ''); ?></p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<div class="no-results content-stream__empty">
|
|
||||||
<p><?php esc_html_e('暂无友链,请在主题后台添加。', 'jio'); ?></p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<p class="text-center" style="margin-top:32px;">
|
|
||||||
<a class="btn-outline" href="<?php echo esc_url(home_url('/')); ?>"><?php esc_html_e('返回首页', 'jio'); ?></a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
|
if (!empty($settings['submit_s'])) {
|
||||||
|
echo jio_submit_links_modal([ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns escaped markup.
|
||||||
|
'title' => (string) ($settings['submit_title'] ?? ''),
|
||||||
|
'dec' => (string) ($settings['submit_dec'] ?? ''),
|
||||||
|
'sign' => !empty($settings['submit_sign_s']),
|
||||||
|
'cats' => (array) ($settings['submit_cats'] ?? []),
|
||||||
|
], $post_id);
|
||||||
|
}
|
||||||
|
|
||||||
if (function_exists('jio_archive_close_shell')) {
|
if (function_exists('jio_archive_close_shell')) {
|
||||||
jio_archive_close_shell(['has_sidebar' => false]);
|
jio_archive_close_shell(['has_sidebar' => false]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ test('the user center and public user home keep their legacy rounded-square avat
|
|||||||
assert.doesNotMatch(userHome, /\.jio-author-hero__avatar img\s*\{[^}]*--radius-avatar/s);
|
assert.doesNotMatch(userHome, /\.jio-author-hero__avatar img\s*\{[^}]*--radius-avatar/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('every other frontend avatar frame is circular', () => {
|
test('link page cards keep their circular logo frames', () => {
|
||||||
const friends = read('assets/css/routes/friends.css');
|
const links = read('assets/css/routes/links.css');
|
||||||
|
|
||||||
assert.match(friends, /\.friend-card__avatar\s*\{[^}]*border-radius:\s*var\(--radius-avatar,\s*50%\)/s);
|
assert.match(links, /\.links-card\.big-card \.link-img img\s*\{[^}]*border-radius:\s*100px/s);
|
||||||
assert.doesNotMatch(friends, /\.friend-card__avatar\s*\{[^}]*border-radius:\s*var\(--radius\)/s);
|
assert.match(links, /\.link-img\s*\{[^}]*width:\s*var\(--this-size\)/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the user card name row can shrink once the VIP icon joins the badge row', () => {
|
test('the user card name row can shrink once the VIP icon joins the badge row', () => {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const test = require('node:test')
|
||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..')
|
||||||
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||||
|
|
||||||
|
test('native links are the only friend-link store', () => {
|
||||||
|
const setup = read('src/Core/Setup.php')
|
||||||
|
const submissions = read('src/Core/LinksSubmission.php')
|
||||||
|
const template = read('templates/pages/page-friends.php')
|
||||||
|
const helper = read('inc/helpers/jio-links.php')
|
||||||
|
|
||||||
|
assert.match(setup, /pre_option_link_manager_enabled/)
|
||||||
|
assert.match(submissions, /ACTION = 'jio_frontend_links_submit'/)
|
||||||
|
assert.match(submissions, /add_action\('wp_ajax_' \. self::ACTION/)
|
||||||
|
assert.match(submissions, /add_action\('wp_ajax_nopriv_' \. self::ACTION/)
|
||||||
|
assert.match(submissions, /wp_insert_link\(/)
|
||||||
|
assert.match(submissions, /jio_require_captcha\('friend_link'/)
|
||||||
|
assert.match(submissions, /jio_captcha_rate_limited\('friend-link-submit'/)
|
||||||
|
assert.match(template, /jio_page_links_html\(/)
|
||||||
|
assert.match(template, /jio_submit_links_modal\(/)
|
||||||
|
assert.match(helper, /get_bookmarks\(/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the retired friend-link option and admin REST surface stay gone', () => {
|
||||||
|
for (const file of [
|
||||||
|
'src/Settings/DisplayRuntime.php',
|
||||||
|
'assets/css/routes/friends.css',
|
||||||
|
]) {
|
||||||
|
assert.equal(fs.existsSync(path.join(root, file)), false, file)
|
||||||
|
}
|
||||||
|
for (const file of [
|
||||||
|
'src/Admin/GlobalController.php',
|
||||||
|
'src/Settings/Settings.php',
|
||||||
|
'src/Settings/DisplayPageGroups.php',
|
||||||
|
'inc/helpers/template-tags/media-brand.php',
|
||||||
|
'assets/admin/src/api/modules/global.js',
|
||||||
|
]) {
|
||||||
|
const source = read(file)
|
||||||
|
assert.doesNotMatch(source, /jio_friend_links|friend-links|get_friend_links|save_friend_links/, file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('page options and submission wiring point at the links template', () => {
|
||||||
|
const metaBox = read('src/Admin/PageLinksMetaBox.php')
|
||||||
|
const scripts = read('src/Core/ScriptsPart1b.php')
|
||||||
|
const captcha = read('inc/modules/captcha.php')
|
||||||
|
const notifications = read('src/Settings/MailNotifications.php')
|
||||||
|
const templates = read('src/Settings/MailTemplates.php')
|
||||||
|
|
||||||
|
assert.match(metaBox, /templates\/pages\/page-friends\.php/)
|
||||||
|
assert.match(metaBox, /wp_verify_nonce/)
|
||||||
|
assert.match(metaBox, /current_user_can\('edit_page'/)
|
||||||
|
assert.match(metaBox, /page_links_submit_s/)
|
||||||
|
assert.match(scripts, /links-submit\.js/)
|
||||||
|
assert.match(captcha, /'friend_link'\s*=>/)
|
||||||
|
assert.match(notifications, /links_submit_admin/)
|
||||||
|
assert.match(notifications, /Message::get_instance\(\)->send/)
|
||||||
|
assert.match(templates, /review_url/)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user