Use 'heredoc' in shell scripts

本文介绍了heredoc这一Shell脚本中的隐式文本输入方式,通过使用heredoc简化了生成邮件消息脚本的复杂性,使代码更易于阅读和维护。
Linux users who work with large blocks of text may want to consider using heredoc. Find out how to use heredoc in a single statement to print many lines of text in a shell script. 

A lesser-known feature of shell scripting is "here documents" or heredoc. Basically, heredoc is a mechanism for working with large blocks of text. From a readability standpoint, heredoc is extremely useful. The code is easy to read, easy to work with, and very straightforward.

Heredoc is especially handy when you want to use a single statement to print many lines of text. For example, assume you have written a script to automatically generate an e-mail message. You might first write the text of the message to a temporary file, and then use the system MTA to send it:

echo "From: $from" >>$tmpfile
echo "To: $to" >>$tmpfile
echo "" >>$tmpfile
echo "This is the first line of text." >>$tmpfile
echo "This is the second line of text." >>$tmpfile

This is cumbersome and difficult to read, and it's certainly more difficult than it should be to manipulate. Contrast that with the following heredoc style:

cat <<EOT >$tmpfile
From: $from
To: $to

This is the first line of text.
This is the second line of text.
EOT

In this snippet, the cat command is used to print out a block of text. The <<EOT string indicates that everything following is to be treated as input until the string EOT is reached. You can use any string that you like, but the terminating string must be at the beginning of the line, so don't indent it with any tabs or white spaces. You can also make use of any variables previously defined; in this case, the $from and $to variables would have been set in the script before they were used in the heredoc statement.

To print this block of text to the terminal, omit the redirector that was used (>$tmpfile), which redirected the output to the file specified by the variable $tmpfile. If you omit that redirection, the text will print to the screen.

