Files
WhoReadThis/Plugin.php

895 lines
31 KiB
PHP
Raw Normal View History

2026-02-23 21:09:17 +08:00
<?php
/**
* 谁读了本文
*
* @package WhoReadThis
* @author 石头厝
* @version 1.0.8
* @link https://www.shitoucuo.com
*/
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
class WhoReadThis_Plugin implements Typecho_Plugin_Interface
{
// 数据库表名
private static $_table = 'whoreadthis';
// 是否已输出资源
private static $_assetsOutput = false;
/**
* 激活插件
*/
public static function activate()
{
// 创建数据表记录阅读者
$db = Typecho_Db::get();
$prefix = $db->getPrefix();
$table = $prefix . self::$_table;
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`cid` int(11) NOT NULL COMMENT '文章ID',
`uid` int(11) DEFAULT 0 COMMENT '用户ID',
`username` varchar(200) DEFAULT NULL COMMENT '用户名',
`email` varchar(200) DEFAULT NULL COMMENT '用户邮箱',
`website` varchar(500) DEFAULT NULL COMMENT '用户网站',
`avatar` varchar(500) DEFAULT NULL COMMENT '头像URL',
`ip` varchar(45) DEFAULT NULL COMMENT 'IP地址',
`created` int(10) NOT NULL COMMENT '阅读时间',
PRIMARY KEY (`id`),
KEY `cid` (`cid`),
KEY `uid` (`uid`),
KEY `ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;";
try {
$db->query($sql);
} catch (Exception $e) {
// 表可能已经存在
}
// 挂载文章页
Typecho_Plugin::factory('Widget_Archive')->footer = array(__CLASS__, 'trackVisitor');
return _t('插件已激活,请在后台设置中开启功能');
}
/**
* 禁用插件
*/
public static function deactivate()
{
return _t('插件已禁用');
}
/**
* 插件配置方法
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
// 是否开启功能
$enable = new Typecho_Widget_Helper_Form_Element_Radio(
'enable',
array('1' => _t('开启'), '0' => _t('关闭')),
'1',
_t('是否开启功能'),
_t('关闭后前端将不会显示谁读了本文')
);
$form->addInput($enable);
// 是否屏蔽管理员记录和显示
$hideAdmin = new Typecho_Widget_Helper_Form_Element_Radio(
'hide_admin',
array(
'1' => _t('是(不记录也不显示)'),
'0' => _t('否(正常记录和显示)')
),
'0',
_t('是否屏蔽管理员'),
_t('开启后管理员阅读本文不会被记录,也不会在列表中显示')
);
$form->addInput($hideAdmin);
// 显示模式
$mode = new Typecho_Widget_Helper_Form_Element_Radio(
'mode',
array(
'all' => _t('显示所有阅读者'),
'login' => _t('仅显示登录用户'),
'recent' => _t('仅显示近期阅读者24小时内')
),
'all',
_t('显示模式'),
_t('选择要显示的用户类型')
);
$form->addInput($mode);
// 显示数量限制
$limit = new Typecho_Widget_Helper_Form_Element_Text(
'limit',
NULL,
'20',
_t('最大显示数量'),
_t('最多显示多少个阅读者0表示不限制')
);
$limit->input->setAttribute('class', 'mini');
$limit->addRule('isInteger', _t('请输入数字'));
$form->addInput($limit);
// IP去重时间小时
$ip_interval = new Typecho_Widget_Helper_Form_Element_Text(
'ip_interval',
NULL,
'24',
_t('IP去重时间小时'),
_t('同一IP多少小时内不重复记录')
);
$ip_interval->input->setAttribute('class', 'mini');
$ip_interval->addRule('isInteger', _t('请输入数字'));
$form->addInput($ip_interval);
// 自定义标题
$title = new Typecho_Widget_Helper_Form_Element_Text(
'title',
NULL,
'谁读了本文',
_t('显示标题'),
_t('前端显示的标题文字')
);
$form->addInput($title);
// 展开默认状态
$default_open = new Typecho_Widget_Helper_Form_Element_Radio(
'default_open',
array('0' => _t('收起'), '1' => _t('展开')),
'0',
_t('默认状态'),
_t('初始显示时是展开还是收起状态')
);
$form->addInput($default_open);
// 头像大小
$avatar_size = new Typecho_Widget_Helper_Form_Element_Text(
'avatar_size',
NULL,
'40',
_t('头像尺寸(像素)'),
_t('头像显示的大小,单位:像素')
);
$avatar_size->input->setAttribute('class', 'mini');
$avatar_size->addRule('isInteger', _t('请输入数字'));
$form->addInput($avatar_size);
}
/**
* 配置保存处理
*/
public static function configHandle($settings, $isInit)
{
// 确保所有设置都有值
$defaults = array(
'enable' => '1',
'hide_admin' => '0',
'mode' => 'all',
'limit' => '20',
'ip_interval' => '24',
'title' => '谁读了本文',
'default_open' => '0',
'avatar_size' => '40'
);
foreach ($defaults as $key => $value) {
if (!isset($settings[$key])) {
$settings[$key] = $value;
}
}
Helper::configPlugin('WhoReadThis', $settings);
}
/**
* 个人用户配置面板
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
/**
* 追踪访问者并输出资源
*/
public static function trackVisitor()
{
$archive = Typecho_Widget::widget('Widget_Archive');
if (!$archive->is('single')) {
return;
}
$options = Helper::options()->plugin('WhoReadThis');
if (!$options || $options->enable == '0') {
return;
}
$cid = $archive->cid;
$ip = self::getClientIP();
// 检查是否屏蔽管理员
$hideAdmin = isset($options->hide_admin) ? $options->hide_admin : '0';
$user = Typecho_Widget::widget('Widget_User');
$isAdmin = $user->hasLogin() && $user->pass('administrator', true);
if ($hideAdmin == '1' && $isAdmin) {
return; // 如果是管理员且开启屏蔽,则不记录
}
// 检查IP是否在指定时间内已记录
$db = Typecho_Db::get();
$table = $db->getPrefix() . self::$_table;
$ipInterval = isset($options->ip_interval) ? intval($options->ip_interval) : 24;
$checkSql = $db->select('id')
->from($table)
->where('cid = ?', $cid)
->where('ip = ?', $ip)
->where('created > ?', time() - ($ipInterval * 3600))
->limit(1);
$check = $db->fetchRow($checkSql);
if (!$check) {
// 获取当前用户信息
$uid = $user->hasLogin() ? $user->uid : 0;
$username = $user->hasLogin() ? $user->screenName : '访客';
$email = $user->hasLogin() ? $user->mail : '';
$website = $user->hasLogin() ? $user->url : '';
// 获取头像
$avatar = '';
if ($email) {
$avatar = 'https://cravatar.cn/avatar/' . md5(strtolower(trim($email))) . '?s=200&d=identicon';
} else {
// 为访客生成随机头像
$avatar = 'https://cravatar.cn/avatar/' . md5($ip) . '?s=200&d=identicon';
}
// 记录阅读者
$insert = $db->insert($table)->rows(array(
'cid' => $cid,
'uid' => $uid,
'username' => $username,
'email' => $email,
'website' => $website,
'avatar' => $avatar,
'ip' => $ip,
'created' => time()
));
$db->query($insert);
}
// 输出资源文件(只输出一次)
if (!self::$_assetsOutput) {
echo '<!-- WhoReadThis Plugin Assets -->';
echo '<style>';
echo self::getPluginStyles();
echo '</style>';
echo '<script>';
echo self::getPluginScripts();
echo '</script>';
self::$_assetsOutput = true;
}
}
/**
* 前端调用函数
*/
public static function show()
{
$archive = Typecho_Widget::widget('Widget_Archive');
if (!$archive->is('single')) {
return '';
}
$options = Helper::options()->plugin('WhoReadThis');
if (!$options || $options->enable == '0') {
return '';
}
$cid = $archive->cid;
$db = Typecho_Db::get();
$table = $db->getPrefix() . self::$_table;
// 构建查询
$select = $db->select()->from($table)->where('cid = ?', $cid);
// 如果开启了屏蔽管理员,排除管理员记录
$hideAdmin = isset($options->hide_admin) ? $options->hide_admin : '0';
if ($hideAdmin == '1') {
// 获取所有管理员ID
$adminUsers = $db->fetchAll($db->select('uid')->from('table.users')->where('group = ?', 'administrator'));
if ($adminUsers) {
$adminIds = array_column($adminUsers, 'uid');
if (!empty($adminIds)) {
$select->where('uid NOT IN ?', $adminIds);
}
}
}
$mode = isset($options->mode) ? $options->mode : 'all';
switch ($mode) {
case 'login':
$select->where('uid > 0');
break;
case 'recent':
$select->where('created > ?', time() - 86400); // 24小时内
break;
}
$select->order('id', Typecho_Db::SORT_DESC);
$limit = isset($options->limit) ? intval($options->limit) : 20;
if ($limit > 0) {
$select->limit($limit);
}
try {
$readers = $db->fetchAll($select);
} catch (Exception $e) {
return '';
}
// 如果没有阅读者数据,返回空字符串(不显示任何内容)
if (empty($readers)) {
return '';
}
$usersHtml = '';
$uniqueReaders = array();
foreach ($readers as $reader) {
// 去重,同个用户只显示一次
$key = $reader['uid'] ?: $reader['ip'];
if (isset($uniqueReaders[$key])) {
continue;
}
$uniqueReaders[$key] = true;
$avatar = $reader['avatar'] ?: 'https://cravatar.cn/avatar/0?s=200&d=identicon';
$username = htmlspecialchars($reader['username']);
$link = !empty($reader['website']) ? $reader['website'] : '#';
$usersHtml .= '<li class="whoreadthis-user">
<a href="' . htmlspecialchars($link) . '" target="_blank" class="whoreadthis-avatar-wrapper"
title="' . $username . '" rel="noopener noreferrer">
<div class="whoreadthis-avatar">
<img src="' . htmlspecialchars($avatar) . '" alt="' . $username . '" loading="lazy">
</div>
</a>
</li>';
}
$count = count($uniqueReaders);
$defaultOpen = isset($options->default_open) ? $options->default_open : '0';
$isOpen = $defaultOpen == '1' ? ' expanded' : ' collapsed';
$title = isset($options->title) ? $options->title : '谁读了本文';
$avatarSize = isset($options->avatar_size) ? intval($options->avatar_size) : 40;
// 生成唯一的容器ID
$containerId = 'whoreadthis-' . $cid;
$html = '<div class="whoreadthis-container" id="' . $containerId . '" data-avatar-size="' . $avatarSize . '">
<div class="whoreadthis-header-section">
<div class="whoreadthis-header">
<h3>' . htmlspecialchars($title) . '<span class="whoreadthis-count-badge">' . $count . '</span></h3>
<button class="whoreadthis-toggle-btn' . ($isOpen ? ' expanded' : ' collapsed') . '">
<span class="toggle-arrow"></span>
</button>
</div>
</div>
<div class="whoreadthis-content-section">
<div class="whoreadthis-items-wrapper' . $isOpen . '">
<ul class="whoreadthis-users">' . $usersHtml . '</ul>
</div>
</div>
</div>';
return $html;
}
/**
* 获取插件CSS样式包含深色模式
*/
private static function getPluginStyles()
{
return '
/* WhoReadThis 插件专用样式 - 包含深色模式 */
.whoreadthis-container {
border-radius: 10px;
overflow: hidden;
position: relative;
z-index: 10;
margin: 0 0 30px 0;
clear: both;
box-sizing: border-box;
}
/* 标题区域 */
.whoreadthis-container .whoreadthis-header-section {
padding: 18px 24px;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
position: relative;
z-index: 20;
box-sizing: border-box;
}
/* 深色模式标题区域 */
body.dark .whoreadthis-container .whoreadthis-header-section,
body.dark-mode .whoreadthis-container .whoreadthis-header-section,
body.theme-dark .whoreadthis-container .whoreadthis-header-section,
.dark .whoreadthis-container .whoreadthis-header-section,
.dark-mode .whoreadthis-container .whoreadthis-header-section {
background: rgb(10 12 25 / 1);
color:rgb(156 163 175 / var(--tw-text-opacity))!important;
}
/* 标题文字 */
.whoreadthis-container .whoreadthis-header {
margin: 0;
font-size: 18px;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
font-weight: 600;
position: relative;
z-index: 25;
box-sizing: border-box;
}
.dark .whoreadthis-container .whoreadthis-toggle-btn {color: rgb(156 163 175 / var(--tw-text-opacity))!important;}
.dark .whoreadthis-count-badge {color: rgb(156 163 175 / var(--tw-text-opacity))!important;}
/* 深色模式标题文字 */
body.dark .whoreadthis-container .whoreadthis-header h3,
body.dark-mode .whoreadthis-container .whoreadthis-header h3,
body.theme-dark .whoreadthis-container .whoreadthis-header h3,
.dark .whoreadthis-container .whoreadthis-header h3,
.dark-mode .whoreadthis-container .whoreadthis-header h3 {
color: rgb(156 163 175 / var(--tw-text-opacity))!important;
}
/* 计数徽章 */
.whoreadthis-container .whoreadthis-count-badge {
background-color: rgba(255, 255, 255, 0.2);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 13px;
margin-left: 8px;
display: inline-block;
vertical-align: middle;
}
/* 深色模式计数徽章 */
body.dark .whoreadthis-container .whoreadthis-count-badge,
body.dark-mode .whoreadthis-container .whoreadthis-count-badge,
body.theme-dark .whoreadthis-container .whoreadthis-count-badge,
.dark .whoreadthis-container .whoreadthis-count-badge,
.dark-mode .whoreadthis-container .whoreadthis-count-badge {
background-color: rgba(255, 255, 255, 0.1);
color: #e2e8f0;
}
/* 切换按钮 */
.whoreadthis-container .whoreadthis-toggle-btn {
font-size: 0.8em;
color: #fff;
display: flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 4px;
transition: all 0.3s ease;
font-weight: normal;
border: none;
cursor: pointer;
position: relative;
z-index: 30;
background: transparent;
box-sizing: border-box;
}
.whoreadthis-container .whoreadthis-toggle-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.whoreadthis-container .whoreadthis-toggle-btn .toggle-arrow {
font-size: 14px;
line-height: 1;
transition: transform 0.3s ease;
display: inline-block;
}
.whoreadthis-container .whoreadthis-toggle-btn.expanded .toggle-arrow {
transform: rotate(180deg);
}
/* 内容区域 */
.whoreadthis-container .whoreadthis-content-section {
padding: 0 24px;
background: #f9fafb;
border-top: none;
border-radius: 0 0 10px 10px;
position: relative;
z-index: 15;
box-sizing: border-box;
}
/* 深色模式内容区域 */
body.dark .whoreadthis-container .whoreadthis-content-section,
body.dark-mode .whoreadthis-container .whoreadthis-content-section,
body.theme-dark .whoreadthis-container .whoreadthis-content-section,
.dark .whoreadthis-container .whoreadthis-content-section,
.dark-mode .whoreadthis-container .whoreadthis-content-section {
background: rgb(15 23 42 / 1);
}
/* 项目包装器 */
.whoreadthis-container .whoreadthis-items-wrapper {
transition: all 0.3s ease;
overflow: hidden;
position: relative;
z-index: 15;
box-sizing: border-box;
}
.whoreadthis-container .whoreadthis-items-wrapper.collapsed {
max-height: 0;
opacity: 0;
visibility: hidden;
padding: 0;
}
.whoreadthis-container .whoreadthis-items-wrapper.expanded {
max-height: 5000px;
opacity: 1;
visibility: visible;
padding: 20px 0;
}
/* 阅读者列表 */
.whoreadthis-container .whoreadthis-users {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
position: relative;
z-index: 40;
box-sizing: border-box;
}
.whoreadthis-container .whoreadthis-user {
position: relative;
z-index: 45;
box-sizing: border-box;
}
/* 头像包装器 */
.whoreadthis-container .whoreadthis-avatar-wrapper {
position: relative;
display: block;
z-index: 50;
text-decoration: none;
box-sizing: border-box;
}
/* 头像样式 */
.whoreadthis-container .whoreadthis-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
display: block;
position: relative;
z-index: 55;
transition: transform 0.3s ease;
box-sizing: border-box;
}
.whoreadthis-container .whoreadthis-avatar:hover {
transform: scale(1.05);
}
.whoreadthis-container .whoreadthis-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
padding: 2px;
border: 2px solid #f15a22;
display: block;
transition: transform 0.4s ease-out;
box-sizing: border-box;
}
.whoreadthis-container .whoreadthis-avatar img:hover {
transform: rotateZ(360deg);
}
/* 空状态 */
.whoreadthis-container .whoreadthis-empty {
text-align: center;
color: #95a5a6;
font-style: italic;
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.whoreadthis-container .whoreadthis-header-section,
.whoreadthis-container .whoreadthis-content-section {
padding: 14px 18px;
}
.whoreadthis-container .whoreadthis-header {
font-size: 16px;
}
.whoreadthis-container .whoreadthis-users {
gap: 8px;
}
}
@media (max-width: 480px) {
.whoreadthis-container .whoreadthis-header-section,
.whoreadthis-container .whoreadthis-content-section {
padding: 12px 15px;
}
.whoreadthis-container .whoreadthis-header {
font-size: 14px;
}
.whoreadthis-container .whoreadthis-count-badge {
font-size: 11px;
padding: 3px 6px;
}
.whoreadthis-container .whoreadthis-users {
gap: 6px;
justify-content: center;
}
}
/* 动画效果 */
@keyframes whoreadthisFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user {
animation: whoreadthisFadeIn 0.3s ease forwards;
}
/* 为每个头像添加延迟动画 */
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(1) { animation-delay: 0.05s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(2) { animation-delay: 0.1s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(3) { animation-delay: 0.15s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(4) { animation-delay: 0.2s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(5) { animation-delay: 0.25s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(6) { animation-delay: 0.3s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(7) { animation-delay: 0.35s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(8) { animation-delay: 0.4s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(9) { animation-delay: 0.45s; }
.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(10) { animation-delay: 0.5s; }
/* 头像大小动态设置 */
.whoreadthis-container[data-avatar-size] .whoreadthis-avatar {
width: var(--whoreadthis-avatar-size, 40px);
height: var(--whoreadthis-avatar-size, 40px);
}
.whoreadthis-container[data-avatar-size] .whoreadthis-avatar img {
width: var(--whoreadthis-avatar-size, 40px);
height: var(--whoreadthis-avatar-size, 40px);
}
';
}
/**
* 获取插件JavaScript
*/
private static function getPluginScripts()
{
return '
(function() {
function initWhoReadThis() {
const containers = document.querySelectorAll(".whoreadthis-container");
containers.forEach(container => {
if (container.dataset.whoReadThisInitialized === "true") {
return;
}
container.dataset.whoReadThisInitialized = "true";
// 设置头像大小
const avatarSize = container.getAttribute("data-avatar-size");
if (avatarSize && avatarSize !== "40") {
container.style.setProperty("--whoreadthis-avatar-size", avatarSize + "px");
}
const header = container.querySelector(".whoreadthis-header");
const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
if (!header || !itemsWrapper || !toggleBtn) return;
// 绑定标题点击事件
header.addEventListener("click", function(e) {
if (e.target.closest(".whoreadthis-toggle-btn")) {
return;
}
toggleWhoReadThis(container);
});
// 绑定按钮点击事件
toggleBtn.addEventListener("click", function(e) {
e.stopPropagation();
toggleWhoReadThis(container);
});
// 处理头像链接点击
const avatarWrappers = container.querySelectorAll(".whoreadthis-avatar-wrapper");
avatarWrappers.forEach(wrapper => {
const link = wrapper.getAttribute("href");
if (link && link !== "#") {
wrapper.addEventListener("click", function(e) {
e.stopPropagation();
window.open(link, "_blank");
});
}
});
// 锚点跳转支持
const containerId = container.id;
if (window.location.hash === "#whoreadthis" || window.location.hash === "#" + containerId) {
setTimeout(function() {
container.scrollIntoView({ behavior: "smooth" });
expandWhoReadThis(container);
}, 300);
}
});
}
function toggleWhoReadThis(container) {
const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
const arrow = toggleBtn.querySelector(".toggle-arrow");
if (!itemsWrapper || !toggleBtn || !arrow) return;
if (itemsWrapper.classList.contains("collapsed")) {
expandWhoReadThis(container);
} else {
collapseWhoReadThis(container);
}
}
function expandWhoReadThis(container) {
const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
const arrow = toggleBtn.querySelector(".toggle-arrow");
if (itemsWrapper && toggleBtn && arrow) {
itemsWrapper.className = "whoreadthis-items-wrapper expanded";
toggleBtn.className = "whoreadthis-toggle-btn expanded";
arrow.textContent = "";
}
}
function collapseWhoReadThis(container) {
const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
const arrow = toggleBtn.querySelector(".toggle-arrow");
if (itemsWrapper && toggleBtn && arrow) {
itemsWrapper.className = "whoreadthis-items-wrapper collapsed";
toggleBtn.className = "whoreadthis-toggle-btn collapsed";
arrow.textContent = "";
}
}
// 初始化
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function() {
setTimeout(initWhoReadThis, 100);
});
} else {
setTimeout(initWhoReadThis, 100);
}
// 暴露公共API
window.WhoReadThis = {
init: initWhoReadThis,
expand: function(containerId) {
const container = document.getElementById(containerId);
if (container) {
expandWhoReadThis(container);
}
},
collapse: function(containerId) {
const container = document.getElementById(containerId);
if (container) {
collapseWhoReadThis(container);
}
},
toggle: function(containerId) {
const container = document.getElementById(containerId);
if (container) {
toggleWhoReadThis(container);
}
}
};
})();
';
}
/**
* 获取客户端IP
*/
private static function getClientIP()
{
$ip = '';
// 检查HTTP头
$headers = array(
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
);
foreach ($headers as $header) {
if (isset($_SERVER[$header]) && !empty($_SERVER[$header]) && strcasecmp($_SERVER[$header], 'unknown')) {
$ip = $_SERVER[$header];
break;
}
}
// 处理多个IP的情况
if (strpos($ip, ',') !== false) {
$ips = explode(',', $ip);
$ip = trim($ips[0]);
}
// 验证IP格式
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
return $ip;
}
}
/**
* 前端调用助手函数
*/
function showWhoReadThis()
{
echo WhoReadThis_Plugin::show();
}