The "other login methods" block drifted from zibll on three axes. Gitee was mapped to fa-code-branch because FA5 Free ships no Gitee brand glyph, so the button showed a generic branch icon; Google showed the monochrome FA glyph instead of the reference's multi-colour mark. The modal also printed an implicit agreement line under the social buttons, which the reference guest card does not have. And the top-nav user popover built its own .social-login-divider / .oauth-row / .oauth-btn markup and then wrapped that in a second separator and .social_loginbar, so the block rendered nested and unlike the reference. aksrt_brand_icon() now routes zibsvg-/customsvg- values through the shared icon renderer, and the brand map resolves gitee to zibsvg-gitee and google to zibsvg-google-color. Both sprite keys already exist in the runtime symbol sheet, which the loader pulls in on every frontend page. Because get_auth_providers_data() and binding_row() both go through aksrt_brand_icon(), one map change fixes the icon on the modal, the user card, the top-nav popover, the user-center binding list and the profile social links. render_social_buttons() is rewritten to the reference shape: an optional .social-separator, a .social_loginbar, and one .social-login-item <type> toggle-radius anchor per enabled provider with rel="nofollow" and a "<label>登录" title. oauth_button_lg switches to the button-lg variant with a label, and the WeChat entry carries qrcode-signin so auth.js opens the QR panel instead of navigating, matching the reference. The navbar and the link apply modal now append the block verbatim instead of re-wrapping it, and the user-card loop adopts the same item attributes. The retired .social-login-divider / .oauth-row / .oauth-btn rules are dropped from base-1.css, and the binding-list icon rule gains an svg selector so the sprite icons keep the 22px the FA glyphs had. Verification: node --test across widget-system, navigation, admin, asset-ledger, dev, front-submit and visual (613 tests), plus php -l on every changed PHP file. Extended the auth source contract with the agreement-line, brand-icon and popover-structure cases, and pinned the user-card guest markup.
472 lines
16 KiB
PHP
472 lines
16 KiB
PHP
<?php
|
||
/**
|
||
* AKSRT Theme — Brand / contact icons (Font Awesome 5)
|
||
*
|
||
* Short-key → FA5 class map, escaped <i> markup, contact option labels,
|
||
* and public social-link renderer. Local SVG inventory removed.
|
||
*
|
||
* @package AKSRT
|
||
* @version 3.4.0
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
// ============================================================
|
||
// PHP 7.x 兼容辅助函数(替代 PHP 8.0+ 的 match 表达式)
|
||
// ============================================================
|
||
|
||
if (!function_exists('aksrt_parse_country_list')):
|
||
/**
|
||
* 解析 GeoIP 屏蔽国家列表字符串为数组
|
||
*
|
||
* @param string $raw 逗号分隔的国家代码
|
||
* @return array 大写国家代码数组
|
||
*/
|
||
function aksrt_parse_country_list($raw) {
|
||
if (empty($raw)) return array();
|
||
$parts = explode(',', wp_unslash($raw));
|
||
$result = array();
|
||
foreach ($parts as $p) {
|
||
$trimmed = trim($p);
|
||
if ($trimmed !== '') {
|
||
$result[] = strtoupper($trimmed);
|
||
}
|
||
}
|
||
return array_values(array_filter($result));
|
||
}
|
||
endif;
|
||
|
||
// ============================================================
|
||
// Brand icon map (FA 5.15.4)
|
||
// ============================================================
|
||
|
||
if (!function_exists('aksrt_brand_icon_map')) :
|
||
/**
|
||
* Short key → FA5 class string or built-in SVG sprite key (allowlisted).
|
||
*
|
||
* @return array<string, string>
|
||
*/
|
||
function aksrt_brand_icon_map(): array {
|
||
static $map = null;
|
||
if ($map !== null) {
|
||
return $map;
|
||
}
|
||
|
||
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
|
||
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
|
||
// a solid stand-in; every other key stays on FA5.
|
||
$map = [
|
||
'github' => 'fa fab fa-github',
|
||
'google' => 'zibsvg-google-color',
|
||
'x' => 'fa fab fa-twitter',
|
||
'twitter' => 'fa fab fa-twitter',
|
||
'email' => 'fa fas fa-envelope',
|
||
'envelope' => 'fa fas fa-envelope',
|
||
'wechat' => 'fa fab fa-weixin',
|
||
'weixin' => 'fa fab fa-weixin',
|
||
'qq' => 'fa fab fa-qq',
|
||
'weibo' => 'fa fab fa-weibo',
|
||
'telegram' => 'fa fab fa-telegram-plane',
|
||
'discord' => 'fa fab fa-discord',
|
||
'bilibili' => 'fa fas fa-tv',
|
||
'linkedin' => 'fa fab fa-linkedin',
|
||
'facebook' => 'fa fab fa-facebook-f',
|
||
'youtube' => 'fa fab fa-youtube',
|
||
'instagram' => 'fa fab fa-instagram',
|
||
'web' => 'fa fas fa-globe',
|
||
'globe' => 'fa fas fa-globe',
|
||
'gitee' => 'zibsvg-gitee',
|
||
'baidu' => 'fa fas fa-paw',
|
||
'alipay' => 'fa fab fa-alipay',
|
||
];
|
||
|
||
return $map;
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_normalize_icon_key')) :
|
||
/**
|
||
* Normalize short key or full FA class string to a short map key.
|
||
*
|
||
* Examples: 'fa fab fa-github' | 'fa-github' | 'github' | 'weixin' → 'github' | 'wechat'
|
||
*
|
||
* Legacy FA4 values (a bare `fa` token followed by `fa-<glyph>`) are stripped too.
|
||
*
|
||
* @param string $name Raw icon key or FA class string.
|
||
* @return string Short key (may be empty).
|
||
*/
|
||
function aksrt_normalize_icon_key(string $name): string {
|
||
$name = trim($name);
|
||
if ($name === '') {
|
||
return '';
|
||
}
|
||
|
||
// Strip the FA5 layout/style prefixes, the glyph prefix and the legacy FA4
|
||
// double-`fa` form (a bare `fa` token followed by `fa-<glyph>`). The site
|
||
// ships FA5 only, but stored values may predate it.
|
||
$name = preg_replace('/^fa\s+(?:fas|far|fab)\s+/i', '', $name);
|
||
$name = preg_replace('/^fa\s+fa-/i', '', (string) $name);
|
||
$name = preg_replace('/^(?:fas|far|fab)\s+/i', '', (string) $name);
|
||
$name = preg_replace('/^fa-/i', '', (string) $name);
|
||
$name = trim((string) $name, "- \t\n\r\0\x0B");
|
||
$name = strtolower($name);
|
||
|
||
// Aliases → canonical keys used by the map / contact options.
|
||
$aliases = [
|
||
'weixin' => 'wechat',
|
||
'weixingzh' => 'wechat',
|
||
'twitter' => 'x',
|
||
'envelope' => 'email',
|
||
'globe' => 'web',
|
||
];
|
||
if (isset($aliases[$name])) {
|
||
return $aliases[$name];
|
||
}
|
||
|
||
return $name;
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_brand_icon_class')) :
|
||
/**
|
||
* Resolve a short key, FA class string or sprite key to its icon value.
|
||
*
|
||
* @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
|
||
* @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
|
||
*/
|
||
function aksrt_brand_icon_class(string $name): string {
|
||
$raw = trim($name);
|
||
if ($raw === '') {
|
||
return '';
|
||
}
|
||
|
||
// Built-in SVG sprite and inline SVG values pass straight through; they are
|
||
// not FA class strings and must survive to the markup renderer untouched.
|
||
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
|
||
return $raw;
|
||
}
|
||
|
||
// Pass through already-valid full FA class strings (footer contact_more).
|
||
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
|
||
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
|
||
$key = aksrt_normalize_icon_key($raw);
|
||
$map = aksrt_brand_icon_map();
|
||
if ($key !== '' && isset($map[$key])) {
|
||
return $map[$key];
|
||
}
|
||
// Allowlisted style + glyph only — collapse whitespace, lower style prefix.
|
||
$parts = preg_split('/\s+/', strtolower($raw));
|
||
$parts = array_values(array_filter((array) $parts));
|
||
return implode(' ', $parts);
|
||
}
|
||
|
||
$key = aksrt_normalize_icon_key($raw);
|
||
if ($key === '') {
|
||
return '';
|
||
}
|
||
|
||
$map = aksrt_brand_icon_map();
|
||
return $map[$key] ?? '';
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_brand_icon')) :
|
||
/**
|
||
* Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
|
||
*
|
||
* @param string $name Short key, FA class string or zibsvg-/customsvg- value.
|
||
* @param array|string $extra_classes Optional extra classes.
|
||
* @return string Safe HTML or empty string when unknown.
|
||
*/
|
||
function aksrt_brand_icon(string $name, $extra_classes = []): string {
|
||
$class = aksrt_brand_icon_class($name);
|
||
|
||
// Sprite and inline-SVG values carry their own element, so the shared icon
|
||
// renderer owns the markup (it emits <svg class="icon"><use …>).
|
||
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
|
||
return aksrt_icon_markup($class, $extra_classes);
|
||
}
|
||
|
||
if ($class === '') {
|
||
return '';
|
||
}
|
||
|
||
if (is_string($extra_classes) && $extra_classes !== '') {
|
||
$extra_classes = preg_split('/\s+/', $extra_classes) ?: [];
|
||
}
|
||
if (is_array($extra_classes) && $extra_classes !== []) {
|
||
$extra = array_filter(array_map('sanitize_html_class', $extra_classes));
|
||
if ($extra !== []) {
|
||
$class .= ' ' . implode(' ', $extra);
|
||
}
|
||
}
|
||
|
||
return '<i class="' . esc_attr($class) . '" aria-hidden="true"></i>';
|
||
}
|
||
endif;
|
||
|
||
// ============================================================
|
||
// 图标 key → 显示名称
|
||
// ============================================================
|
||
if (!function_exists('aksrt_icon_key_to_name')) :
|
||
function aksrt_icon_key_to_name($key) {
|
||
$key = aksrt_normalize_icon_key((string) $key);
|
||
$map = array(
|
||
'github' => 'GitHub', 'google' => 'Google', 'x' => 'X',
|
||
'email' => '邮箱', 'wechat' => '微信', 'qq' => 'QQ',
|
||
'weibo' => '微博', 'telegram' => 'Telegram',
|
||
'discord' => 'Discord', 'bilibili' => 'Bilibili',
|
||
'linkedin' => 'LinkedIn', 'facebook' => 'Facebook',
|
||
'youtube' => 'YouTube', 'instagram' => 'Instagram',
|
||
'web' => '网站',
|
||
'gitee' => 'Gitee',
|
||
'baidu' => 'Baidu',
|
||
'alipay' => 'Alipay',
|
||
);
|
||
return isset($map[$key]) ? $map[$key] : ucwords(str_replace('-', ' ', $key));
|
||
}
|
||
endif;
|
||
|
||
// ============================================================
|
||
// 联系方式图标选项(仅保留选定的 15 个)
|
||
// ============================================================
|
||
if (!function_exists('aksrt_contact_icon_options')) :
|
||
function aksrt_contact_icon_options($selected = '') {
|
||
$icons = array(
|
||
'github' => 'GitHub',
|
||
'google' => 'Google',
|
||
'x' => 'X',
|
||
'email' => '邮箱',
|
||
'wechat' => '微信',
|
||
'qq' => 'QQ',
|
||
'weibo' => '微博',
|
||
'telegram' => 'Telegram',
|
||
'discord' => 'Discord',
|
||
'bilibili' => 'Bilibili',
|
||
'linkedin' => 'LinkedIn',
|
||
'facebook' => 'Facebook',
|
||
'youtube' => 'YouTube',
|
||
'instagram' => 'Instagram',
|
||
'web' => '网站',
|
||
);
|
||
$selected = aksrt_normalize_icon_key((string) $selected);
|
||
$html = '<option value="">—</option>';
|
||
foreach ($icons as $key => $label) {
|
||
$sel = $key === $selected ? ' selected' : '';
|
||
$html .= '<option value="' . esc_attr($key) . '"' . $sel . '>' . esc_html($label) . '</option>';
|
||
}
|
||
return $html;
|
||
}
|
||
endif;
|
||
|
||
// ============================================================
|
||
// 统一图标渲染(FA5 / 内置 SVG / 自定义 SVG)
|
||
// ============================================================
|
||
if (!function_exists('aksrt_enqueue_fontawesome')) :
|
||
/**
|
||
* Register (once) and enqueue the bundled Font Awesome 5 pack under the
|
||
* single canonical handle. Every context — public, admin screens, widgets,
|
||
* the icon picker — must go through this helper.
|
||
*
|
||
* @param array $deps Style dependencies for the first caller.
|
||
* @return string The canonical handle.
|
||
*/
|
||
function aksrt_enqueue_fontawesome(array $deps = []): string {
|
||
if (!wp_style_is('aksrt-font-awesome', 'registered')) {
|
||
wp_register_style('aksrt-font-awesome', AKSRT_URI . '/assets/fontawesome5/css/all.min.css', $deps, '5.15.4');
|
||
}
|
||
wp_enqueue_style('aksrt-font-awesome');
|
||
return 'aksrt-font-awesome';
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_svg_kses_tags')) :
|
||
/**
|
||
* Allowed SVG tags/attributes for user-supplied custom SVG icon markup.
|
||
*
|
||
* @return array<string, array<string, bool>>
|
||
*/
|
||
function aksrt_svg_kses_tags(): array {
|
||
static $tags = null;
|
||
if ($tags !== null) {
|
||
return $tags;
|
||
}
|
||
|
||
// Shape list mirrors the admin picker's cleanSvg() accept test — every
|
||
// element it lets through must survive the kses pass, otherwise a pasted
|
||
// <circle>/<rect>/… SVG is silently saved without its shapes.
|
||
$shape_attrs = ['fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true];
|
||
|
||
$tags = [
|
||
'svg' => ['class' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'xmlns' => true, 'aria-hidden' => true],
|
||
'use' => ['href' => true, 'xlink:href' => true],
|
||
'path' => ['d' => true] + $shape_attrs,
|
||
'g' => ['fill' => true, 'transform' => true],
|
||
'circle' => ['cx' => true, 'cy' => true, 'r' => true] + $shape_attrs,
|
||
'ellipse' => ['cx' => true, 'cy' => true, 'rx' => true, 'ry' => true] + $shape_attrs,
|
||
'rect' => ['x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true] + $shape_attrs,
|
||
'polygon' => ['points' => true] + $shape_attrs,
|
||
'line' => ['x1' => true, 'y1' => true, 'x2' => true, 'y2' => true] + $shape_attrs,
|
||
];
|
||
return $tags;
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_icon_markup')) :
|
||
/**
|
||
* Render an icon-picker value of any supported format.
|
||
*
|
||
* Supported values:
|
||
* - "fa fas fa-star" / "fa fab fa-weixin" — FA5 class string
|
||
* - "fa-star" — bare glyph, rendered as solid FA5
|
||
* - legacy FA4 string — double-`fa` form, upgraded to FA5
|
||
* - "zibsvg-hot" — theme built-in SVG sprite key
|
||
* - "customsvg-<svg …>" — inline user SVG markup
|
||
*
|
||
* Output is always FA5 — the legacy FA4 double-`fa` prefix is never emitted.
|
||
*
|
||
* @param string $value Raw stored value.
|
||
* @param array|string $extra_classes Extra classes for the rendered element.
|
||
* @return string Safe HTML, empty string for empty values.
|
||
*/
|
||
function aksrt_icon_markup(string $value, $extra_classes = []): string {
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
$extra = '';
|
||
if (is_string($extra_classes) && $extra_classes !== '') {
|
||
$extra_classes = preg_split('/\s+/', $extra_classes) ?: [];
|
||
}
|
||
if (is_array($extra_classes)) {
|
||
$clean = array_filter(array_map('sanitize_html_class', $extra_classes));
|
||
if ($clean !== []) {
|
||
$extra = ' ' . implode(' ', $clean);
|
||
}
|
||
}
|
||
|
||
if (str_starts_with($value, 'customsvg-')) {
|
||
$svg = wp_kses(substr($value, 10), aksrt_svg_kses_tags());
|
||
if ($svg === '') {
|
||
return '';
|
||
}
|
||
if ($extra !== '' && stripos($svg, '<svg') === 0 && stripos($svg, 'class=') === false) {
|
||
$svg = preg_replace('/<svg\b/i', '<svg class="' . ltrim($extra) . '"', $svg, 1);
|
||
}
|
||
return $svg;
|
||
}
|
||
|
||
if (str_starts_with($value, 'zibsvg-')) {
|
||
$key = sanitize_key(substr($value, 7));
|
||
if ($key === '') {
|
||
return '';
|
||
}
|
||
$class = $extra !== '' ? ltrim($extra) : 'icon';
|
||
return '<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-' . $key . '"></use></svg>';
|
||
}
|
||
|
||
// 本站统一 FA5 输出:遗留 FA4 双 fa 前缀、裸字形 "fa-xxx" 与缺少 fa 前缀的
|
||
// "fas/far/fab fa-xxx" 均归一为 "fa fas fa-xxx"(末尾附加类名原样保留)。
|
||
if (preg_match('/^fa\s+fa-/i', $value)) {
|
||
$value = preg_replace('/^fa\s+/i', 'fa fas ', $value, 1);
|
||
} elseif (preg_match('/^fa-/i', $value)) {
|
||
$value = 'fa fas ' . $value;
|
||
} elseif (preg_match('/^(?:fas|far|fab)\s+fa-/i', $value)) {
|
||
$value = 'fa ' . $value;
|
||
}
|
||
return '<i class="' . esc_attr($value . $extra) . '" aria-hidden="true"></i>';
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_sanitize_icon_value')) :
|
||
/**
|
||
* Sanitize an icon-picker value. Keeps the SVG markup of customsvg- values
|
||
* (plain sanitize_text_field would strip the tags), filters FA/zip keys.
|
||
*
|
||
* @param string $value Raw value from a form/REST payload.
|
||
* @return string Sanitized value.
|
||
*/
|
||
function aksrt_sanitize_icon_value(string $value): string {
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
if (str_starts_with($value, 'customsvg-')) {
|
||
$svg = wp_kses(substr($value, 10), aksrt_svg_kses_tags());
|
||
return $svg === '' ? '' : 'customsvg-' . $svg;
|
||
}
|
||
|
||
return sanitize_text_field($value);
|
||
}
|
||
endif;
|
||
|
||
if (!function_exists('aksrt_empty_state_svgs')) :
|
||
/**
|
||
* Shared empty-state SVG icons (24px stroke style, not sprite keys).
|
||
*
|
||
* @return array{post:string,comment:string,favorite:string,follow:string}
|
||
*/
|
||
function aksrt_empty_state_svgs(): array {
|
||
return [
|
||
'post' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6M9 9h1"/></svg>',
|
||
'comment' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||
'favorite' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
|
||
'follow' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
|
||
];
|
||
}
|
||
endif;
|
||
|
||
// ============================================================
|
||
// Public social links (author hero / profile)
|
||
// ============================================================
|
||
if (!function_exists('aksrt_render_social_links')) :
|
||
/**
|
||
* Echo a list of public contact links with FA icons.
|
||
*
|
||
* @param array<int, array{name?:string,url?:string,icon?:string,title?:string,label?:string}> $contacts
|
||
*/
|
||
function aksrt_render_social_links(array $contacts): void {
|
||
$links = [];
|
||
foreach ($contacts as $contact) {
|
||
if (!is_array($contact)) {
|
||
continue;
|
||
}
|
||
$url = isset($contact['url']) ? (string) $contact['url'] : '';
|
||
$icon = isset($contact['icon']) ? (string) $contact['icon'] : '';
|
||
if ($url === '' || $icon === '') {
|
||
continue;
|
||
}
|
||
$icon_html = aksrt_brand_icon($icon);
|
||
if ($icon_html === '') {
|
||
$icon_html = '<i class="fa fas fa-globe" aria-hidden="true"></i>';
|
||
}
|
||
$label = (string) ($contact['name'] ?? $contact['title'] ?? $contact['label'] ?? '');
|
||
if ($label === '') {
|
||
$label = aksrt_icon_key_to_name($icon);
|
||
}
|
||
$links[] = [
|
||
'url' => $url,
|
||
'label' => $label,
|
||
'icon' => $icon_html,
|
||
];
|
||
}
|
||
|
||
if ($links === []) {
|
||
return;
|
||
}
|
||
|
||
echo '<div class="profile-social-links">';
|
||
foreach ($links as $link) {
|
||
printf(
|
||
'<a class="social-icon-link" href="%1$s" target="_blank" rel="noopener noreferrer" title="%2$s" aria-label="%2$s">%3$s</a>',
|
||
esc_url($link['url']),
|
||
esc_attr($link['label']),
|
||
$link['icon'] // pre-escaped via aksrt_brand_icon / fixed globe fallback
|
||
);
|
||
}
|
||
echo '</div>';
|
||
}
|
||
endif;
|