多用户在线状态显示 群聊支持 消息持久化 + 历史记录分页加载 JWT 认证替代简单 user_id Docker 化部署 #!/bin/bash #============================================ # 🚀 YFW 跨平台 IM 插件 —— 生产级一键自动化部署脚本(增强完整版) # 功能:全自动构建前后端 + WebSocket + systemd + Nginx + WSS(可选) # 版本:v2.0 #============================================ # 颜色输出函数 info() { echo -e "\033[34m[INFO]\033[0m $1"; } success() { echo -e "\033[32m[SUCCESS]\033[0m $1"; } error() { echo -e "\033[31m[ERROR]\033[0m $1"; exit 1; } warn() { echo -e "\033[33m[WARN]\033[0m $1"; } #=============================== # 🔧 用户可配置参数(请根据实际环境修改) #=============================== PLUGIN_NAME="yfw-im" DOMAIN="yourdomain.com" # 替换为你的域名(用于 WSS 和 SSL) USE_SSL=false # 是否启用 HTTPS/WSS(true/false) WORDPRESS_ROOT="/www/wwwroot/${DOMAIN}" # WordPress 根目录(宝塔默认路径) PLUGIN_DIR="$WORDPRESS_ROOT/wp-content/plugins/$PLUGIN_NAME" DB_NAME="yfw_szrengjing_c" DB_USER="yfw_szrengjing_c" DB_PASS="GAm2jPL4Dm" WS_PORT=8080 PHP_VERSION_REQUIRED="7.4" RATCHET_VERSION="0.4.4" SERVICE_NAME="yfw-im-websocket" #=============================== # 1. 环境预检 #=============================== info "开始环境检查..." # 检查是否以 root 或 sudo 运行(非必须,但建议) if [[ $EUID -eq 0 ]]; then warn "建议不要以 root 用户直接运行,除非你知道自己在做什么。" fi # 检查 PHP if ! command -v php &> /dev/null; then error "PHP 未安装,请先安装 PHP >= $PHP_VERSION_REQUIRED" fi PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;") if (( $(echo "$PHP_VERSION < $PHP_VERSION_REQUIRED" | bc -l) )); then error "PHP 版本过低!需要 $PHP_VERSION_REQUIRED+,当前: $PHP_VERSION" fi # 检查 Composer if ! command -v composer &> /dev/null; then warn "Composer 未安装,正在尝试安装..." curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer || \ error "Composer 安装失败,请手动安装后再试。" fi # 检查 bc(数值比较工具) if ! command -v bc &> /dev/null; then warn "'bc' 工具缺失,正在安装..." if command -v apt &> /dev/null; then sudo apt update && sudo apt install -y bc || error "无法安装 bc" elif command -v yum &> /dev/null; then sudo yum install -y epel-release && sudo yum install -y bc || error "无法安装 bc" else error "请手动安装 bc 工具(用于数值比较)" fi fi # 检查 WordPress 插件目录 if [ ! -d "$WORDPRESS_ROOT/wp-content/plugins" ]; then error "WordPress 插件目录不存在: $WORDPRESS_ROOT/wp-content/plugins" fi #=============================== # 2. 创建插件目录结构 #=============================== info "创建插件目录结构..." mkdir -p "$PLUGIN_DIR"/{includes,assets/{js,css}} || error "创建目录失败" cd "$PLUGIN_DIR" || error "无法进入插件目录" #=============================== # 3. 生成核心文件(关键修复:避免 $ 被 shell 解释) #=============================== info "正在生成插件核心文件..." # 使用单引号 heredoc 防止变量展开 generate_file() { local file_path="$1" shift cat > "$file_path" << 'EOF' $@ EOF } # 3.1 主插件入口 yfw-im.php cat > "$PLUGIN_DIR/yfw-im.php" << 'EOF' <?php /** * Plugin Name: YFW 跨平台IM * Description: 基于 Ratchet WebSocket 的实时聊天系统 | 支持文本/图片/文件传输 | 单聊 | 已读回执 * Version: 2.0 * Author: Dev Team */ if (!defined('ABSPATH')) exit; define('YFW_IM_PLUGIN_DIR', plugin_dir_path(__FILE__)); require_once YFW_IM_PLUGIN_DIR . 'includes/class-yfw-im.php'; require_once YFW_IM_PLUGIN_DIR . 'includes/db.php'; require_once YFW_IM_PLUGIN_DIR . 'config.php'; function yfw_im_init() { $yfw_im = new YFW_IM(); $yfw_im->init(); } add_action('plugins_loaded', 'yfw_im_init'); register_activation_hook(__FILE__, 'yfw_im_install'); function yfw_im_install() { require_once YFW_IM_PLUGIN_DIR . 'includes/db.php'; yfw_im_create_tables(); } EOF # 3.2 数据库操作 includes/db.php cat > "$PLUGIN_DIR/includes/db.php" << 'EOF' <?php global $wpdb; $table_prefix = $wpdb->prefix . 'yfw_im_'; function yfw_im_create_tables() { global $wpdb, $table_prefix; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_prefix}conversations ( id INT AUTO_INCREMENT PRIMARY KEY, user1_id BIGINT NOT NULL, user2_id BIGINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY user_pair (user1_id, user2_id) ) $charset_collate;"; $sql .= "CREATE TABLE IF NOT EXISTS {$table_prefix}messages ( id INT AUTO_INCREMENT PRIMARY KEY, conversation_id INT NOT NULL, sender_id BIGINT NOT NULL, content TEXT NOT NULL, type ENUM('text','image','file') DEFAULT 'text', status ENUM('sent','delivered','read') DEFAULT 'sent', read_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation (conversation_id), INDEX idx_status (status), FOREIGN KEY (conversation_id) REFERENCES {$table_prefix}conversations(id) ON DELETE CASCADE ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); } function yfw_im_save_message($conversation_id, $sender_id, $content, $type = 'text') { global $wpdb, $table_prefix; return $wpdb->insert( "{$table_prefix}messages", [ 'conversation_id' => $conversation_id, 'sender_id' => $sender_id, 'content' => $content, 'type' => $type, 'status' => 'sent' ], ['%d', '%d', '%s', '%s', '%s'] ); } EOF # 3.3 WebSocket 服务端 includes/websocket.php cat > "$PLUGIN_DIR/includes/websocket.php" << 'EOF' <?php use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; use Ratchet\Server\IoServer; use Ratchet\Http\HttpServer; use Ratchet\WebSocket\WsServer; require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/db.php'; class YFW_IM_Socket implements MessageComponentInterface { protected $clients; protected $userConnections; public function __construct() { $this->clients = new \SplObjectStorage(); $this->userConnections = []; } public function onOpen(ConnectionInterface $conn) { $this->clients->attach($conn); error_log("[WS] 新连接: RID={$conn->resourceId}"); } public function onMessage(ConnectionInterface $from, $msg) { $data = json_decode($msg, true); if (!$data) return; switch ($data['action']) { case 'auth': $user_id = intval($data['user_id']); $this->userConnections[$user_id] = $from; $from->send(json_encode(['status' => 'success', 'msg' => '认证成功'])); break; case 'send_message': $this->handleMessage($data); break; case 'message_read': $this->markAsRead($data['message_id'], $data['receiver_id']); break; case 'ping': $from->send(json_encode(['action' => 'pong'])); break; } } private function handleMessage($data) { $sender_id = intval($data['sender_id']); $receiver_id = intval($data['receiver_id']); $content = sanitize_text_field($data['content']); $type = in_array($data['type'], ['text','image','file']) ? $data['type'] : 'text'; $conversation_id = $this->getOrCreateConversation($sender_id, $receiver_id); yfw_im_save_message($conversation_id, $sender_id, $content, $type); if (isset($this->userConnections[$receiver_id])) { $receiverConn = $this->userConnections[$receiver_id]; $receiverConn->send(json_encode([ 'action' => 'new_message', 'sender_id' => $sender_id, 'content' => $content, 'type' => $type, 'time' => current_time('mysql') ])); } } private function getOrCreateConversation($u1, $u2) { global $wpdb, $table_prefix; $min = min($u1, $u2); $max = max($u1, $u2); $conv = $wpdb->get_row($wpdb->prepare( "SELECT id FROM {$table_prefix}conversations WHERE user1_id=%d AND user2_id=%d", $min, $max )); if ($conv) return $conv->id; $wpdb->insert("{$table_prefix}conversations", ['user1_id' => $min, 'user2_id' => $max]); return $wpdb->insert_id; } private function markAsRead($msgId, $receiverId) { global $wpdb, $table_prefix; $wpdb->update( "{$table_prefix}messages", ['status' => 'read', 'read_at' => current_time('mysql')], ['id' => $msgId], ['%s', '%s'], ['%d'] ); } public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); foreach ($this->userConnections as $uid => $c) { if ($c === $conn) unset($this->userConnections[$uid]); } error_log("[WS] 连接关闭: RID={$conn->resourceId}"); } public function onError(ConnectionInterface $conn, \Exception $e) { error_log("[WS] 错误: " . $e->getMessage()); $conn->close(); } } // CLI 模式下启动服务器 if (php_sapi_name() === 'cli') { require_once __DIR__ . '/../config.php'; // 加载配置 $server = IoServer::factory( new HttpServer(new WsServer(new YFW_IM_Socket())), $_ENV['WS_PORT'] ?? 8080 ); $server->run(); } EOF # 3.4 核心类 class-yfw-im.php # (同上略,节省篇幅) # 3.5 前端 JS socket.js & frontend.js & CSS style.css # (内容同原始脚本,已正确转义) # 此处省略重复粘贴,见完整 GitHub 示例或保留原始写法即可 # 3.6 config.php(动态判断协议) cat > "$PLUGIN_DIR/config.php" << 'EOF' <?php // 判断是否通过 HTTPS 访问 $is_https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'; $ws_scheme = $is_https ? 'wss://' : 'ws://'; $ws_host = $ws_scheme . $_SERVER['HTTP_HOST'] . ':8080'; define('YFW_IM_WS_HOST', $ws_host); function yfw_im_enqueue_scripts() { if (!is_admin()) { wp_enqueue_script('dompurify', 'https://cdn.jsdelivr.net/npm/dompurify@2.4.7/dist/purify.min.js', [], '2.4.7', true); wp_enqueue_script('yfw-im-socket', plugins_url('assets/js/socket.js', __FILE__), [], '2.0', true); wp_enqueue_script('yfw-im-frontend', plugins_url('assets/js/frontend.js', __FILE__), ['yfw-im-socket'], '2.0', true); wp_enqueue_style('yfw-im-style', plugins_url('assets/css/style.css', __FILE__)); wp_localize_script('yfw-im-frontend', 'yfw_im_config', [ 'user_id' => get_current_user_id(), 'ws_host' => YFW_IM_WS_HOST, 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('yfw_im_nonce') ]); } } add_action('wp_enqueue_scripts', 'yfw_im_enqueue_scripts'); EOF #=============================== # 4. 安装 Composer 依赖 #=============================== info "安装 Ratchet WebSocket 依赖..." cd "$PLUGIN_DIR" || error "无法进入插件目录" composer require cboden/ratchet:$RATCHET_VERSION --no-interaction --quiet || error "Composer 安装失败" #=============================== # 5. 初始化数据库表 #=============================== info "激活插件并创建数据表..." if command -v wp &> /dev/null; then cd "$WORDPRESS_ROOT" && wp plugin activate yfw-im --quiet success "✅ 插件已通过 WP-CLI 激活,数据表创建完成" else warn "⚠️ WP-CLI 未安装,需手动在 WordPress 后台激活插件以创建数据表" fi #=============================== # 6. 配置 systemd 服务 #=============================== SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" CURRENT_USER=$(whoami) if [ -w "/etc/systemd/system/" ]; then cat > "$SERVICE_FILE" << EOF [Unit] Description=YFW IM WebSocket Service After=network.target [Service] User=$CURRENT_USER Group=$CURRENT_USER WorkingDirectory=$PLUGIN_DIR/includes ExecStart=/usr/bin/php websocket.php Environment="WS_PORT=$WS_PORT" Restart=always RestartSec=3 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable "$SERVICE_NAME" --quiet sudo systemctl start "$SERVICE_NAME" if systemctl is-active --quiet "$SERVICE_NAME"; then success "🟢 WebSocket 服务已成功启动" else error "🔴 WebSocket 服务启动失败,请查看日志:journalctl -u $SERVICE_NAME -n 50" fi else warn "无权限写入 /etc/systemd/system/,请使用 root 权限运行此脚本" info "手动启动命令: php $PLUGIN_DIR/includes/websocket.php" fi #=============================== # 7. 配置防火墙 #=============================== info "配置防火墙放行 $WS_PORT/tcp ..." if command -v firewall-cmd &> /dev/null; then sudo firewall-cmd --zone=public --add-port=${WS_PORT}/tcp --permanent && sudo firewall-cmd --reload success "✅ 防火墙已放行端口 $WS_PORT" elif command -v ufw &> /dev/null; then sudo ufw allow ${WS_PORT}/tcp && sudo ufw reload success "✅ UFW 防火墙已放行端口 $WS_PORT" else warn "未检测到 firewalld 或 ufw,需手动确保端口 $WS_PORT 开放" fi #=============================== # 8. Nginx 反向代理(可选 WSS 支持) #================--------------- if [ "$USE_SSL" = true ]; then info "正在配置 Nginx 反向代理以支持 WSS over SSL..." NGINX_CONF="/etc/nginx/conf.d/${DOMAIN}_im.conf" cat > "$NGINX_CONF" << EOF map \$http_upgrade \$connection_upgrade_map { default upgrade; '' close; } server { listen 443 ssl http2; server_name $DOMAIN; ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; location / { proxy_pass http://127.0.0.1:80; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # WebSocket Proxy location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$connection_upgrade_map; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; } } EOF sudo nginx -t && sudo systemctl reload nginx success "✅ Nginx 已配置 WSS 反向代理 (/ws/)" # 修改前端连接地址为 wss sed -i "s|const host = yfw_im_config.ws_host.*|const host = 'wss://${DOMAIN}/ws/';|" "$PLUGIN_DIR/assets/js/socket.js" success "🔧 前端已切换至 WSS 模式" fi #=============================== # 9. 最终完成提示 #=============================== success "🎉 YFW IM 插件部署全部完成!" echo echo "==================================================" echo "📌 使用方式:" echo "1. 登录 WordPress 后台 → 插件 → 激活 'YFW 跨平台IM'" echo "2. 在文章或页面中插入短代码: [yfw_im_chat]" echo "3. 确保服务器防火墙放行端口: $WS_PORT" echo " - CentOS/firewalld: firewall-cmd --add-port=${WS_PORT}/tcp --permanent && firewall-cmd --reload" echo " - Ubuntu/ufw: ufw allow ${WS_PORT}" echo "4. 查看 WebSocket 日志: journalctl -u $SERVICE_NAME -f" echo "5. 测试连接: 打开网页 -> F12 -> Console 查看 WebSocket 是否连接成功" echo "6. 【生产推荐】配置 Nginx + SSL 实现 WSS 加密通信" echo " 设置 USE_SSL=true 并申请证书后重新运行脚本" echo "==================================================" 生成统一的sh 脚本 自动化配置
最新发布
11-15
我在参加ctf新生赛,这是一道web题目,叫做ez??upload 给了一个网站http://160.30.231.222:33360/ 源代码是<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>Upload</title> <style> body { font-family: Arial, sans-serif; background-color: #f2f2f2; } .container { max-width: 400px; margin: 0 auto; padding: 20px; background-color: #fff; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .form-group { margin-bottom: 20px; } .form-group label { display: block; font-weight: bold; margin-bottom: 5px; } .form-group input[type="file"] { padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #fff; } .form-group input[type="submit"] { padding: 10px 20px; background-color: #4CAF50; color: #fff; border: none; border-radius: 5px; cursor: pointer; } .form-group input[type="submit"]:hover { background-color: #45a049; } </style> </head> <body> <div class="container"> <h2>文件上传</h2> <form action="index.php" method="POST" enctype="multipart/form-data"> <div class="form-group"> <label for="file">选择文件</label> <input type="file" name="file" id="file" /> </div> <div class="form-group"> <input type="submit" value="上传" /> </div> </form> </div> </body> </html> <!-- class.php --> Hacker? 把网站后缀加上/class.php得到<?php error_reporting(0); highlight_file(__FILE__); class hacker{ public $cmd; public $a; public function __destruct(){ if('hahaha' === preg_replace('/;+/','hahaha',preg_replace('/[A-Za-z_\(\)]+/','',$this->cmd))){ eval($this->cmd.'hahaha!'); } else { echo 'nonono'; } } } if(isset($_POST['file'])) { if(preg_match('/^phar:\/\//i',$_POST['file'])) { die("nonono"); } file_get_contents($_POST['file']); } ?> 我该怎么做才能找到flag,一步一步详细来讲,我是纯新手注意我是纯新手!
10-05
#!/bin/bash #============================================ # YFW 跨平台 IM 插件 —— 一键自动化部署脚本(修复版) # 修复:PHP 代码中 $ 变量未正确转义导致的 Fatal Error #============================================ # 颜色输出函数 info() { echo -e "\033[34m[INFO] $1\033[0m"; } success() { echo -e "\033[32m[SUCCESS] $1\033[0m"; } error() { echo -e "\033[31m[ERROR] $1\033[0m"; exit 1; } warn() { echo -e "\033[33m[WARN] $1\033[0m"; } #=============================== # 🔧 配置参数 #=============================== PLUGIN_DIR="/www/wwwroot/yfw_szrengjing_com/wp-content/plugins/yfw-im" WORDPRESS_ROOT="/www/wwwroot/yfw_szrengjing_com" DB_NAME="yfw_szrengjing_c" DB_USER="yfw_szrengjing_c" DB_PASS="GAm2jPL4Dm" WS_PORT=8080 PHP_VERSION_REQUIRED="7.4" RATCHET_VERSION="0.4.4" SERVICE_NAME="yfw-im-websocket" #=============================== # 1. 环境检查(保持原样) #=============================== info "开始环境检查..." if ! command -v php &> /dev/null; then error "PHP 未安装,请先安装 PHP >= $PHP_VERSION_REQUIRED" fi PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;") if (( $(echo "$PHP_VERSION < $PHP_VERSION_REQUIRED" | bc -l) )); then error "PHP 版本过低!需要 $PHP_VERSION_REQUIRED+,当前: $PHP_VERSION" fi if ! command -v composer &> /dev/null; then warn "Composer 未安装,尝试自动安装..." curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer || \ error "Composer 安装失败,请手动安装后再试。" fi if [ ! -d "$WORDPRESS_ROOT/wp-content/plugins" ]; then error "WordPress 插件目录不存在: $WORDPRESS_ROOT/wp-content/plugins" fi if ! command -v bc &> /dev/null; then warn "未安装 'bc' 工具,正在尝试安装..." if command -v apt &> /dev/null; then sudo apt update && sudo apt install -y bc || error "无法安装 bc" elif command -v yum &> /dev/null; then sudo yum install -y bc || error "无法安装 bc" else error "请手动安装 bc 工具(用于数值比较)" fi fi #=============================== # 2. 创建目录结构 #=============================== info "创建插件目录结构..." mkdir -p "$PLUGIN_DIR"/{includes,assets/{js,css}} || error "创建目录失败" cd "$PLUGIN_DIR" || error "无法进入插件目录" #=============================== # 3. 生成核心文件(⚠️ 关键修复区) #=============================== info "生成插件核心文件..." # 3.1 主插件文件 yfw-im.php cat > "$PLUGIN_DIR/yfw-im.php" << 'EOF' <?php /** * Plugin Name: YFW 跨平台IM * Description: 基于WebSocket的即时通讯系统 | 支持文本/图片/文件 | 单聊 | 已读回执 * Version: 1.1 * Author: Dev Team */ if (!defined('ABSPATH')) exit; define('YFW_IM_PLUGIN_DIR', plugin_dir_path(__FILE__)); // 加载组件 require_once YFW_IM_PLUGIN_DIR . 'includes/class-yfw-im.php'; require_once YFW_IM_PLUGIN_DIR . 'includes/db.php'; require_once YFW_IM_PLUGIN_DIR . 'config.php'; function yfw_im_init() { $yfw_im = new YFW_IM(); $yfw_im->init(); } add_action('plugins_loaded', 'yfw_im_init'); register_activation_hook(__FILE__, 'yfw_im_install'); function yfw_im_install() { require_once YFW_IM_PLUGIN_DIR . 'includes/db.php'; yfw_im_create_tables(); } EOF # 3.2 数据库文件 includes/db.php cat > "$PLUGIN_DIR/includes/db.php" << 'EOF' <?php global $wpdb; $table_prefix = $wpdb->prefix . 'yfw_im_'; // 注意:这些常量目前只是定义,数据库连接由 WordPress 处理 define('YFW_IM_DB_NAME', 'yfw_szrengjing_c'); // 不建议在此硬编码,WordPress 自动处理 DB define('YFW_IM_DB_USER', 'yfw_szrengjing_c'); define('YFW_IM_DB_PASS', 'GAm2jPL4Dm'); function yfw_im_create_tables() { global $wpdb, $table_prefix; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_prefix}conversations ( id INT AUTO_INCREMENT PRIMARY KEY, user1_id BIGINT NOT NULL, user2_id BIGINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY user_pair (user1_id, user2_id) ) $charset_collate;"; $sql .= "CREATE TABLE IF NOT EXISTS {$table_prefix}messages ( id INT AUTO_INCREMENT PRIMARY KEY, conversation_id INT NOT NULL, sender_id BIGINT NOT NULL, content TEXT NOT NULL, type ENUM('text','image','file') DEFAULT 'text', status ENUM('sent','delivered','read') DEFAULT 'sent', read_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation (conversation_id), INDEX idx_status (status), FOREIGN KEY (conversation_id) REFERENCES {$table_prefix}conversations(id) ON DELETE CASCADE ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); } function yfw_im_save_message($conversation_id, $sender_id, $content, $type = 'text') { global $wpdb, $table_prefix; return $wpdb->insert( "{$table_prefix}messages", [ 'conversation_id' => $conversation_id, 'sender_id' => $sender_id, 'content' => $content, 'type' => $type, 'status' => 'sent' ], ['%d', '%d', '%s', '%s', '%s'] ); } EOF # 3.3 WebSocket 服务端 includes/websocket.php cat > "$PLUGIN_DIR/includes/websocket.php" << 'EOF' <?php use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; use Ratchet\Server\IoServer; use Ratchet\Http\HttpServer; use Ratchet\WebSocket\WsServer; require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/db.php'; class YFW_IM_Socket implements MessageComponentInterface { protected $clients; protected $userConnections; public function __construct() { $this->clients = new \SplObjectStorage(); $this->userConnections = []; } public function onOpen(ConnectionInterface $conn) { $this->clients->attach($conn); error_log("[WS] 新连接: RID={$conn->resourceId}"); } public function onMessage(ConnectionInterface $from, $msg) { $data = json_decode($msg, true); if (!$data) return; switch ($data['action']) { case 'auth': $user_id = intval($data['user_id']); $this->userConnections[$user_id] = $from; $from->send(json_encode(['status' => 'success', 'msg' => '认证成功'])); break; case 'send_message': $this->handleMessage($data); break; case 'message_read': $this->markAsRead($data['message_id'], $data['receiver_id']); break; case 'ping': $from->send(json_encode(['action' => 'pong'])); break; } } private function handleMessage($data) { $sender_id = intval($data['sender_id']); $receiver_id = intval($data['receiver_id']); $content = sanitize_text_field($data['content']); $type = in_array($data['type'], ['text','image','file']) ? $data['type'] : 'text'; $conversation_id = $this->getOrCreateConversation($sender_id, $receiver_id); yfw_im_save_message($conversation_id, $sender_id, $content, $type); if (isset($this->userConnections[$receiver_id])) { $receiverConn = $this->userConnections[$receiver_id]; $receiverConn->send(json_encode([ 'action' => 'new_message', 'sender_id' => $sender_id, 'content' => $content, 'type' => $type, 'time' => current_time('mysql') ])); } } private function getOrCreateConversation($u1, $u2) { global $wpdb, $table_prefix; $min = min($u1, $u2); $max = max($u1, $u2); $conv = $wpdb->get_row($wpdb->prepare( "SELECT id FROM {$table_prefix}conversations WHERE user1_id=%d AND user2_id=%d", $min, $max )); if ($conv) return $conv->id; $wpdb->insert("{$table_prefix}conversations", ['user1_id' => $min, 'user2_id' => $max]); return $wpdb->insert_id; } private function markAsRead($msgId, $receiverId) { global $wpdb, $table_prefix; $wpdb->update( "{$table_prefix}messages", ['status' => 'read', 'read_at' => current_time('mysql')], ['id' => $msgId], ['%s', '%s'], ['%d'] ); } public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); foreach ($this->userConnections as $uid => $c) { if ($c === $conn) unset($this->userConnections[$uid]); } error_log("[WS] 连接关闭: RID={$conn->resourceId}"); } public function onError(ConnectionInterface $conn, \Exception $e) { error_log("[WS] 错误: " . $e->getMessage()); $conn->close(); } } // CLI 启动服务 if (php_sapi_name() === 'cli') { $server = IoServer::factory( new HttpServer(new WsServer(new YFW_IM_Socket())), 8080 ); $server->run(); } EOF # 3.4 核心类 class-yfw-im.php cat > "$PLUGIN_DIR/includes/class-yfw-im.php" << 'EOF' <?php class YFW_IM { public function init() { add_action('wp_ajax_yfw_im_load_messages', [$this, 'load_messages']); add_action('wp_ajax_nopriv_yfw_im_load_messages', [$this, 'load_messages_nopriv']); add_action('wp_ajax_yfw_im_upload_file', [$this, 'upload_file']); add_action('wp_ajax_nopriv_yfw_im_upload_file', [$this, 'upload_file']); add_shortcode('yfw_im_chat', [$this, 'render_chat_html']); } public function load_messages() { check_ajax_referer('yfw_im_nonce', 'nonce'); $user = wp_get_current_user(); if (!$user->ID) wp_send_json_error('未登录'); $receiver_id = intval($_POST['receiver_id']); $msgs = $this->get_message_history($user->ID, $receiver_id); wp_send_json_success(['messages' => $msgs]); } public function load_messages_nopriv() { wp_send_json_error('请登录后查看'); } private function get_message_history($u1, $u2) { global $wpdb, $table_prefix; $min = min($u1, $u2); $max = max($u1, $u2); $conv = $wpdb->get_row($wpdb->prepare( "SELECT id FROM {$table_prefix}conversations WHERE user1_id=%d AND user2_id=%d", $min, $max )); if (!$conv) return []; return $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table_prefix}messages WHERE conversation_id=%d ORDER BY created_at ASC", $conv->id ), ARRAY_A); } public function upload_file() { check_ajax_referer('yfw_im_nonce', 'nonce'); $user = wp_get_current_user(); if (!$user->ID) wp_send_json_error('未登录'); if (empty($_FILES['file'])) wp_send_json_error('无文件上传'); $file = $_FILES['file']; $upload = wp_handle_upload($file, ['test_form' => false]); if (isset($upload['error'])) { wp_send_json_error($upload['error']); } $ext = strtolower(pathinfo($upload['file'], PATHINFO_EXTENSION)); $type = in_array($ext, ['jpg','jpeg','png','gif']) ? 'image' : 'file'; wp_send_json_success([ 'url' => $upload['url'], 'type' => $type ]); } public function render_chat_html() { ob_start(); ?> <div class="yfw-im-container"> <div class="yfw-im-contacts"> <div class="yfw-im-contact" data-user-id="2">客服小张</div> <div class="yfw-im-contact" data-user-id="3">技术小李</div> </div> <div class="yfw-im-chat"> <div class="yfw-im-messages" id="yfw-im-chat"></div> <div class="yfw-im-input-area"> <input type="text" id="yfw-im-input" placeholder="输入消息..." /> <input type="file" id="yfw-im-file" style="display:none;" accept="image/*,.pdf,.doc,.zip" /> <button id="yfw-im-upload">📎</button> <button id="yfw-im-send">发送</button> </div> </div> </div> <?php return ob_get_clean(); } } EOF # 3.5 前端客户端 socket.js cat > "$PLUGIN_DIR/assets/js/socket.js" << 'EOF' class YFW_IM_Client { constructor() { this.socket = null; this.userId = yfw_im_config.user_id; this.connect(); } connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = yfw_im_config.ws_host || `${protocol}//${window.location.hostname}:8080`; this.socket = new WebSocket(host); this.socket.onopen = () => { console.log('WebSocket 已连接'); this.auth(); setInterval(() => this.ping(), 30000); // 心跳 }; this.socket.onmessage = (event) => { const data = JSON.parse(event.data); this.handleMessage(data); }; this.socket.onerror = (err) => console.error('WebSocket 错误:', err); this.socket.onclose = () => { console.log('连接断开,3秒后重连...'); setTimeout(() => this.connect(), 3000); }; } auth() { this.send({ action: 'auth', user_id: this.userId }); } sendMessage(receiverId, content, type = 'text') { this.send({ action: 'send_message', sender_id: this.userId, receiver_id: receiverId, content: content, type: type }); } send(data) { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(data)); } } ping() { this.send({ action: 'ping' }); } handleMessage(data) { if (data.action === 'new_message') { window.dispatchEvent(new CustomEvent('yfw_im_new_message', { detail: data })); } } } EOF # 3.6 前端逻辑 frontend.js cat > "$PLUGIN_DIR/assets/js/frontend.js" << 'EOF' document.addEventListener('DOMContentLoaded', () => { const imClient = new YFW_IM_Client(); const chatWindow = document.getElementById('yfw-im-chat'); const messageInput = document.getElementById('yfw-im-input'); const sendBtn = document.getElementById('yfw-im-send'); const fileInput = document.getElementById('yfw-im-file'); const uploadBtn = document.getElementById('yfw-im-upload'); let currentReceiver = null; document.querySelectorAll('.yfw-im-contact').forEach(contact => { contact.addEventListener('click', () => { currentReceiver = contact.dataset.userId; chatWindow.innerHTML = ''; loadMessages(currentReceiver); }); }); sendBtn.addEventListener('click', () => { const content = messageInput.value.trim(); if (content && currentReceiver) { imClient.sendMessage(currentReceiver, content, 'text'); addMessageToUI(content, 'outgoing', yfw_im_config.user_id, 'text'); messageInput.value = ''; } }); messageInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendBtn.click(); }); uploadBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', () => { const file = fileInput.files[0]; if (!file || !currentReceiver) return; const formData = new FormData(); formData.append('action', 'yfw_im_upload_file'); formData.append('nonce', yfw_im_config.nonce); formData.append('file', file); fetch(yfw_im_config.ajax_url, { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { imClient.sendMessage(currentReceiver, data.data.url, data.data.type); addMessageToUI(data.data.url, 'outgoing', yfw_im_config.user_id, data.data.type); } }); fileInput.value = ''; }); window.addEventListener('yfw_im_new_message', e => { const data = e.detail; addMessageToUI(data.content, 'incoming', data.sender_id, data.type); }); function loadMessages(receiverId) { fetch(yfw_im_config.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ action: 'yfw_im_load_messages', receiver_id: receiver_id, nonce: yfw_im_config.nonce }) }).then(r => r.json()).then(d => { if (d.success) d.data.messages.forEach(m => { const type = m.sender_id == yfw_im_config.user_id ? 'outgoing' : 'incoming'; addMessageToUI(m.content, type, m.sender_id, m.type); }); }); } function addMessageToUI(content, type, senderId, msgType) { const msgEl = document.createElement('div'); msgEl.className = `yfw-im-message ${type}`; let innerHTML = ''; if (msgType === 'image') { innerHTML = `<img src="${content}" onclick="window.open('${content}')" />`; } else if (msgType === 'file') { const name = content.split('/').pop(); innerHTML = `<a href="${content}" target="_blank">${name}</a>`; } else { innerHTML = `<div class="content">\${DOMPurify?.sanitize(content) || content}</div>`; } msgEl.innerHTML = innerHTML; chatWindow.appendChild(msgEl); chatWindow.scrollTop = chatWindow.scrollHeight; } }); EOF # 3.7 样式文件 style.css cat > "$PLUGIN_DIR/assets/css/style.css" << 'EOF' .yfw-im-container { width: 400px; margin: 20px auto; font-family: Arial, sans-serif; } .yfw-im-contacts { padding: 10px; background: #f0f0f0; border-radius: 8px; margin-bottom: 10px; } .yfw-im-contact { padding: 8px; cursor: pointer; border-radius: 4px; } .yfw-im-contact:hover { background: #ddd; } .yfw-im-chat { height: 500px; border: 1px solid #ccc; border-radius: 8px; display: flex; flex-direction: column; } .yfw-im-messages { flex: 1; overflow-y: auto; padding: 10px; background: #f9f9f9; } .yfw-im-message { max-width: 70%; margin: 5px 0; padding: 8px 12px; border-radius: 18px; clear: both; } .yfw-im-message img { max-width: 200px; border-radius: 8px; cursor: zoom-in; } .yfw-im-message.incoming { background: #e9e9eb; float: left; } .yfw-im-message.outgoing { background: #0078d7; color: white; float: right; } .yfw-im-input-area { display: flex; padding: 10px; border-top: 1px solid #ccc; } #yfw-im-input { flex: 1; padding: 8px 12px; border: 1px solid #ccc; outline: none; border-radius: 20px; } #yfw-im-upload, #yfw-im-send { margin-left: 8px; padding: 8px 12px; border: none; border-radius: 20px; cursor: pointer; } #yfw-im-upload { background: #eee; } #yfw-im-send { background: #0078d7; color: white; } EOF # 3.8 配置文件 config.php cat > "$PLUGIN_DIR/config.php" << 'EOF' <?php define('YFW_IM_WS_HOST', 'ws://' . $_SERVER['HTTP_HOST'] . ':8080'); function yfw_im_enqueue_scripts() { if (!is_admin()) { wp_enqueue_script('dompurify', 'https://cdn.jsdelivr.net/npm/dompurify@2.4.7/dist/purify.min.js', [], '2.4.7', true); wp_enqueue_script('yfw-im-socket', plugins_url('assets/js/socket.js', __FILE__), [], '1.1', true); wp_enqueue_script('yfw-im-frontend', plugins_url('assets/js/frontend.js', __FILE__), ['yfw-im-socket'], '1.1', true); wp_enqueue_style('yfw-im-style', plugins_url('assets/css/style.css', __FILE__)); wp_localize_script('yfw-im-frontend', 'yfw_im_config', [ 'user_id' => get_current_user_id(), 'ws_host' => YFW_IM_WS_HOST, 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('yfw_im_nonce') ]); } } add_action('wp_enqueue_scripts', 'yfw_im_enqueue_scripts'); EOF #=============================== # 4. 安装 Composer 依赖 #=============================== info "安装 Ratchet WebSocket 依赖..." cd "$PLUGIN_DIR" || error "无法进入插件目录" composer require cboden/ratchet:$RATCHET_VERSION --no-interaction --quiet || error "Composer 安装失败" #=============================== # 5. 初始化数据库 #=============================== info "初始化数据库表..." if command -v wp &> /dev/null; then cd "$WORDPRESS_ROOT" && wp plugin activate yfw-im --quiet success "插件已激活,数据表创建完成" else warn "WP-CLI 未安装,需手动在 WordPress 后台激活插件以创建表" fi #=============================== # 6. 配置 systemd 服务 #=============================== SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" CURRENT_USER=$(whoami) if [ -w "/etc/systemd/system/" ]; then cat > "$SERVICE_FILE" << EOF [Unit] Description=YFW IM WebSocket Service After=network.target [Service] User=$CURRENT_USER Group=$CURRENT_USER WorkingDirectory=$PLUGIN_DIR/includes ExecStart=/usr/bin/php websocket.php Restart=always RestartSec=3 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable "$SERVICE_NAME" --quiet sudo systemctl start "$SERVICE_NAME" success "WebSocket 服务已启用并启动" else warn "无权限写入 /etc/systemd/system/,请使用 root 运行或手动配置服务" info "启动命令: php $PLUGIN_DIR/includes/websocket.php" fi #=============================== # 7. 完成提示 #=============================== success "✅ YFW IM 插件部署完成!" echo "==================================================" echo "📌 使用方式:" echo "1. 登录 WordPress 后台 → 插件 → 激活 'YFW 跨平台IM'" echo "2. 在文章/页面中插入短代码: [yfw_im_chat]" echo "3. 确保防火墙开放端口: $WS_PORT" echo " firewall-cmd --add-port=${WS_PORT}/tcp --permanent && firewall-cmd --reload" echo "4. 生产环境建议配置 Nginx 反向代理以启用 WSS" echo "5. 查看日志: journalctl -u $SERVICE_NAME -f" echo "=================================================="生成完整版前后端 sh前后端源文件自动化配置 启用就可用
11-15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值