diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..fb01efc --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,341 @@ + +.wpavatar-settings .form-table th { + width: 220px; +} + +.wpavatar-settings .description { + font-size: 13px; + margin: 2px 0 5px; + color: #666; +} + +.wpavatar-tabs-wrapper { + margin-bottom: 20px; +} + +.wpavatar-sync-tabs { + display: flex; + flex-wrap: wrap; + gap: 5px; + border-bottom: 1px solid #c3c4c7; + margin-bottom: 20px; +} + +.wpavatar-tab { + padding: 8px 16px; + border: none; + background: none; + cursor: pointer; + font-size: 14px; + border-bottom: 2px solid transparent; +} + +.wpavatar-tab.active { + border-bottom: 2px solid #007cba; + font-weight: 600; + background: #f0f0f1; +} + +.wpavatar-tab:hover:not(.active) { + background: #f0f0f1; + border-bottom-color: #dcdcde; +} + +.wpavatar-card { + background: #fff; + border: 1px solid #ccd0d4; + border-radius: 4px; + margin-top: 20px; + padding: 20px; +} + +.wpavatar-card h2 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #f0f0f1; +} + +.wpavatar-section-desc { + color: #666; + margin-bottom: 20px; +} + +.wpavatar-stats-card { + background: #f9f9f9; + border-radius: 4px; + padding: 15px; + margin-bottom: 20px; +} + +.wpavatar-stats-card h3 { + margin-top: 0; + margin-bottom: 15px; +} + +.cache-stats-wrapper { + margin-top: 15px; + border-left: 4px solid #2271b1; + background: #f0f6fc; + padding: 12px; + margin-bottom: 15px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.cache-stats p { + margin: 0.5em 0; +} + +.cache-stats .error { + background: #ffebee; + padding: 10px; + border-left: 4px solid #d32f2f; +} + +.wpavatar-action-buttons { + display: flex; + gap: 10px; + margin-top: 15px; +} + +.wpavatar-submit-wrapper { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #f0f0f1; +} + +.wpavatar-switch { + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; +} + +.wpavatar-switch input { + opacity: 0; + width: 0px; + height: 0; + min-width: 0; + margin: -.25rem -0.15rem 0 0; +} + +.wpavatar-slider { + position: relative; + display: inline-block; + width: 40px; + height: 20px; + background-color: #ccc; + border-radius: 34px; + transition: .4s; +} + +.wpavatar-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + border-radius: 50%; + transition: .4s; +} + +.wpavatar-switch input:checked + .wpavatar-slider { + background-color: #2196F3; +} + +.wpavatar-switch input:focus + .wpavatar-slider { + box-shadow: 0 0 1px #2196F3; +} + +.wpavatar-switch input:checked + .wpavatar-slider:before { + transform: translateX(20px); +} + +.wpavatar-switch-label { + margin-left: 10px; +} + +.wpavatar-radio { + display: inline-flex; + align-items: center; + margin-bottom: 8px; +} + +.wpavatar-radio-label { + margin-left: 8px; +} + +.wpavatar-input { + border: 1px solid #ddd; + padding: 6px 10px; + border-radius: 4px; +} + +.wpavatar-input:focus { + border-color: #2271b1; + box-shadow: 0 0 0 1px #2271b1; + outline: none; +} + +.wpavatar-select { + border: 1px solid #ddd; + padding: 6px 10px; + border-radius: 4px; + height: 36px; + min-width: 240px; +} + +.wpavatar-select:focus { + border-color: #2271b1; + box-shadow: 0 0 0 1px #2271b1; + outline: none; +} + +.shortcode-docs { + margin-top: 20px; + background: #f9f9f9; + border: none; + border-radius: 4px; +} + +.wpavatar-table-wrapper { + overflow-x: auto; +} + +.wpavatar-table { + border-collapse: collapse; + width: 100%; + margin-bottom: 20px; + border: 1px solid #e2e4e7!important; +} + +.wpavatar-table th { + background-color: #f6f7f7; + text-align: left; + padding: 10px; + border-bottom: 1px solid #e2e4e7; +} + +.wpavatar-table td { + padding: 10px; + vertical-align: top; + border-bottom: 1px solid #f0f0f1; +} + +.wpavatar-table code { + background: #f0f0f1; + padding: 3px 5px; + border-radius: 3px; + font-size: 13px; +} + +.wpavatar-table ul { + margin: 0; + padding-left: 20px; +} + +.notice { + padding: 8px 12px; + border-radius: 3px; +} + +.notice-success { + background-color: #dff0d8; + border-left: 4px solid #46b450; + padding: 1px 12px; +} + +.notice-error { + background-color: #f2dede; + border-left: 4px solid #dc3232; +} + +.wpavatar-preview-container { + background: #f9f9f9; + border-radius: 4px; + padding: 15px; + margin-bottom: 20px; +} + +.wpavatar-preview-wrapper { + display: flex; + flex-wrap: wrap; + gap: 30px; + margin: 15px 0; +} + +.wpavatar-preview-item { + text-align: center; +} + +.wpavatar-preview-item h4 { + margin-bottom: 10px; + font-weight: normal; +} + +.avatar-circle { + border-radius: 50% !important; + overflow: hidden; +} + +.avatar-rounded { + border-radius: 8px !important; + overflow: hidden; +} + +.default-avatar-options { + display: flex; + flex-wrap: wrap; + gap: 15px; + margin-bottom: 15px; +} + +.default-avatar-options label { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + padding: 8px; + border: 2px solid transparent; + border-radius: 5px; + width: 80px; + text-align: center; +} + +.default-avatar-options label:hover { + background-color: #f0f0f1; +} + +.default-avatar-options input[type="radio"] { + margin-bottom: 8px; +} + +.default-avatar-options input[type="radio"]:checked + img { + border: 2px solid #2271b1; + border-radius: 50%; +} + +.avatar-option-name { + margin-top: 5px; + font-size: 12px; + line-height: 1.3; +} + +@media (max-width: 782px) { + .wpavatar-settings .form-table th { + width: 100%; + } + + .wpavatar-action-buttons { + flex-direction: column; + } + + .default-avatar-options { + gap: 10px; + } + + .wpavatar-preview-wrapper { + justify-content: center; + } +} diff --git a/assets/images/cravatar-default.png b/assets/images/cravatar-default.png new file mode 100644 index 0000000..270a6f1 Binary files /dev/null and b/assets/images/cravatar-default.png differ diff --git a/assets/images/cravatar-logo.png b/assets/images/cravatar-logo.png new file mode 100644 index 0000000..cef123d Binary files /dev/null and b/assets/images/cravatar-logo.png differ diff --git a/assets/images/default-avatar.png b/assets/images/default-avatar.png new file mode 100644 index 0000000..0b60a28 Binary files /dev/null and b/assets/images/default-avatar.png differ diff --git a/assets/images/placeholder-avatar.png b/assets/images/placeholder-avatar.png new file mode 100644 index 0000000..0b60a28 Binary files /dev/null and b/assets/images/placeholder-avatar.png differ diff --git a/assets/images/wapuu-china.png b/assets/images/wapuu-china.png new file mode 100644 index 0000000..b50809a Binary files /dev/null and b/assets/images/wapuu-china.png differ diff --git a/assets/images/wapuu.png b/assets/images/wapuu.png new file mode 100644 index 0000000..a9e80ad Binary files /dev/null and b/assets/images/wapuu.png differ diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..fda06b3 --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,234 @@ +jQuery(document).ready(function($) { + if (typeof wpavatar === 'undefined') { + console.error('WPAvatar admin script error: wpavatar object not defined'); + return; + } + + if (typeof wpavatar_l10n === 'undefined') { + console.error('WPAvatar admin script error: wpavatar_l10n object not defined'); + return; + } + + $('.wpavatar-tab').on('click', function() { + var tab = $(this).data('tab'); + if (!tab) return; + + $('.wpavatar-tab').removeClass('active'); + $(this).addClass('active'); + + $('.wpavatar-section').hide(); + $('#wpavatar-section-' + tab).show(); + + if (tab === 'cache' && $('#cache-stats').is(':empty')) { + setTimeout(function() { + $('#check-cache').trigger('click'); + }, 300); + } + + if (window.history && window.history.pushState) { + var url = new URL(window.location.href); + url.searchParams.set('tab', tab); + window.history.pushState({}, '', url); + } + }); + + function updateCdnOptions() { + var selectedType = $('input[name="wpavatar_cdn_type"]:checked').val(); + + $('.cdn-option').hide(); + + if (selectedType === 'cravatar_route') { + $('.cravatar-route-option').show(); + forceMd5HashMethod(true); + } else if (selectedType === 'third_party') { + $('.third-party-option').show(); + checkIfCravatarRelated($('select[name="wpavatar_third_party_mirror"]').val()); + } else if (selectedType === 'custom') { + $('.custom-cdn-option').show(); + checkIfCravatarRelated($('input[name="wpavatar_custom_cdn"]').val()); + } + } + + function checkIfCravatarRelated(value) { + if (value && value.toLowerCase().indexOf('cravatar') !== -1) { + forceMd5HashMethod(true); + } else { + // 非Cravatar服务默认选择SHA256,但不强制 + var currentHashMethod = $('input[name="wpavatar_hash_method"]:checked').val(); + if (!currentHashMethod) { + $('input[name="wpavatar_hash_method"][value="sha256"]').prop('checked', true); + } + forceMd5HashMethod(false); + } + } + + function forceMd5HashMethod(force) { + if (force) { + $('input[name="wpavatar_hash_method"][value="md5"]').prop('checked', true); + $('input[name="wpavatar_hash_method"][value="sha256"]').prop('disabled', true); + $('.hash-method-notice').show(); + } else { + $('input[name="wpavatar_hash_method"][value="sha256"]').prop('disabled', false); + $('.hash-method-notice').hide(); + } + } + + $('input[name="wpavatar_cdn_type"]').on('change', updateCdnOptions); + + updateCdnOptions(); + + $('select[name="wpavatar_third_party_mirror"]').on('change', function() { + checkIfCravatarRelated($(this).val()); + }); + + $('input[name="wpavatar_custom_cdn"]').on('input', function() { + checkIfCravatarRelated($(this).val()); + }); + + $('#check-cache').on('click', function() { + var $button = $(this); + var $stats = $('#cache-stats'); + + $button.prop('disabled', true).text(wpavatar_l10n.checking); + $stats.html('

' + wpavatar_l10n.checking_status + '

'); + + $.ajax({ + type: 'POST', + url: wpavatar.ajaxurl, + data: { + action: 'wpavatar_check_cache', + nonce: wpavatar.nonce + }, + success: function(response) { + if (response.success) { + $stats.html(response.data); + } else { + $stats.html('

' + (response.data || wpavatar_l10n.check_failed) + '

'); + } + }, + error: function() { + $stats.html('

' + wpavatar_l10n.request_failed + '

'); + }, + complete: function() { + $button.prop('disabled', false).text(wpavatar_l10n.check_cache); + } + }); + }); + + $('#purge-cache').on('click', function() { + var $button = $(this); + var $stats = $('#cache-stats'); + var $status = $('#wpavatar-status'); + + if (!confirm(wpavatar_l10n.confirm_purge)) { + return; + } + + $button.prop('disabled', true).text(wpavatar_l10n.purging); + $stats.html('

' + wpavatar_l10n.purging_cache + '

'); + + $.ajax({ + type: 'POST', + url: wpavatar.ajaxurl, + data: { + action: 'wpavatar_purge_cache', + nonce: wpavatar.nonce + }, + success: function(response) { + if (response.success) { + $status.removeClass('notice-error') + .addClass('notice-success') + .text(response.data) + .show() + .delay(3000) + .fadeOut(); + + setTimeout(function() { + $('#check-cache').trigger('click'); + }, 1000); + } else { + $status.removeClass('notice-success') + .addClass('notice-error') + .text(response.data || wpavatar_l10n.purge_failed) + .show(); + } + }, + error: function() { + $status.removeClass('notice-success') + .addClass('notice-error') + .text(wpavatar_l10n.request_failed) + .show(); + }, + complete: function() { + $button.prop('disabled', false).text(wpavatar_l10n.purge_cache); + } + }); + }); + + $('#wpavatar-basic-form, #wpavatar-cache-form, #wpavatar-advanced-form, #wpavatar-shortcodes-form').on('submit', function(e) { + var formId = $(this).attr('id'); + var $status = $('#wpavatar-status'); + var isValid = true; + + if (formId === 'wpavatar-basic-form' && $('input[name="wpavatar_cdn_type"]:checked').val() === 'custom') { + var customCdn = $('input[name="wpavatar_custom_cdn"]').val().trim(); + if (!customCdn) { + $status.removeClass('notice-success') + .addClass('notice-error') + .text(wpavatar_l10n.enter_custom_cdn) + .show(); + $('input[name="wpavatar_custom_cdn"]').focus(); + isValid = false; + } + } + + if (formId === 'wpavatar-cache-form' && $('input[name="wpavatar_enable_cache"]:checked').length > 0) { + var cachePath = $('input[name="wpavatar_cache_path"]').val().trim(); + if (!cachePath) { + $status.removeClass('notice-success') + .addClass('notice-error') + .text(wpavatar_l10n.enter_cache_path) + .show(); + $('input[name="wpavatar_cache_path"]').focus(); + isValid = false; + } + } + + if (!isValid) { + e.preventDefault(); + return false; + } + + return true; + }); + + if (window.location.search.indexOf('settings-updated=true') > -1) { + $('#wpavatar-status') + .removeClass('notice-error') + .addClass('notice-success') + .text(wpavatar_l10n.settings_saved) + .show() + .delay(3000) + .fadeOut(); + } + + var currentTab = ''; + + if (window.location.search.indexOf('tab=') > -1) { + var urlParams = new URLSearchParams(window.location.search); + currentTab = urlParams.get('tab'); + } + + if (!currentTab) { + var $activeTab = $('.wpavatar-tab.active'); + if ($activeTab.length) { + currentTab = $activeTab.data('tab'); + } + } + + if (currentTab) { + $('.wpavatar-tab[data-tab="' + currentTab + '"]').trigger('click'); + } else { + $('.wpavatar-tab:first').trigger('click'); + } +}); diff --git a/includes/admin.php b/includes/admin.php new file mode 100644 index 0000000..ab1a9a7 --- /dev/null +++ b/includes/admin.php @@ -0,0 +1,708 @@ + 'boolean']); + register_setting('wpavatar_basic', 'wpavatar_cdn_type', ['type' => 'string']); + register_setting('wpavatar_basic', 'wpavatar_cravatar_route', ['type' => 'string']); + register_setting('wpavatar_basic', 'wpavatar_third_party_mirror', ['type' => 'string']); + register_setting('wpavatar_basic', 'wpavatar_custom_cdn', ['type' => 'string']); + register_setting('wpavatar_basic', 'wpavatar_hash_method', ['type' => 'string']); + register_setting('wpavatar_basic', 'wpavatar_timeout', ['type' => 'integer']); + + register_setting('wpavatar_cache', 'wpavatar_enable_cache', ['type' => 'boolean']); + register_setting('wpavatar_cache', 'wpavatar_cache_path', [ + 'type' => 'string', + 'sanitize_callback' => [__CLASS__, 'sanitize_cache_path'] + ]); + register_setting('wpavatar_cache', 'wpavatar_cache_expire', ['type' => 'integer']); + + register_setting('wpavatar_advanced', 'wpavatar_seo_alt', ['type' => 'string']); + register_setting('wpavatar_advanced', 'wpavatar_fallback_mode', ['type' => 'boolean']); + register_setting('wpavatar_advanced', 'wpavatar_fallback_avatar', ['type' => 'string']); + + register_setting('wpavatar_shortcodes', 'wpavatar_shortcode_size', ['type' => 'integer']); + register_setting('wpavatar_shortcodes', 'wpavatar_shortcode_class', ['type' => 'string']); + register_setting('wpavatar_shortcodes', 'wpavatar_shortcode_shape', ['type' => 'string']); + } + + public static function sanitize_cache_path($value) { + $value = sanitize_text_field($value); + $value = rtrim($value, '/\\') . '/'; + + if (!preg_match('~^(?:/|\\\\|[a-zA-Z]:)~', $value)) { + $value = WP_CONTENT_DIR . '/' . ltrim($value, '/\\'); + } + + if (!file_exists($value)) { + if (!wp_mkdir_p($value)) { + add_settings_error( + 'wpavatar_cache', + 'cache_path_invalid', + __('无法创建缓存目录,请检查权限', 'wpavatar'), + 'error' + ); + return get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + } + } elseif (!is_dir($value)) { + add_settings_error( + 'wpavatar_cache', + 'cache_path_invalid', + __('指定的路径不是有效目录', 'wpavatar'), + 'error' + ); + return get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + } elseif (!is_writable($value)) { + add_settings_error( + 'wpavatar_cache', + 'cache_path_invalid', + __('缓存目录不可写,请检查权限', 'wpavatar'), + 'error' + ); + return get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + } + + $index_file = $value . 'index.php'; + if (!file_exists($index_file)) { + @file_put_contents($index_file, ' wp_create_nonce('wpavatar_admin_nonce'), + 'ajaxurl' => admin_url('admin-ajax.php'), + 'cache_path' => get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR), + 'plugin_url' => WPAVATAR_PLUGIN_URL, + 'assets_url' => WPAVATAR_PLUGIN_URL . 'assets/', + ]); + + wp_localize_script('wpavatar-admin', 'wpavatar_l10n', [ + 'checking' => __('检查中...', 'wpavatar'), + 'checking_status' => __('正在检查缓存状态...', 'wpavatar'), + 'check_failed' => __('检查失败,请重试', 'wpavatar'), + 'request_failed' => __('请求失败,请检查网络连接', 'wpavatar'), + 'check_cache' => __('检查缓存状态', 'wpavatar'), + 'confirm_purge' => __('确定要清空所有缓存头像吗?此操作无法撤销。', 'wpavatar'), + 'purging' => __('清空中...', 'wpavatar'), + 'purging_cache' => __('正在清空缓存...', 'wpavatar'), + 'purge_failed' => __('清空失败,请重试', 'wpavatar'), + 'purge_cache' => __('清空缓存', 'wpavatar'), + 'enter_custom_cdn' => __('请输入自定义CDN域名', 'wpavatar'), + 'enter_cache_path' => __('请输入缓存目录路径', 'wpavatar'), + 'settings_saved' => __('设置已成功保存。', 'wpavatar') + ]); + } + + public static function display_notices() { + settings_errors('wpavatar_basic'); + settings_errors('wpavatar_cache'); + settings_errors('wpavatar_advanced'); + settings_errors('wpavatar_shortcodes'); + } + + public static function render_settings_page() { + $active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'basic'; + ?> +
+

+ + + +

+ + +
+
+
+ + + + +
+
+ +
+

+

+ +
+ + + + + + + + + + + > + + + + > + + + + > + + + + + + + + + + + +
+ +

+
+
+
+ +
+ +

+
+ +

+
+ +

+
+
+ +

+

+
+ + +

+
+
+ + +
+
+
+ +
+

+

+ +
+

+
+
+ + +
+
+ +
+ + + + + + + + + + + + + + +
+ +

+
+ +

+
+ + +

+
+
+ + +
+
+
+ +
+

+

+ +
+ + + + + + + + + + + + + + +
+ +

+
+ +

+
+
+ $avatar) : ?> + + +
+

