Files
chatroom/app/Http/Controllers/Admin/BannerBroadcastController.php

136 lines
5.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:管理员大卡片通知广播控制器
*
* 仅超级管理员chat.level:super 中间件保护)可调用此接口,
* 通过 BannerNotification 事件向指定用户或房间推送自定义大卡通知。
*
* 安全保证:
* - 路由被 ['chat.auth', 'chat.has_position', 'chat.level:super'] 三层中间件保护
* - 普通用户无权访问此接口,无法伪造对他人的广播
* - options 中的用户输入字段在后端统一降级为纯文本 / 白名单样式值
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers\Admin;
use App\Events\BannerNotification;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 类功能:安全地下发大卡片广播消息。
*/
class BannerBroadcastController extends Controller
{
/**
* 向指定目标广播大卡片通知。
*
* 请求参数:
* - target: 'user' | 'room'
* - target_id: 用户名 或 房间 ID
* - options: 与 window.chatBanner.show() 参数相同的对象
* - icon, title, name, body, sub, gradient(array), titleColor, autoClose, buttons(array)
*/
public function send(Request $request): JsonResponse
{
$validated = $request->validate([
'target' => ['required', 'in:user,room'],
'target_id' => ['required'],
'options' => ['required', 'array'],
'options.icon' => ['nullable', 'string', 'max:20'],
'options.title' => ['nullable', 'string', 'max:50'],
'options.name' => ['nullable', 'string', 'max:100'],
'options.body' => ['nullable', 'string', 'max:500'],
'options.sub' => ['nullable', 'string', 'max:200'],
'options.gradient' => ['nullable', 'array', 'max:5'],
'options.gradient.*' => ['nullable', 'string', 'max:30'],
'options.titleColor' => ['nullable', 'string', 'max:30'],
'options.autoClose' => ['nullable', 'integer', 'min:0', 'max:30000'],
'options.buttons' => ['nullable', 'array', 'max:4'],
'options.buttons.*.label' => ['nullable', 'string', 'max:30'],
'options.buttons.*.color' => ['nullable', 'string', 'max:30'],
'options.buttons.*.action' => ['nullable', 'string', 'max:20'],
]);
// 所有可见文案一律降级为纯文本,避免允许标签残留属性后在前端 innerHTML 中执行。
$opts = $validated['options'];
foreach (['title', 'name', 'body', 'sub'] as $field) {
if (isset($opts[$field])) {
$opts[$field] = $this->sanitizeBannerText($opts[$field]);
}
}
if (isset($opts['titleColor'])) {
$opts['titleColor'] = $this->sanitizeCssValue($opts['titleColor'], '#fde68a');
}
if (! empty($opts['gradient'])) {
$opts['gradient'] = array_values(array_map(
fn ($color) => $this->sanitizeCssValue($color, '#4f46e5'),
$opts['gradient']
));
}
// 按钮 label 与颜色都只允许安全文本 / 颜色值。
if (! empty($opts['buttons'])) {
$opts['buttons'] = array_map(function ($btn) {
$btn['label'] = $this->sanitizeBannerText($btn['label'] ?? '');
$btn['color'] = $this->sanitizeCssValue($btn['color'] ?? '#10b981', '#10b981');
// action 只允许预定义值,防止注入任意 JS
$btn['action'] = in_array($btn['action'] ?? '', ['close', 'add_friend', 'remove_friend', 'link'])
? $btn['action'] : 'close';
return $btn;
}, $opts['buttons']);
}
broadcast(new BannerNotification(
target: $validated['target'],
targetId: $validated['target_id'],
options: $opts,
));
return response()->json(['status' => 'success', 'message' => '广播已发送']);
}
/**
* 将 Banner 文案净化为安全纯文本。
*/
private function sanitizeBannerText(?string $text): string
{
return trim(strip_tags((string) $text));
}
/**
* 清洗颜色 / 渐变等 CSS 值,阻断样式属性注入。
*/
private function sanitizeCssValue(?string $value, string $default): string
{
$sanitized = strtolower(trim((string) $value));
if ($sanitized === '' || preg_match('/(?:javascript|expression|url\s*\(|data:|var\s*\()/i', $sanitized)) {
return $default;
}
$allowedPatterns = [
'/^#[0-9a-f]{3,8}$/i',
'/^rgba?\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/i',
'/^hsla?\(\s*\d{1,3}(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/i',
'/^(?:white|black|red|blue|green|gray|grey|yellow|orange|pink|purple|teal|cyan|indigo|amber|emerald|transparent|currentcolor)$/i',
];
foreach ($allowedPatterns as $allowedPattern) {
if (preg_match($allowedPattern, $sanitized)) {
return $sanitized;
}
}
return $default;
}
}