本文為系列第三部分,對應 cinput.php3 —— 聊天室底部「輸入面板」的完整邏輯。該文件承擔:消息發送入口、內容過濾與轉義、表情/顏色/悄悄話附加屬性、指令解析(/t 想、/: 動作、/w 找人、// 系統動作)、管理員廣播、刷新頻率設置、退出入口。代碼採用「大量輔助函數 + 主邏輯分支」風格,典型的早期 PHP 過程式編程。
| 函數 | 功能 | 關鍵點 |
|---|---|---|
suiji($max) | 生成 0~$max-1 隨機整數 | 用 srand((double)microtime()*1e6) 播種,rand()/getrandmax() 歸一化 |
StrOccurs($sStr, $sFind) | 統計子串出現次數 | 邏輯錯誤:if (strstr($sTemp, $sFind)) break; 應為 !strstr,導致永遠返回 0 |
StrDupl($sStr, $iCnt) | 重複字符串 | 簡單循環拼接 |
DelQuot($sStr) | 轉義特殊字符 | 將 | ' " 替換為 HTML 實體 ¦ ´ " |
DelTag($sStr) | 過濾危險 HTML 標籤 | 黑名單模式:檢測 <TABLE> <SCRIPT> <BODY> 等 30+ 標籤,若無則 <→< >→>;繞過風險極高 |
AddLost($sStr) | 補全未閉合標籤 | 統計開閉標籤差值,補全 >、</FONT>、</B> 等 20+ 標籤;僅處理已知標籤,無法應對任意嵌套 |
$id=="1" && $ps=="superldz"):
RefRate(刷新間隔)。cmdSend=="送出" 時,將 txtSend 經 DelQuot(htmlspecialchars()) 處理後,以灰色系統消息格式寫入 ChtCont(RoomID=0 全域可見)。LstTime >= time()-1800)。Secret(悄悄話默認)、RefRate、ToID/ToName(私聊目標)、RoomID。cmdSend=="送出"):
$sSend = DelQuot(htmlspecialchars($txtSend))(註釋掉的 AddLost(DelTag()) 更嚴格但有 Bug)。chkSecret=="Yes" → $sSecret="True"、$sTalk="悄悄說:"。EmotionID、ColorID、Secret、RefRate、LstTime。/t ... → 藍色斜體「%m想...」,$bCht=False、$sSecret=False。/: ... → 紅色「%m...」,$bCht=False。/w 名字 → 查詢用戶所在房間,返回系統消息「XXX目前在房間N」,$bToMe=True(僅自己可見)、$sSecret=True。// 動作ID → 查詢 Action 表獲取動作內容,紅色顯示,$bCht=False。Emotion 表),隨機選一條模板「%m對%g{說/悄悄說}內容」。$sColor。ChtCont:私聊($bToMe)寫 sIDFrom=0, sIDTo=$id;公開寫 sIDFrom=$id, sIDTo=$sToID。sEmotion、sColor、id、ps;勾選框「悄悄話」;輸入框 txtSend;提交按鈕;刷新間隔輸入框;退出按鈕(跳轉 leave.php3)。if (substr($sSend,0,3)=="/t ") {
$sChtCont = "<font color=blue><i>%m想" . substr($sSend,3) . "</i></font>";
$sSecret = "False"; $bCht = "False";
} elseif (substr($sSend,0,3)=="/: ") {
$sChtCont = "<font color=red>%m" . substr($sSend,3) . "</font>";
$sSecret = "False"; $bCht = "False";
} elseif (strtolower(substr($sSend,0,3))=="/w ") {
$targetName = trim(substr($sSend,3));
$result = @odbc_exec($ConnID, "SELECT RoomID FROM User WHERE UserName='$targetName'");
if (@odbc_fetch_into($result,0,$rArr)) {
if ($rArr[0] > 0) {
$result = @odbc_exec($ConnID, "SELECT RoomName FROM Room WHERE RoomID=".$rArr[0]);
if (@odbc_fetch_into($result,0,$rArr))
$sChtCont = "<font color=gray>【系統消息】$targetName目前在房間".$rArr[0]."。</font>";
else
$sChtCont = "<font color=gray>【系統消息】系統混亂,$targetName房間不可識別!</font>";
} else
$sChtCont = "<font color=gray>【系統消息】$targetName目前沒有上線。</font>";
} else
$sChtCont = "<font color=gray>【系統消息】沒有$targetName這個人。</font>";
$sSecret = "True"; $bCht = "False"; $bToMe = "True";
} elseif (substr($sSend,0,3)=="// ") {
$act = trim(substr($sSend,3));
$result = @odbc_exec($ConnID, "SELECT ActCont FROM Action WHERE ActID='$act'");
if (@odbc_fetch_into($result,0,$rArr)) {
$sChtCont = "<font color=red>".$rArr[0]."</font>";
$sSecret = "False"; $bCht = "False";
} else $bCht = "True";
}
$id、$txtSend、$targetName、$act 直接拼接,DelQuot 僅替換三個字符,無法防禦注入。htmlspecialchars 僅用於管理員廣播與基礎消息,指令返回的系統消息直接拼接用戶名 $targetName 未轉義;表情模板 %m %g 後期替換時若未轉義同樣危險。DelTag 黑名單機制極易繞過(大小寫、編碼、嵌套、事件屬性);AddLost 補全邏輯錯誤(StrOccurs 歸零)且無法處理任意嵌套。現代應採用 HTMLPurifier 或 只允許純文本+標記語言。suiji() 依賴 rand()(非加密安全),表情隨機選取可預測。RefRate、Secret 等字段易覆蓋丟失。gb2312,數據庫存儲可能混雜,htmlspecialchars 默認不處理多字節,需指定 ENT_QUOTES, 'GB2312'。// 值對象:消息內容
class MessageContent {
public string $raw; // 用戶原始輸入
public string $sanitized; // 純文本(已轉義)
public array $mentions = []; // @用戶
public ?string $command = null; // /t /: /w //
public ?string $commandArg = null;
public static function fromRaw(string $raw): self {
$obj = new self();
$obj->raw = $raw;
$obj->sanitized = htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');
// 指令解析
if (preg_match('/^\/(t|:|w|\/)\s*(.+)$/', $raw, $m)) {
$obj->command = $m[1];
$obj->commandArg = trim($m[2]);
}
return $obj;
}
}
// 實體:聊天消息
class ChatMessage {
public int $fromId;
public int $toId; // 0=公開
public int $roomId;
public MessageContent $content;
public bool $isSecret;
public string $color; // #RRGGBB
public ?int $emotionId = null;
public bool $isSystem = false;
public string $renderedHtml; // 最终渲染 HTML(服務端生成,前端直接 innerHTML)
}
class MessageProcessor {
public function process(User $sender, MessageContent $content, array $options): ChatMessage {
$msg = new ChatMessage();
$msg->fromId = $sender->id;
$msg->roomId = $sender->room_id;
$msg->toId = $options['to_id'] ?? 0;
$msg->isSecret = $options['secret'] ?? false;
$msg->color = $options['color'] ?? '#000000';
$msg->emotionId = $options['emotion_id'] ?? null;
// 1. 指令處理器鏈
$handler = new CommandHandlerChain([
new ThoughtCommandHandler(), // /t
new ActionCommandHandler(), // /:
new WhereCommandHandler(), // /w
new SystemActionCommandHandler(), // //
]);
$cmdResult = $handler->handle($content, $sender);
if ($cmdResult) {
$msg->isSystem = true;
$msg->renderedHtml = $cmdResult->html;
$msg->toId = $cmdResult->targetId ?? 0;
$msg->isSecret = $cmdResult->isSecret ?? false;
return $msg;
}
// 2. 普通消息:表情模板
$template = $this->getEmotionTemplate($msg->emotionId)
?? "%m對%g{$msg->isSecret ? '悄悄說' : '說'}:%c";
$msg->renderedHtml = $this->renderTemplate($template, $sender, $msg);
return $msg;
}
private function renderTemplate(string $tpl, User $sender, ChatMessage $msg): string {
$color = $msg->isSecret ? '#008000' : $msg->color;
$fromName = htmlspecialchars($sender->username, ENT_QUOTES, 'UTF-8');
$toName = $msg->toId ? htmlspecialchars(User::find($msg->toId)?->username ?? '未知', ENT_QUOTES, 'UTF-8') : '大家';
$content = htmlspecialchars($msg->content->sanitized, ENT_QUOTES, 'UTF-8');
return str_replace(
['%m', '%g', '%c'],
["{$fromName}", "{$toName}", "{$content}"],
$tpl
);
}
}
interface CommandHandler {
public function handle(MessageContent $content, User $sender): ?CommandResult;
}
class WhereCommandHandler implements CommandHandler {
public function handle(MessageContent $content, User $sender): ?CommandResult {
if ($content->command !== 'w') return null;
$target = User::where('username', $content->commandArg)->first();
if (!$target) {
return new CommandResult(
html: "【系統消息】找不到用戶 {$content->commandArg}",
targetId: $sender->id, // 僅發送者可見
isSecret: true
);
}
$roomName = Room::find($target->room_id)?->name ?? '未知房間';
return new CommandResult(
html: "【系統消息】{$target->username} 目前在房間 {$roomName}",
targetId: $sender->id,
isSecret: true
);
}
}
<script setup>
import { ref, computed } from 'vue'
import { useChatStore } from '@/stores/chat'
import { useUserStore } from '@/stores/user'
const chatStore = useChatStore()
const userStore = useUserStore()
const message = ref('')
const secret = ref(false)
const emotionId = ref(null)
const color = ref('#000000')
const refreshRate = ref(5)
const send = () => {
if (!message.value.trim()) return
chatStore.sendMessage({
content: message.value,
secret: secret.value,
emotion_id: emotionId.value,
color: color.value,
to_id: userStore.user.to_id,
})
message.value = ''
}
</script>
<template>
<div class="input-panel">
<label><input type="checkbox" v-model="secret"> 悄悄話</label>
<input v-model="message" maxlength="120" placeholder="輸入消息..." @keyup.enter="send">
<select v-model="emotionId"><option value="">無表情</option><option v-for="e in chatStore.emotions" :key="e.id" :value="e.id">{{e.name}}</option></select>
<input type="color" v-model="color">
<input type="number" v-model.number="refreshRate" min="2" max="60" style="width:60px"> 秒刷新
<button @click="send">送出</button>
<button @click="chatStore.leave" class="danger">退出</button>
</div>
</template>
cinput.php3 在 300 行內實現了極其豐富的聊天室輸入功能:多級權限、指令系統、表情模板、悄悄話、顏色、私聊、刷新控制。其業務模型(消息類型、指令、模板、權限)設計合理,可直接映射到現代領域模型。重構關鍵在於:引入領域驅動設計分離關注點、用管道模式處理指令、模板引擎替換字符串拼接、預處理語句消除 SQL 注入、HTMLPurifier/純文本+標記語言消除 XSS、WebSocket 替換輪詢。本文提供的重構藍圖可指導完整遷移。