+ %s', + esc_url($wpcy_link), + __('文派叶子 🍃(WPCY.COM)', 'wpavatar') + ) + ); + ?> +

+
+
+ + +
+
+
+ + +
+

+

+ +
+

+
+
+

+ +
+
+

+ +
+
+

+ +
+
+

+ +
+
+

+ +
+
+

+ +
+
+

+
+ +
+ + + + + + + + + + + + + + +
+ +

+
+ +

+
+ +

+
+ +
+ + +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + +
[wpavatar] +
    +
  • size -
  • +
  • user_id -
  • +
  • class -
  • +
  • shape -
  • +
  • title -
  • +
+
[wpavatar size="128" user_id="1" shape="circle" class="my-avatar"]
[wpavatar_username] +
    +
  • user_id -
  • +
  • before -
  • +
  • after -
  • +
+
[wpavatar_username before="欢迎," after="!"]
+

+

+
    +
  • {wpavatar} -
  • +
  • {wpavatar_username} -
  • +
+
+
+
+
+
+
+ + +
+

+ + + +
+

+

+ +
+ + + + + + + + + + + + + + + + + + +
+ +

+
+ +
+ +
+

+
+
+ +
+
+
+
+ 'wpavatar-network', 'updated' => 'true'], network_admin_url('settings.php'))); + exit; + } +} diff --git a/includes/core.php b/includes/core.php new file mode 100644 index 0000000..a71b5dd --- /dev/null +++ b/includes/core.php @@ -0,0 +1,690 @@ + WPAVATAR_PLUGIN_URL . 'assets/images/default-avatar.png', + 'name' => __('默认头像', 'wpavatar') + ]; + } + + if (file_exists(WPAVATAR_PLUGIN_DIR . 'assets/images/wapuu.png')) { + $avatars['wapuu'] = [ + 'url' => WPAVATAR_PLUGIN_URL . 'assets/images/wapuu.png', + 'name' => __('文派Wapuu', 'wpavatar') + ]; + } + + if (file_exists(WPAVATAR_PLUGIN_DIR . 'assets/images/wapuu-china.png')) { + $avatars['wapuu-china'] = [ + 'url' => WPAVATAR_PLUGIN_URL . 'assets/images/wapuu-china.png', + 'name' => __('中国Wapuu', 'wpavatar') + ]; + } + + if (file_exists(WPAVATAR_PLUGIN_DIR . 'assets/images/cravatar-default.png')) { + $avatars['cravatar'] = [ + 'url' => WPAVATAR_PLUGIN_URL . 'assets/images/cravatar-default.png', + 'name' => __('Cravatar默认', 'wpavatar') + ]; + } + + if (file_exists(WPAVATAR_PLUGIN_DIR . 'assets/images/cravatar-logo.png')) { + $avatars['cravatar-logo'] = [ + 'url' => WPAVATAR_PLUGIN_URL . 'assets/images/cravatar-logo.png', + 'name' => __('Cravatar标志', 'wpavatar') + ]; + } + + return $avatars; + } + + public static function modify_profile_picture_description() { + return '' . __('您可以在初认头像修改您的资料图片', 'wpavatar') . ''; + } + + public static function pre_get_avatar_data($args, $id_or_email) { + if (is_null($args)) { + $args = []; + } + + $email = ''; + if (is_numeric($id_or_email)) { + $user = get_user_by('id', (int)$id_or_email); + if ($user) { + $email = $user->user_email; + } + } elseif (is_string($id_or_email)) { + if (is_email($id_or_email)) { + $email = $id_or_email; + } + } elseif (is_object($id_or_email)) { + if (isset($id_or_email->user_id) && $id_or_email->user_id) { + $user = get_user_by('id', $id_or_email->user_id); + if ($user) { + $email = $user->user_email; + } + } elseif (isset($id_or_email->comment_author_email)) { + $email = $id_or_email->comment_author_email; + } elseif (isset($id_or_email->user_email)) { + $email = $id_or_email->user_email; + } + } + + if (empty($email)) { + return $args; + } + + // 确定使用的哈希方法 + $cdn_type = get_option('wpavatar_cdn_type', 'cravatar_route'); + $use_md5 = true; + + // Cravatar只支持MD5,如果使用Cravatar相关服务,强制使用MD5 + if ($cdn_type !== 'cravatar_route') { + // 检查第三方镜像或自定义CDN是否与Cravatar相关 + $third_party_mirror = get_option('wpavatar_third_party_mirror', ''); + $custom_cdn = get_option('wpavatar_custom_cdn', ''); + $is_cravatar_related = ( + strpos(strtolower($third_party_mirror), 'cravatar') !== false || + strpos(strtolower($custom_cdn), 'cravatar') !== false + ); + + if (!$is_cravatar_related) { + // 非Cravatar服务使用用户设置的哈希方法 + $hash_method = get_option('wpavatar_hash_method', 'sha256'); + $use_md5 = ($hash_method === 'md5'); + } + } + + // 检查WordPress版本,决定是否支持SHA256 + $wp_version = get_bloginfo('version'); + $use_sha256_support = version_compare($wp_version, '6.8', '>='); + + // 设置哈希方法 + if (!$use_md5 && $use_sha256_support) { + $args['hash_method'] = 'sha256'; + $args['hash'] = 'sha256'; + } else { + $args['hash_method'] = 'md5'; + + if (isset($args['hash']) && $args['hash'] === 'sha256') { + unset($args['hash']); + } + } + + // 计算邮箱地址的哈希值 + if (!isset($args['wpavatar_email_hash'])) { + if (!$use_md5) { + $args['wpavatar_email_hash'] = hash('sha256', strtolower(trim($email))); + } else { + $args['wpavatar_email_hash'] = md5(strtolower(trim($email))); + } + } + + // 设置超时属性 + $timeout = get_option('wpavatar_timeout', 5); + $args['extra_attr'] = isset($args['extra_attr']) ? $args['extra_attr'] : ''; + $args['extra_attr'] .= ' data-timeout="' . esc_attr($timeout) . '"'; + + return $args; + } + + public static function replace_avatar_url($url) { + // 遍历所有Gravatar域名,替换为Cravatar相关域名 + foreach (self::$gravatar_domains as $domain) { + if (strpos($url, $domain) !== false) { + $cdn_type = get_option('wpavatar_cdn_type', 'cravatar_route'); + $cdn_domain = ''; + + if ($cdn_type === 'cravatar_route') { + $cdn_domain = get_option('wpavatar_cravatar_route', 'cravatar.com'); + } elseif ($cdn_type === 'third_party') { + $cdn_domain = get_option('wpavatar_third_party_mirror', 'weavatar.com'); + } elseif ($cdn_type === 'custom') { + $custom_cdn = get_option('wpavatar_custom_cdn', ''); + if (!empty($custom_cdn)) { + $cdn_domain = $custom_cdn; + } else { + $cdn_domain = 'cravatar.com'; + } + } + + return str_replace($domain, $cdn_domain, $url); + } + } + + return $url; + } + + public static function add_seo_alt($avatar, $id_or_email, $size, $default, $alt) { + // 添加SEO友好的alt属性 + $seo_alt = get_option('wpavatar_seo_alt'); + if (!empty($seo_alt)) { + $user = false; + if (is_numeric($id_or_email)) { + $user = get_user_by('id', $id_or_email); + } elseif (is_object($id_or_email)) { + if (isset($id_or_email->user_id) && $id_or_email->user_id) { + $user = get_user_by('id', $id_or_email->user_id); + } elseif (isset($id_or_email->comment_author_email)) { + $user = (object) [ + 'display_name' => $id_or_email->comment_author + ]; + } + } elseif (is_string($id_or_email) && is_email($id_or_email)) { + $user = get_user_by('email', $id_or_email); + } + + $alt_text = $user ? sprintf($seo_alt, $user->display_name) : __('用户头像', 'wpavatar'); + $avatar = preg_replace('/alt=([\'"])[^\'"]*\\1/', "alt='$alt_text'", $avatar); + } + + // 添加头像加载失败的备用显示 + if (get_option('wpavatar_fallback_mode', 1)) { + $fallback_type = get_option('wpavatar_fallback_avatar', 'default'); + $local_avatars = self::get_local_avatars(); + + if (isset($local_avatars[$fallback_type])) { + $fallback_url = $local_avatars[$fallback_type]['url']; + + if (strpos($avatar, 'onerror') === false) { + $avatar = str_replace(' (time() - $cache_expire)) { + $cached_url = content_url(str_replace(WP_CONTENT_DIR, '', $cache_file)); + return str_replace($url, esc_url($cached_url), $avatar); + } + + if (self::cache_remote_avatar($url, $cache_file)) { + $cached_url = content_url(str_replace(WP_CONTENT_DIR, '', $cache_file)); + return str_replace($url, esc_url($cached_url), $avatar); + } + + return $avatar; + } + + public static function get_avatar_hash($id_or_email) { + $email = ''; + + if (is_object($id_or_email)) { + if (isset($id_or_email->comment_author_email)) { + $email = $id_or_email->comment_author_email; + } elseif (isset($id_or_email->user_email)) { + $email = $id_or_email->user_email; + } + } elseif (is_numeric($id_or_email)) { + $user = get_user_by('id', $id_or_email); + $email = $user ? $user->user_email : ''; + } elseif (is_string($id_or_email) && is_email($id_or_email)) { + $email = $id_or_email; + } + + if (empty($email)) { + return ''; + } + + $cdn_type = get_option('wpavatar_cdn_type', 'cravatar_route'); + $use_md5 = true; + + // 确定使用的哈希方法 + if ($cdn_type !== 'cravatar_route' && + strpos(get_option('wpavatar_third_party_mirror', ''), 'cravatar') === false && + strpos(get_option('wpavatar_custom_cdn', ''), 'cravatar') === false) { + $hash_method = get_option('wpavatar_hash_method', 'md5'); + $use_md5 = ($hash_method === 'md5'); + } + + if (!$use_md5) { + return hash('sha256', strtolower(trim($email))); + } else { + return md5(strtolower(trim($email))); + } + } + + public static function get_cache_path($hash, $size) { + $dir = get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + $dir = trailingslashit($dir); + wp_mkdir_p($dir); + return $dir . "{$hash}-{$size}.jpg"; + } + + public static function cache_remote_avatar($url, $dest) { + $dir = dirname($dest); + if (!file_exists($dir)) { + if (!wp_mkdir_p($dir)) { + error_log('WPAvatar: Failed to create cache directory: ' . $dir); + return false; + } + } + + if (!is_writable($dir)) { + error_log('WPAvatar: Cache directory is not writable: ' . $dir); + return false; + } + + $timeout = get_option('wpavatar_timeout', 5); + + $response = wp_remote_get($url, [ + 'timeout' => $timeout, + 'user-agent' => 'WPAvatar/' . WPAVATAR_VERSION . '; ' . home_url() + ]); + + if (is_wp_error($response)) { + error_log('WPAvatar: Error fetching avatar: ' . $response->get_error_message()); + return false; + } + + $response_code = wp_remote_retrieve_response_code($response); + if ($response_code !== 200) { + error_log('WPAvatar: Bad response code: ' . $response_code); + return false; + } + + $image = wp_remote_retrieve_body($response); + if (empty($image)) { + error_log('WPAvatar: Empty image received'); + return false; + } + + $content_type = wp_remote_retrieve_header($response, 'content-type'); + if (strpos($content_type, 'image/') !== 0) { + error_log('WPAvatar: Invalid content type: ' . $content_type); + return false; + } + + $result = file_put_contents($dest, $image); + if ($result === false) { + error_log('WPAvatar: Failed to write cache file: ' . $dest); + return false; + } + + @chmod($dest, 0644); + + return true; + } + + public static function cache_comment_avatar($comment_id, $comment_approved) { + if ($comment_approved !== 1 || !get_option('wpavatar_enable_cache', true)) { + return; + } + + $comment = get_comment($comment_id); + if (!$comment || empty($comment->comment_author_email)) { + return; + } + + $email = $comment->comment_author_email; + $size = get_option('wpavatar_shortcode_size', 96); + + $avatar_url = get_avatar_url($email, ['size' => $size]); + + $hash = self::get_avatar_hash($email); + if (empty($hash)) { + return; + } + + $cache_file = self::get_cache_path($hash, $size); + + self::cache_remote_avatar($avatar_url, $cache_file); + + // 缓存2x尺寸的头像,用于高分辨率显示器 + $retina_url = get_avatar_url($email, ['size' => $size * 2]); + $retina_cache_file = self::get_cache_path($hash, $size * 2); + self::cache_remote_avatar($retina_url, $retina_cache_file); + } + + public static function cache_user_avatar($user_id) { + if (!get_option('wpavatar_enable_cache', true)) { + return; + } + + $user = get_user_by('id', $user_id); + if (!$user) { + return; + } + + $email = $user->user_email; + $size = get_option('wpavatar_shortcode_size', 96); + + $avatar_url = get_avatar_url($email, ['size' => $size]); + + $hash = self::get_avatar_hash($email); + if (empty($hash)) { + return; + } + + $cache_file = self::get_cache_path($hash, $size); + + self::cache_remote_avatar($avatar_url, $cache_file); + + // 缓存2x尺寸的头像,用于高分辨率显示器 + $retina_url = get_avatar_url($email, ['size' => $size * 2]); + $retina_cache_file = self::get_cache_path($hash, $size * 2); + self::cache_remote_avatar($retina_url, $retina_cache_file); + } + + public static function purge_expired() { + $dir = get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + if (!file_exists($dir) || !is_dir($dir)) { + return; + } + + $files = glob(trailingslashit($dir) . '*.jpg'); + if (!$files) { + return; + } + + $expire_days = get_option('wpavatar_cache_expire', 7); + $expire_time = time() - ($expire_days * DAY_IN_SECONDS); + + foreach ($files as $file) { + if (filemtime($file) < $expire_time) { + @unlink($file); + } + } + } + + public static function schedule_purge() { + if (!wp_next_scheduled('wpavatar_purge_cache')) { + wp_schedule_event(time(), 'daily', 'wpavatar_purge_cache'); + } + } + + public static function check_cache_status() { + $dir = get_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + $stats = [ + 'path' => $dir, + 'exists' => file_exists($dir) && is_dir($dir), + 'writable' => is_writable($dir), + 'file_count' => 0, + 'size' => 0 + ]; + + if ($stats['exists']) { + $files = glob(trailingslashit($dir) . '*.jpg'); + $stats['file_count'] = count($files ?: []); + foreach ($files ?: [] as $file) { + $stats['size'] += filesize($file); + } + $stats['size'] = size_format($stats['size']); + } + + ob_start(); + ?> +
+

+

+

+

+

+
+ $default_size, + 'user_id' => get_current_user_id(), + 'class' => $default_class, + 'email' => '', + 'shape' => $default_shape, + 'title' => '' + ], $atts); + + if (empty($atts['user_id']) && empty($atts['email'])) { + $atts['user_id'] = get_current_user_id(); + } + + $classes = [$atts['class']]; + + if ($atts['shape'] === 'circle') { + $classes[] = 'avatar-circle'; + $style = 'style="border-radius: 50%; overflow: hidden;"'; + } elseif ($atts['shape'] === 'rounded') { + $classes[] = 'avatar-rounded'; + $style = 'style="border-radius: 8px; overflow: hidden;"'; + } else { + $classes[] = 'avatar-square'; + $style = ''; + } + + $avatar_args = [ + 'class' => implode(' ', $classes), + 'size' => intval($atts['size']), + 'extra_attr' => $style + ]; + + if (!empty($atts['title'])) { + $avatar_args['extra_attr'] .= ' title="' . esc_attr($atts['title']) . '"'; + } + + ob_start(); + if (!empty($atts['email'])) { + echo get_avatar($atts['email'], intval($atts['size']), 'default', '', $avatar_args); + } else { + echo get_avatar($atts['user_id'], intval($atts['size']), 'default', '', $avatar_args); + } + return ob_get_clean(); + } + + public static function render_username($atts) { + $atts = shortcode_atts([ + 'user_id' => get_current_user_id(), + 'before' => '', + 'after' => '' + ], $atts); + + if (!empty($atts['user_id'])) { + $user = get_user_by('id', $atts['user_id']); + } else { + $user = wp_get_current_user(); + } + + $username = $user && $user->display_name ? $user->display_name : __('匿名用户', 'wpavatar'); + return $atts['before'] . $username . $atts['after']; + } + + public static function menu_item_replace($item_output, $item, $depth, $args) { + if (strpos($item_output, '{wpavatar}') !== false) { + $item_output = str_replace('{wpavatar}', do_shortcode('[wpavatar shape="circle" size="32"]'), $item_output); + } + if (strpos($item_output, '{wpavatar_username}') !== false) { + $item_output = str_replace('{wpavatar_username}', do_shortcode('[wpavatar_username]'), $item_output); + } + return $item_output; + } + + public static function generate_preview($user_id = 0, $shape = 'square', $size = 96) { + if (!$user_id) { + $user_id = get_current_user_id(); + } + + $atts = [ + 'user_id' => $user_id, + 'shape' => $shape, + 'size' => $size, + 'class' => 'wpavatar-preview' + ]; + + return self::render_avatar($atts); + } +} diff --git a/includes/cravatar.php b/includes/cravatar.txt similarity index 100% rename from includes/cravatar.php rename to includes/cravatar.txt diff --git a/wpavatar.php b/wpavatar.php index ffc12c2..d495577 100644 --- a/wpavatar.php +++ b/wpavatar.php @@ -1,44 +1,258 @@ id : ''; + + $is_discussion_page = ($screen_id === 'options-discussion' || + (isset($_GET['page']) && $_GET['page'] === 'discussion')); + + $is_comments_page = ($screen_id === 'edit-comments' || + $screen_id === 'comment'); + + $is_profile_page = ($screen_id === 'profile' || + $screen_id === 'user-edit'); + + if ($is_discussion_page || $is_comments_page || $is_profile_page) { + $translated_text = str_replace('Gravatar', 'Cravatar', $translated_text); + $translated_text = str_replace('gravatar', 'cravatar', $translated_text); + } + + return $translated_text; +} + +function wpavatar_replace_gravatar_text_plural($translated_text, $single, $plural, $number) { + if (!get_option('wpavatar_enable_cravatar', 1)) { + return $translated_text; + } + + $current_screen = function_exists('get_current_screen') ? get_current_screen() : null; + $screen_id = $current_screen ? $current_screen->id : ''; + + $is_relevant_page = ($screen_id === 'options-discussion' || + $screen_id === 'edit-comments' || + $screen_id === 'comment' || + $screen_id === 'profile' || + $screen_id === 'user-edit' || + (isset($_GET['page']) && $_GET['page'] === 'discussion')); + + if ($is_relevant_page) { + $translated_text = str_replace('Gravatar', 'Cravatar', $translated_text); + $translated_text = str_replace('gravatar', 'cravatar', $translated_text); + } + + return $translated_text; +} + +register_activation_hook(__FILE__, function() { + add_option('wpavatar_enable_cravatar', 1); + add_option('wpavatar_cdn_type', 'cravatar_route'); + add_option('wpavatar_cravatar_route', 'cravatar.com'); + add_option('wpavatar_third_party_mirror', 'weavatar.com'); + add_option('wpavatar_custom_cdn', ''); + add_option('wpavatar_hash_method', 'md5'); + add_option('wpavatar_timeout', 5); + + add_option('wpavatar_enable_cache', 1); + add_option('wpavatar_cache_path', WPAVATAR_CACHE_DIR); + add_option('wpavatar_cache_expire', 15); + + add_option('wpavatar_seo_alt', '%s的头像'); + add_option('wpavatar_fallback_mode', 1); + add_option('wpavatar_fallback_avatar', 'default'); + + add_option('wpavatar_shortcode_size', 96); + add_option('wpavatar_shortcode_class', 'wpavatar'); + add_option('wpavatar_shortcode_shape', 'square'); + + wp_mkdir_p(WPAVATAR_CACHE_DIR); + + $index_file = rtrim(WPAVATAR_CACHE_DIR, '/\\') . '/index.php'; + if (!file_exists($index_file)) { + @file_put_contents($index_file, '\n"; + $htaccess_content .= "ExpiresActive On\n"; + $htaccess_content .= "ExpiresByType image/jpeg \"access plus 1 week\"\n"; + $htaccess_content .= "\n"; + @file_put_contents($htaccess_file, $htaccess_content); + } + + if (is_multisite()) { + add_site_option('wpavatar_network_enabled', 1); + add_site_option('wpavatar_network_cdn_type', 'cravatar_route'); + add_site_option('wpavatar_network_cravatar_route', 'cravatar.com'); + } + + if (!wp_next_scheduled('wpavatar_purge_cache')) { + wp_schedule_event(time(), 'daily', 'wpavatar_purge_cache'); + } +}); + +register_deactivation_hook(__FILE__, function() { + wp_clear_scheduled_hook('wpavatar_purge_cache'); +});