├── data └── .gitignore ├── email-tpl ├── .gitignore └── default.html ├── app ├── Functions.php ├── ArtalkServer.php ├── components │ ├── Http.php │ ├── Permission.php │ ├── Table.php │ ├── AdminAction.php │ ├── Captcha.php │ └── Action.php └── Utils.php ├── .gitignore ├── public └── index.php ├── .editorconfig ├── composer.json ├── README.md ├── Config.example.php ├── composer.lock └── LICENSE /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /email-tpl/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !default.html 4 | -------------------------------------------------------------------------------- /app/Functions.php: -------------------------------------------------------------------------------- 1 | Hi, {{comment.nick}}:

2 |

3 | 您在 {{conf.site_name}} 收到了回复: 4 |

{{reply.content_html}}
5 |

6 |

传送门 >

7 |

Powered By qwqcode/Artalk

8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qwqcode/artalk-server-php", 3 | "version": "0.0.3", 4 | "type": "project", 5 | "license": "GPL-2.0", 6 | "authors": [ 7 | { 8 | "name": "qwqcode", 9 | "email": "qwqcode@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "PHP": ">=7.1.3", 14 | "ext-curl": "*", 15 | "ext-json": "*", 16 | "greg0/lazer-database": "^2.0", 17 | "gregwar/captcha": "^1.1", 18 | "phpmailer/phpmailer": "^6.1.5" 19 | }, 20 | "scripts": { 21 | "dev": "php -S localhost:23366 -t public" 22 | }, 23 | "config": { 24 | "process-timeout": 0 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "app\\": "app/" 29 | }, 30 | "files": [ 31 | "app/Functions.php" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Artalk Server PHP 2 | 3 | > [Artalk](https://artalk.js.org) 的后端,PHP 版 4 | 5 | - 轻巧(使用 JSON 文件存储数据) 6 | - 支持 SMTP / 阿里云DM(Http API) / [qwqaq-email-server](https://github.com/qwqcode/qwqaq-email-server) 发送邮件通知 7 | - 管理员防冒 8 | - 提交频繁验证码 9 | 10 | ## 需求 11 | 12 | - PHP >=7.1.3 13 | - ~~MySql~~(无需 SQL 数据库,JSON 文件存储数据) 14 | 15 | ## 部署 16 | 17 | ```bash 18 | git clone https://github.com/qwqcode/ArtalkServerPhp.git 19 | composer install 20 | php -r "copy('Config.example.php', 'Config.php');" 21 | ``` 22 | 23 | 之后: 24 | 25 | 1. 打开 `/Config.php` 文件,按照注释来配置 26 | 2. 修改前端页面 Artalk 配置 `serverUrl` 为文件 `/public/index.php` 外部可访问的 URL,例如: 27 | 28 | ```js 29 | new Artalk({ 30 | // ... 31 | serverUrl: 'https://example.com/index.php' 32 | }); 33 | ``` 34 | 35 | ## 注意事项 36 | 37 | ### 安全性 38 | 39 | > 您需要阻止用户访问 `/data/` 这个目录,因为该目录下的文件中包含用户的个人信息:邮箱、IP 地址 等... 40 | 41 | 通用方法 42 | 43 | ``` 44 | 若本程序存在于单独的域名下,您可以直接设置网站根目录为 /public 45 | ``` 46 | 47 | Apache 配置 48 | 49 | ```conf 50 | RewriteEngine on 51 | RewriteRule ^data/* - [F,L] 52 | ``` 53 | 54 | Nginx 配置 55 | 56 | ```conf 57 | location ~ /data/.* { 58 | deny all; 59 | return 403; 60 | } 61 | ``` 62 | 63 | ## 开发 64 | 65 | 1. 命令行敲入 `composer dev` 66 | 2. 浏览器访问 http://localhost:23366 67 | -------------------------------------------------------------------------------- /Config.example.php: -------------------------------------------------------------------------------- 1 | 'XXX 的博客', 5 | // 支持跨域访问的域名 6 | 'allow_origin' => [ 7 | 'http://localhost:8080' // 或 '*' 跨域无限制 8 | ], 9 | // 管理员用户 10 | 'admin_users' => [ 11 | ['nick' => 'admin', 'email' => 'admin@example.com', 'password' => '', 'badge_name' => '管理员', 'badge_color' => '#FF6C00'] 12 | ], 13 | // 评论审核 14 | 'moderation' => [ 15 | 'pending_default' => false, // 发表新评论默认为 “待审状态” 16 | ], 17 | // 验证码 18 | 'captcha' => [ 19 | 'on' => true, // 总开关 20 | // ↓↓ 在 {timeout} 秒内,若再次评论超过 {limit} 次则需要验证码 21 | 'timeout' => 4*60, // 超时:重置评论次数统计(单位:秒) 22 | 'limit' => 3, // 激活验证码的评论次数(设置为 0 总是需要验证码) 23 | ], 24 | // 邮件通知 25 | 'email' => [ 26 | 'on' => true, // 总开关 27 | 'admin_addr' => 'example@example.com', // 管理员邮箱地址(文章收到评论时通知) 28 | 'sender_type' => 'smtp', // 发送方式(ali_dm or smtp) 29 | 'mail_title' => '您在 [站名] 收到了新的回复', 30 | 'mail_title_to_admin' => '您的文章收到了新的回复', 31 | 'mail_tpl_name' => 'default.html', // 邮件模板文件(/email-tpl 目录下存放) 32 | 'ali_dm' => [ 33 | // https://help.aliyun.com/document_detail/29414.html 34 | 'AccessKeyId' => '', // 阿里云颁发给用户的访问服务所用的密钥 ID 35 | 'AccessKeySecret' => '', // 用于加密的密钥 36 | 'AccountName' => 'example@example.com', // 管理控制台中配置的发信地址 37 | ], 38 | 'smtp' => [ 39 | 'Host' => 'smtp.qq.com', 40 | 'Port' => 465, 41 | 'SMTPAuth' => true, 42 | 'Username' => 'example@qq.com', 43 | 'Password' => '', 44 | 'SMTPSecure' => 'ssl', 45 | 'FromAddr' => 'example@qq.com', // 发件人邮箱 46 | 'FromName' => '站名', // 发件人显示的名称 47 | ] 48 | ] 49 | ]; 50 | -------------------------------------------------------------------------------- /app/ArtalkServer.php: -------------------------------------------------------------------------------- 1 | version = '(unknow)'; 21 | 22 | $composerFile = __DIR__ . '/../composer.json'; 23 | if (file_exists($composerFile)) { 24 | $composerJson = @file_get_contents(__DIR__ . '/../composer.json'); 25 | $composerJson = @json_decode($composerJson, true); 26 | $this->version = $composerJson['version'] ?? '(unknow)'; 27 | } 28 | 29 | $this->allowOriginControl(); 30 | $this->initTables(); 31 | 32 | $actionName = $_GET['action'] ?? $_POST['action'] ?? null; 33 | $methodName = "action{$actionName}"; 34 | if (method_exists($this, $methodName)) { 35 | $result = $this->{$methodName}(); 36 | } else { 37 | // action 参数不正确显示 38 | if (!$this->wantsJson()) { 39 | header('Content-Type: text/plain'); 40 | echo "Artalk Server Php v{$this->version}\n\n" 41 | . " > https://github.com/ArtalkJS/ArtalkServerPhp\n" 42 | . " > https://artalk.js.org\n" 43 | . " > https://github.com/ArtalkJS/Artalk"; 44 | return; 45 | } else { 46 | $result = $this->error('这是哪?我要干什么?现在几点?蛤?什么鬼!?(╯‵□′)╯︵┴─┴'); 47 | } 48 | } 49 | 50 | $this->response($result); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/components/Http.php: -------------------------------------------------------------------------------- 1 | true, 44 | 'msg' => $msg, 45 | 'data' => $data 46 | ]; 47 | } 48 | 49 | private function error($msg = null, $data = null) 50 | { 51 | return [ 52 | 'success' => false, 53 | 'msg' => $msg, 54 | 'data' => $data 55 | ]; 56 | } 57 | 58 | private function response($data) 59 | { 60 | header('Content-Type: application/json'); 61 | echo json_encode($data, JSON_UNESCAPED_UNICODE); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/components/Permission.php: -------------------------------------------------------------------------------- 1 | getUserNick().$this->getUserEmail().$this->getUserIp()); 12 | } 13 | 14 | private function getUserEmail() { 15 | return trim($_POST['email'] ?? ''); 16 | } 17 | 18 | private function getUserNick() { 19 | return trim($_POST['nick'] ?? ''); 20 | } 21 | 22 | private function getUserPassword() { 23 | return trim($_POST['password'] ?? ''); 24 | } 25 | 26 | private function isAdmin($nick, $email) 27 | { 28 | if (empty($this->getAdminUsers())) 29 | return false; 30 | 31 | if (empty($this->findAdminUser($nick, $email))) 32 | return false; 33 | 34 | return true; 35 | } 36 | 37 | private function checkAdminPassword($nick, $email, $password) 38 | { 39 | $password = trim($password); 40 | $user = $this->findAdminUser($nick, $email); 41 | if (!empty($user) && $password === trim($user['password'])) { 42 | return true; 43 | } else { 44 | return false; 45 | } 46 | } 47 | 48 | private function getAdminUsers() { 49 | return _config()['admin_users'] ?? []; 50 | } 51 | 52 | private function findAdminUser($nick, $email) 53 | { 54 | $nick = trim($nick); 55 | $email = trim($email); 56 | 57 | $adminUsers = $this->getAdminUsers(); 58 | if (empty($adminUsers)) { 59 | return null; 60 | } 61 | 62 | $user = []; 63 | foreach ($adminUsers as $i => $item) { 64 | if (strtolower($item['nick']) === strtolower($nick) || strtolower($item['email']) === strtolower($email)) { 65 | $user = $item; 66 | break; 67 | } 68 | } 69 | 70 | return $user; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/components/Table.php: -------------------------------------------------------------------------------- 1 | exists(); 12 | } catch(\Lazer\Classes\LazerException $e){ 13 | Lazer::create('comments', [ 14 | 'id' => 'integer', 15 | 'content' => 'string', 16 | 'nick' => 'string', 17 | 'email' => 'string', 18 | 'link' => 'string', 19 | 'ua' => 'string', 20 | 'page_key' => 'string', 21 | 'rid' => 'integer', 22 | 'ip' => 'string', 23 | 'date' => 'string', 24 | 'is_collapsed' => 'boolean', 25 | 'is_pending' => 'boolean', 26 | ]); 27 | } 28 | // captcha 验证码数据 29 | try { 30 | \Lazer\Classes\Helpers\Validate::table('captcha')->exists(); 31 | } catch(\Lazer\Classes\LazerException $e){ 32 | Lazer::create('captcha', [ 33 | 'ip' => 'string', 34 | 'str' => 'string', 35 | ]); 36 | } 37 | // action_logs 操作记录 38 | try { 39 | \Lazer\Classes\Helpers\Validate::table('action_logs')->exists(); 40 | } catch(\Lazer\Classes\LazerException $e){ 41 | Lazer::create('action_logs', [ 42 | 'user' => 'string', // 用户唯一标识 43 | 'ip' => 'string', // IP 地址 44 | 'count' => 'integer', // 操作次数 45 | 'last_time' => 'string', // 最后一次操作时间 46 | 'ua' => 'string', // UserAgent 47 | 'is_admin' => 'boolean', // 是否为管理员 48 | 'is_ban' => 'boolean', // 是否被封禁 49 | 'note' => 'string' // 备注 50 | ]); 51 | } 52 | // pages 页面配置数据 53 | try { 54 | \Lazer\Classes\Helpers\Validate::table('pages')->exists(); 55 | } catch(\Lazer\Classes\LazerException $e){ 56 | Lazer::create('pages', [ 57 | 'page_key' => 'string', 58 | 'is_close_comment' => 'boolean', 59 | ]); 60 | } 61 | } 62 | 63 | public static function getCommentsTable() 64 | { 65 | return Lazer::table('comments'); 66 | } 67 | 68 | public static function getCaptchaTable() 69 | { 70 | return Lazer::table('captcha'); 71 | } 72 | 73 | public static function getActionLogsTable() 74 | { 75 | return Lazer::table('action_logs'); 76 | } 77 | 78 | 79 | public static function getPagesTable() 80 | { 81 | return Lazer::table('pages'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/components/AdminAction.php: -------------------------------------------------------------------------------- 1 | getUserNick(); 19 | $email = $this->getUserEmail(); 20 | $password = $this->getUserPassword(); 21 | if ($nick == '') return $this->error('昵称 不能为空'); 22 | if ($email == '') return $this->error('邮箱 不能为空'); 23 | if ($password == '') return $this->error('密码 不能为空'); 24 | 25 | if ($this->isNeedCaptcha()) { 26 | $imgData = $this->refreshGetCaptcha(); // 生成新的验证码 27 | return $this->error('需要验证码', ['need_captcha' => true, 'img_data' => $imgData]); 28 | } 29 | 30 | $this->logAction(); // 记录一次 IP 操作 31 | 32 | if (!$this->isAdmin($nick, $email)) { 33 | return $this->error('非管理员无需进行身份认证'); 34 | } 35 | 36 | if ($this->checkAdminPassword($nick, $email, $password)) { 37 | $this->actionLogMarkAdmin(); // 标记为管理员 38 | return $this->success('密码正确'); 39 | } else { 40 | return $this->error('密码错误'); 41 | } 42 | } 43 | 44 | public function NeedAdmin () { 45 | $nick = $this->getUserNick(); 46 | $email = $this->getUserEmail(); 47 | $password = $this->getUserPassword(); 48 | 49 | if ($this->isNeedCaptcha()) { 50 | $imgData = $this->refreshGetCaptcha(); // 生成新的验证码 51 | return $this->error('需要验证码', ['need_captcha' => true, 'img_data' => $imgData]); // 防止密码被暴力破解 (TODO: 密码错误逐渐提高限制) 52 | } 53 | 54 | $this->logAction(); // 记录一次 IP 操作 55 | 56 | if (!$this->isAdmin($nick, $email)) { 57 | $this->response($this->error('需要管理员身份', ['need_password' => true])); 58 | exit(); 59 | } 60 | 61 | if (!$this->checkAdminPassword($nick, $email, $password)) { 62 | $this->response($this->error('需要管理员身份', ['need_password' => true])); 63 | exit(); 64 | } 65 | } 66 | 67 | /** 68 | * Action: CommentCollapse 69 | * Desc : 评论折叠 70 | */ 71 | public function actionCommentCollapse() 72 | { 73 | $this->NeedAdmin(); 74 | 75 | $id = intval(trim($_POST['id'] ?? 0)); 76 | if (empty($id)) return $this->error('id 不能为空'); 77 | $isCollapsed = boolval(trim($_POST['is_collapsed'] ?? 1)); // 1为折叠,0为取消折叠 78 | 79 | $commentTable = self::getCommentsTable(); 80 | $comment = $commentTable->where('id', '=', $id)->find(); 81 | 82 | if ($comment->count() === 0) { 83 | return $this->error("未找到 ID 为 {$id} 的评论项"); 84 | } 85 | 86 | $comment->is_collapsed = $isCollapsed; 87 | $comment->save(); 88 | 89 | return $this->success($isCollapsed ? '评论已折叠' : '评论已取消折叠', [ 90 | 'id' => $id, 91 | 'is_collapsed' => $isCollapsed 92 | ]); 93 | } 94 | 95 | /** 96 | * Action: CommentDel 97 | * Desc : 评论删除 98 | */ 99 | public function actionCommentDel() 100 | { 101 | $this->NeedAdmin(); 102 | 103 | // 评论项 ID 104 | $id = intval(trim($_POST['id'] ?? 0)); 105 | if (empty($id)) return $this->error('id 不能为空'); 106 | 107 | $commentTable = self::getCommentsTable(); 108 | 109 | if ($commentTable->where('id', '=', $id)->find()->count() === 0) { 110 | return $this->error("未找到 ID 为 {$id} 的评论项,或已删除"); 111 | } 112 | 113 | $delTotal = 0; 114 | 115 | try { 116 | $commentTable->where('id', '=', $id)->find()->delete(); 117 | $delTotal++; 118 | } catch (\Exception $ex) { 119 | return $this->error('删除评论时出现错误'.$ex); 120 | } 121 | 122 | // 删除所有子评论 123 | $QueryAndDelChild = function ($parentId) use (&$commentTable, &$QueryAndDelChild, &$delTotal) { 124 | $comments = $commentTable 125 | ->where('rid', '=', $parentId) 126 | ->findAll(); 127 | 128 | foreach ($comments as $item) { 129 | $QueryAndDelChild($item->id); 130 | try { 131 | $commentTable->where('id', '=', $item->id)->find()->delete(); 132 | $delTotal++; 133 | } catch (\Exception $ex) {} 134 | } 135 | }; 136 | $QueryAndDelChild($id); 137 | 138 | return $this->success('评论已删除', [ 139 | 'del_total' => $delTotal 140 | ]); 141 | } 142 | 143 | /** 144 | * Action: SetPage 145 | * Desc : 设置页面配置数据 146 | */ 147 | public function actionSetPage() 148 | { 149 | $this->NeedAdmin(); 150 | 151 | // 评论项 ID 152 | $pageKey = trim($_POST['page_key'] ?? ''); 153 | if ($pageKey == '') return $this->error('page_key 不能为空'); 154 | $isCloseComment = boolval(trim($_POST['is_close_comment'] ?? 1)); // 1为关闭评论,0为打开评论 155 | 156 | $page = self::getPagesTable()->where('page_key', '=', $pageKey)->find(); 157 | if ($page->count() === 0) { 158 | $page = self::getPagesTable(); 159 | $page->page_key = $pageKey; 160 | } 161 | $page->is_close_comment = $isCloseComment; 162 | $page->save(); 163 | 164 | $page = self::getPagesTable()->where('page_key', '=', $pageKey)->findAll()->asArray(); 165 | return $this->success('页面已更新', $page[0] ?? []); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/components/Captcha.php: -------------------------------------------------------------------------------- 1 | isCaptchaOn()) 28 | return; 29 | 30 | if (!function_exists('imagettfbbox')) { 31 | $this->response($this->error('验证码功能已开启,但 GD 库不存在或不完整')); 32 | exit(); 33 | } 34 | } 35 | 36 | /** 37 | * 是否需要验证码 38 | * 39 | * @return bool 40 | */ 41 | private function isNeedCaptcha() 42 | { 43 | if (!$this->isCaptchaOn()) // 总开关 44 | return false; 45 | 46 | $this->checkCaptchaSupported(); 47 | 48 | if ($this->checkCaptcha(trim($_POST['captcha'] ?? ''))) 49 | return false; // 若验证码是正确的,则不需要再次验证 50 | 51 | // 操作次数统计 52 | $timeout = $this->getCaptchaTimeout(); 53 | $limit = $this->getCaptchaLimit(); 54 | 55 | $actionLog = $this->getActionLog(); 56 | if (!empty($actionLog) && $actionLog->is_admin) { 57 | return false; // 已被标记为管理员的 IP 无需验证码 58 | } 59 | 60 | if ($limit == 0) { 61 | return true; // 一直需要验证码 62 | } 63 | 64 | $isInTime = (strtotime($actionLog->last_time)+$timeout >= time()); // 在超时内 65 | if ($isInTime && $actionLog->count >= $limit) { // 超过操作限制次数 66 | // 若超过限制评论次数 67 | return true; 68 | } else { 69 | return false; 70 | } 71 | } 72 | 73 | /** 74 | * 获取验证码的值 75 | */ 76 | private function getCaptchaStr() 77 | { 78 | $ip = $this->getUserIp(); 79 | $captcha = self::getCaptchaTable() 80 | ->where('ip', '=', $ip) 81 | ->find(); 82 | if (!empty($captcha) && !empty($captcha->str)) { 83 | return $captcha->str; 84 | } else { 85 | return null; 86 | } 87 | } 88 | 89 | /** 90 | * 检验验证码 91 | * 92 | * @param $str 93 | * @return bool 94 | */ 95 | private function checkCaptcha($str) 96 | { 97 | $rightStr = $this->getCaptchaStr(); 98 | return (!empty($rightStr) && strtolower($str) === strtolower($rightStr)); 99 | } 100 | 101 | /** 102 | * 刷新并获得验证码图片 103 | */ 104 | private function refreshGetCaptcha() 105 | { 106 | if (!$this->isCaptchaOn()) // 总开关 107 | return null; 108 | 109 | $this->checkCaptchaSupported(); 110 | 111 | $builder = new CaptchaBuilder; 112 | $builder->setBackgroundColor(255, 255, 255); 113 | $builder->build(); 114 | 115 | $ip = $this->getUserIp(); 116 | $captcha = self::getCaptchaTable() 117 | ->where('ip', '=', $ip) 118 | ->find(); 119 | 120 | if (empty($captcha)) { 121 | $captcha = self::getCaptchaTable(); 122 | } 123 | 124 | $captcha->set([ 125 | 'ip' => $ip, 126 | 'str' => $builder->getPhrase() 127 | ]); 128 | $captcha->save(); 129 | 130 | return $builder->inline(); 131 | } 132 | 133 | /** 134 | * 获取 IP 操作记录 135 | */ 136 | private function getActionLog() 137 | { 138 | $user = $this->getUserKey(); 139 | $actionLog = self::getActionLogsTable() 140 | ->where('user', '=', $user) 141 | ->find(); 142 | 143 | if (!empty($actionLog)) { 144 | return $actionLog; 145 | } else { 146 | return null; 147 | } 148 | } 149 | 150 | /** 151 | * 添加一次 IP 操作记录(用于限制操作频率) 152 | */ 153 | private function logAction() 154 | { 155 | $this->refreshGetCaptcha(); // 刷新验证码 156 | 157 | $ip = $this->getUserIp(); 158 | $user = $this->getUserKey(); 159 | $count = 0; 160 | $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; 161 | $isBan = false; 162 | $note = ''; 163 | 164 | $actionLog = self::getActionLogsTable() 165 | ->where('user', '=', $user) 166 | ->find(); 167 | 168 | if (!empty($actionLog)) { 169 | $count = $actionLog->count; 170 | $isBan = $actionLog->is_ban; 171 | $note = $actionLog->note; 172 | $isInTime = (strtotime($actionLog->last_time)+$this->getCaptchaTimeout() >= time()); // 在超时内 173 | if (!$isInTime) { 174 | $count = 0; // 在超时结束后,重置计数 175 | } 176 | } else { 177 | $actionLog = self::getActionLogsTable(); 178 | } 179 | 180 | $actionLog->set([ 181 | 'user' => $user, 182 | 'ip' => $ip, 183 | 'count' => $count+1, 184 | 'last_time' => date("Y-m-d H:i:s"), 185 | 'ua' => $ua, 186 | 'is_ban' => $isBan, 187 | 'note' => $note, 188 | ]); 189 | $actionLog->save(); 190 | } 191 | 192 | /** 193 | * 标记该 IP 为管理员,放开限制 194 | */ 195 | private function actionLogMarkAdmin() 196 | { 197 | $user = $this->getUserKey(); 198 | $ip = $this->getUserIp(); 199 | 200 | $actionLog = self::getActionLogsTable() 201 | ->where('user', '=', $user) 202 | ->find(); 203 | 204 | if (!empty($actionLog)) { 205 | $actionLog->set([ 206 | 'is_admin' => true 207 | ]); 208 | $actionLog->save(); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/Utils.php: -------------------------------------------------------------------------------- 1 | where('id', '=', $rid) 76 | ->find() 77 | ->asArray()[0] ?? []; 78 | if (!empty($commentFind)) { 79 | $comment = $commentFind; 80 | } 81 | } 82 | 83 | $replacement = []; 84 | foreach ($comment as $key => $item) { 85 | $replacement['{{comment.'.$key.'}}'] = $item; 86 | } 87 | foreach ($replyComment as $key => $item) { 88 | $replacement['{{reply.'.$key.'}}'] = $item; 89 | } 90 | 91 | $replacement['{{reply_link}}'] = self::urlAddQuery($replyComment['page_key'], 'artalk_comment='.$replyComment['id']); 92 | $replacement['{{reply.content_html}}'] = '@'.$replyComment['nick'].':
'.$replyComment['content']; 93 | $replacement['{{conf.site_name}}'] = _config()['site_name']; 94 | $mailContent = str_replace(array_keys($replacement), array_values($replacement), $mailTplRaw); 95 | 96 | // 邮件发送 97 | $adminAddr = _config()['email']['admin_addr'] ?? null; 98 | if (!empty($rid) && $comment['email'] !== $replyComment['email']) { 99 | $sendEmail($mailTitle, $mailContent, $comment['email']); 100 | } 101 | if (empty($rid) && !empty($adminAddr) && $replyComment['email'] !== $adminAddr) { 102 | $sendEmail($mailTitleToAdmin, $mailContent, $adminAddr); 103 | } 104 | 105 | return; 106 | } 107 | 108 | public static function sendEmailBySMTP($title, $content, $toAddr) 109 | { 110 | $mail = new PHPMailer(true); // Passing `true` enables exceptions 111 | try { 112 | // Server settings 113 | //$mail->SMTPDebug = 2; 114 | $mail->isSMTP(); 115 | $mail->Host = _config()['email']['smtp']['Host']; 116 | $mail->Port = _config()['email']['smtp']['Port']; 117 | $mail->SMTPAuth = _config()['email']['smtp']['SMTPAuth']; 118 | $mail->Username = _config()['email']['smtp']['Username']; 119 | $mail->Password = _config()['email']['smtp']['Password']; 120 | $mail->SMTPSecure = _config()['email']['smtp']['SMTPSecure']; 121 | $mail->CharSet = 'UTF-8'; 122 | 123 | // Recipients 124 | $mail->setFrom(_config()['email']['smtp']['FromAddr'], _config()['email']['smtp']['FromName']); 125 | $mail->addAddress($toAddr); // Add a recipient 126 | 127 | // Content 128 | $mail->isHTML(true); // Set email format to HTML 129 | $mail->Subject = $title; 130 | $mail->Body = $content; 131 | 132 | $mail->send(); 133 | return ['success' => true, 'msg' => 'Message has been sent']; 134 | } catch (Exception $e) { 135 | return ['success' => false, 'msg' => 'Message could not be sent. Mailer Error: '.$mail->ErrorInfo]; 136 | } 137 | } 138 | 139 | public static function sendEmailByALI_DM($title, $content, $toAddr) 140 | { 141 | return json_encode(Utils::requestAli('http://dm.aliyuncs.com', [ 142 | 'Action' => 'SingleSendMail', 143 | 'AccountName' => _config()['email']['ali_dm']['AccountName'], 144 | 'ReplyToAddress' => 'true', 145 | 'AddressType' => 1, 146 | 'ToAddress' => $toAddr, 147 | 'Subject' => $title, 148 | 'HtmlBody' => $content 149 | ]), true); 150 | } 151 | 152 | public static function curl($url) 153 | { 154 | $ch = \curl_init(); 155 | \curl_setopt($ch, CURLOPT_URL, $url); 156 | \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 157 | $result= \curl_exec($ch); 158 | return $result; 159 | } 160 | 161 | public static function percentEncode($value=null) 162 | { 163 | $en = urlencode($value); 164 | $en = str_replace('+', '%20', $en); 165 | $en = str_replace('*', '%2A', $en); 166 | $en = str_replace('%7E', '~', $en); 167 | return $en; 168 | } 169 | 170 | public static function aliSign($params, $accessSecret, $method="GET") 171 | { 172 | ksort($params); 173 | $stringToSign = strtoupper($method).'&'.self::percentEncode('/').'&'; 174 | 175 | $tmp = ''; 176 | foreach($params as $key=>$val){ 177 | $tmp .= '&'.self::percentEncode($key).'='.self::percentEncode($val); 178 | } 179 | $tmp = trim($tmp, '&'); 180 | $stringToSign = $stringToSign.self::percentEncode($tmp); 181 | 182 | $key = $accessSecret.'&'; 183 | $hmac = hash_hmac('sha1', $stringToSign, $key, true); 184 | 185 | return base64_encode($hmac); 186 | } 187 | 188 | public static function requestAli($baseUrl, $requestParams) 189 | { 190 | $publicParams = [ 191 | 'Format' => 'JSON', 192 | 'Version' => '2015-11-23', 193 | 'AccessKeyId' => _config()['email']['ali_dm']['AccessKeyId'], 194 | 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), 195 | 'SignatureMethod' => 'HMAC-SHA1', 196 | 'SignatureVersion' => '1.0', 197 | 'SignatureNonce' => substr(md5(rand(1, 99999999)), rand(1, 9), 14), 198 | ]; 199 | 200 | $params = array_merge($publicParams, $requestParams); 201 | $params['Signature'] = self::aliSign($params, _config()['email']['ali_dm']['AccessKeySecret']); 202 | $uri = http_build_query($params); 203 | $url = $baseUrl.'/?'.$uri; 204 | 205 | return self::curl($url); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "de1fd34a67e776a7137d662a80d51bd0", 8 | "packages": [ 9 | { 10 | "name": "greg0/lazer-database", 11 | "version": "2.0.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Lazer-Database/Lazer-Database.git", 15 | "reference": "ee82e7836fe46119ab6c6ad62c0ca4cc1a78b172" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Lazer-Database/Lazer-Database/zipball/ee82e7836fe46119ab6c6ad62c0ca4cc1a78b172", 20 | "reference": "ee82e7836fe46119ab6c6ad62c0ca4cc1a78b172", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.0" 25 | }, 26 | "require-dev": { 27 | "mikey179/vfsstream": "~1.6", 28 | "phpunit/phpunit": "6.2.0" 29 | }, 30 | "type": "library", 31 | "autoload": { 32 | "psr-4": { 33 | "Lazer\\": "src/", 34 | "Lazer\\Test\\": "tests/src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "Grzegorz Kuźnik", 44 | "email": "gerg0sz92@gmail.com", 45 | "role": "Owner" 46 | } 47 | ], 48 | "description": "PHP library to use JSON files like flat-file database", 49 | "homepage": "https://github.com/Greg0/Lazer-Database", 50 | "keywords": [ 51 | "Simple", 52 | "database", 53 | "db", 54 | "file", 55 | "flat", 56 | "json", 57 | "lazer" 58 | ], 59 | "support": { 60 | "issues": "https://github.com/Lazer-Database/Lazer-Database/issues", 61 | "source": "https://github.com/Lazer-Database/Lazer-Database/tree/master" 62 | }, 63 | "time": "2020-07-14T16:40:31+00:00" 64 | }, 65 | { 66 | "name": "gregwar/captcha", 67 | "version": "v1.1.8", 68 | "source": { 69 | "type": "git", 70 | "url": "https://github.com/Gregwar/Captcha.git", 71 | "reference": "6088ad3db59bc226423ad1476a9f0424b19b1866" 72 | }, 73 | "dist": { 74 | "type": "zip", 75 | "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/6088ad3db59bc226423ad1476a9f0424b19b1866", 76 | "reference": "6088ad3db59bc226423ad1476a9f0424b19b1866", 77 | "shasum": "" 78 | }, 79 | "require": { 80 | "ext-gd": "*", 81 | "ext-mbstring": "*", 82 | "php": ">=5.3.0", 83 | "symfony/finder": "*" 84 | }, 85 | "require-dev": { 86 | "phpunit/phpunit": "^6.4" 87 | }, 88 | "type": "captcha", 89 | "autoload": { 90 | "psr-4": { 91 | "Gregwar\\": "src/Gregwar" 92 | } 93 | }, 94 | "notification-url": "https://packagist.org/downloads/", 95 | "license": [ 96 | "MIT" 97 | ], 98 | "authors": [ 99 | { 100 | "name": "Grégoire Passault", 101 | "email": "g.passault@gmail.com", 102 | "homepage": "http://www.gregwar.com/" 103 | }, 104 | { 105 | "name": "Jeremy Livingston", 106 | "email": "jeremy.j.livingston@gmail.com" 107 | } 108 | ], 109 | "description": "Captcha generator", 110 | "homepage": "https://github.com/Gregwar/Captcha", 111 | "keywords": [ 112 | "bot", 113 | "captcha", 114 | "spam" 115 | ], 116 | "support": { 117 | "issues": "https://github.com/Gregwar/Captcha/issues", 118 | "source": "https://github.com/Gregwar/Captcha/tree/master" 119 | }, 120 | "time": "2020-01-22T14:54:02+00:00" 121 | }, 122 | { 123 | "name": "phpmailer/phpmailer", 124 | "version": "v6.2.0", 125 | "source": { 126 | "type": "git", 127 | "url": "https://github.com/PHPMailer/PHPMailer.git", 128 | "reference": "e38888a75c070304ca5514197d4847a59a5c853f" 129 | }, 130 | "dist": { 131 | "type": "zip", 132 | "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e38888a75c070304ca5514197d4847a59a5c853f", 133 | "reference": "e38888a75c070304ca5514197d4847a59a5c853f", 134 | "shasum": "" 135 | }, 136 | "require": { 137 | "ext-ctype": "*", 138 | "ext-filter": "*", 139 | "ext-hash": "*", 140 | "php": ">=5.5.0" 141 | }, 142 | "require-dev": { 143 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", 144 | "doctrine/annotations": "^1.2", 145 | "phpcompatibility/php-compatibility": "^9.3.5", 146 | "roave/security-advisories": "dev-latest", 147 | "squizlabs/php_codesniffer": "^3.5.6", 148 | "yoast/phpunit-polyfills": "^0.2.0" 149 | }, 150 | "suggest": { 151 | "ext-mbstring": "Needed to send email in multibyte encoding charset", 152 | "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", 153 | "league/oauth2-google": "Needed for Google XOAUTH2 authentication", 154 | "psr/log": "For optional PSR-3 debug logging", 155 | "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", 156 | "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" 157 | }, 158 | "type": "library", 159 | "autoload": { 160 | "psr-4": { 161 | "PHPMailer\\PHPMailer\\": "src/" 162 | } 163 | }, 164 | "notification-url": "https://packagist.org/downloads/", 165 | "license": [ 166 | "LGPL-2.1-only" 167 | ], 168 | "authors": [ 169 | { 170 | "name": "Marcus Bointon", 171 | "email": "phpmailer@synchromedia.co.uk" 172 | }, 173 | { 174 | "name": "Jim Jagielski", 175 | "email": "jimjag@gmail.com" 176 | }, 177 | { 178 | "name": "Andy Prevost", 179 | "email": "codeworxtech@users.sourceforge.net" 180 | }, 181 | { 182 | "name": "Brent R. Matzelle" 183 | } 184 | ], 185 | "description": "PHPMailer is a full-featured email creation and transfer class for PHP", 186 | "support": { 187 | "issues": "https://github.com/PHPMailer/PHPMailer/issues", 188 | "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.2.0" 189 | }, 190 | "funding": [ 191 | { 192 | "url": "https://github.com/Synchro", 193 | "type": "github" 194 | } 195 | ], 196 | "time": "2020-11-25T15:24:57+00:00" 197 | }, 198 | { 199 | "name": "symfony/finder", 200 | "version": "v5.2.1", 201 | "source": { 202 | "type": "git", 203 | "url": "https://github.com/symfony/finder.git", 204 | "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba" 205 | }, 206 | "dist": { 207 | "type": "zip", 208 | "url": "https://api.github.com/repos/symfony/finder/zipball/0b9231a5922fd7287ba5b411893c0ecd2733e5ba", 209 | "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba", 210 | "shasum": "" 211 | }, 212 | "require": { 213 | "php": ">=7.2.5" 214 | }, 215 | "type": "library", 216 | "autoload": { 217 | "psr-4": { 218 | "Symfony\\Component\\Finder\\": "" 219 | }, 220 | "exclude-from-classmap": [ 221 | "/Tests/" 222 | ] 223 | }, 224 | "notification-url": "https://packagist.org/downloads/", 225 | "license": [ 226 | "MIT" 227 | ], 228 | "authors": [ 229 | { 230 | "name": "Fabien Potencier", 231 | "email": "fabien@symfony.com" 232 | }, 233 | { 234 | "name": "Symfony Community", 235 | "homepage": "https://symfony.com/contributors" 236 | } 237 | ], 238 | "description": "Symfony Finder Component", 239 | "homepage": "https://symfony.com", 240 | "support": { 241 | "source": "https://github.com/symfony/finder/tree/v5.2.1" 242 | }, 243 | "funding": [ 244 | { 245 | "url": "https://symfony.com/sponsor", 246 | "type": "custom" 247 | }, 248 | { 249 | "url": "https://github.com/fabpot", 250 | "type": "github" 251 | }, 252 | { 253 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 254 | "type": "tidelift" 255 | } 256 | ], 257 | "time": "2020-12-08T17:02:38+00:00" 258 | } 259 | ], 260 | "packages-dev": [], 261 | "aliases": [], 262 | "minimum-stability": "stable", 263 | "stability-flags": [], 264 | "prefer-stable": false, 265 | "prefer-lowest": false, 266 | "platform": { 267 | "php": ">=7.1.3", 268 | "ext-curl": "*", 269 | "ext-json": "*" 270 | }, 271 | "platform-dev": [], 272 | "plugin-api-version": "2.0.0" 273 | } 274 | -------------------------------------------------------------------------------- /app/components/Action.php: -------------------------------------------------------------------------------- 1 | getUserNick(); 25 | $email = $this->getUserEmail(); 26 | $password = $this->getUserPassword(); 27 | 28 | if ($nick == '') return $this->error('昵称不能为空'); 29 | if ($email == '') return $this->error('邮箱不能为空'); 30 | 31 | if ($this->isNeedCaptcha()) { 32 | $imgData = $this->refreshGetCaptcha(); // 生成新的验证码 33 | return $this->error('需要验证码', ['need_captcha' => true, 'img_data' => $imgData]); 34 | } 35 | 36 | $this->logAction(); // 记录一次 IP 操作 37 | 38 | if ($this->isAdmin($nick, $email) && !$this->checkAdminPassword($nick, $email, $password)) { 39 | return $this->error('需要管理员身份', ['need_password' => true]); 40 | } 41 | 42 | if ($rid !== 0) { 43 | $replyComment = self::getCommentsTable()->where('id', '=', $rid)->find(); 44 | if ($replyComment->count() === 0) return $this->error('回复评论已被删除'); 45 | if ($replyComment->is_collapsed || ($this->isParentCommentCollapsed($replyComment))) { 46 | return $this->error('禁止回复被折叠的评论'); 47 | } 48 | } 49 | 50 | if ($pageKey == '') return $this->error('page_key 不能为空'); 51 | if ($content == '') return $this->error('内容不能为空'); 52 | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return $this->error('邮箱格式错误'); 53 | if ($link !== '' && !Utils::urlValidator($link)) return $this->error('网址格式错误'); 54 | 55 | // 获取页面数据 56 | $page = self::getPagesTable()->where('page_key', '=', $pageKey)->findAll()->asArray(); 57 | if (isset($page[0])) { 58 | $page = $page[0]; 59 | // 评论已关闭 60 | if (!$this->isAdmin($nick, $email) && !empty($page['is_close_comment']) && $page['is_close_comment'] === true) { 61 | return $this->error('评论已关闭'); 62 | } 63 | } 64 | 65 | $comment = self::getCommentsTable(); 66 | $comment->content = $content; 67 | $comment->nick = $nick; 68 | $comment->email = $email; 69 | $comment->link = $link; 70 | $comment->page_key = $pageKey; 71 | $comment->rid = $rid; 72 | $comment->ua = $ua; 73 | $comment->date = date("Y-m-d H:i:s"); 74 | $comment->ip = $this->getUserIP(); 75 | $comment->is_collapsed = false; 76 | $comment->is_pending = false; 77 | 78 | if (_config()['moderation']['pending_default']) { 79 | $comment->is_pending = true; // 默认待审状态 80 | } 81 | 82 | $comment->save(); 83 | 84 | $lastId = $comment->lastId(); 85 | $comment = self::getCommentsTable()->where('id', '=', $lastId); 86 | $commentArr = @$comment->findAll()->asArray()[0]; 87 | $comment1 = self::getCommentsTable()->where('id', '=', $lastId)->find(); 88 | 89 | try { 90 | Utils::sendEmailToCommenter($commentArr); // 发送邮件通知 91 | } catch (\Exception $e) { 92 | return $this->error('通知邮件发送失败,请联系网站管理员', ['error-msg' => $e->getMessage(), 'error-detail' => $e->getTraceAsString()]); 93 | } 94 | 95 | return $this->success('评论成功', ['comment' => $this->beautifyCommentData($comment1)]); 96 | } 97 | 98 | /** 99 | * Action: CommentGet 100 | * Desc : 评论获取 101 | */ 102 | public function actionCommentGet() 103 | { 104 | $pageKey = trim($_POST['page_key'] ?? ''); 105 | if ($pageKey == '') { 106 | return $this->error('page_key 不能为空'); 107 | } 108 | 109 | $offset = intval(trim($_POST['offset'] ?? 0)); 110 | $limit = intval(trim($_POST['limit'] ?? 0)); 111 | if ($offset < 0) $offset = 0; 112 | if ($limit <= 0) $limit = 15; 113 | 114 | $condList = [ 115 | 'page_key' => $pageKey, 116 | 'is_pending' => 0, 117 | ]; 118 | 119 | $commentTable = self::getCommentsTable(); 120 | $comments = $this->getComments($condList, $offset, $limit); 121 | 122 | // 管理员信息 123 | $adminUsers = $this->getAdminUsers(); 124 | $adminNicks = []; 125 | $adminEncryptedEmails = []; 126 | foreach ($adminUsers as $admin) { 127 | $adminNicks[] = $admin['nick']; 128 | $adminEncryptedEmails[] = md5($admin['email']); 129 | } 130 | 131 | // 页面数据 132 | $page = self::getPagesTable()->where('page_key', '=', $pageKey)->findAll()->asArray(); 133 | $pageData = []; 134 | if (!isset($page[0])) { 135 | $page = self::getPagesTable(); 136 | $page->page_key = $pageKey; 137 | $page->is_close_comment = false; 138 | $page->save(); 139 | $pageData = $page->where('page_key', '=', $pageKey)->findAll()->asArray()[0]; 140 | } else { 141 | $pageData = $page[0]; 142 | } 143 | 144 | return $this->success('获取成功', [ 145 | 'comments' => $comments, 146 | 'offset' => $offset, 147 | 'limit' => $limit, 148 | 'total_parents' => $this->countComments($condList, true), 149 | 'total' => $this->countComments($condList), 150 | 'admin_nicks' => $adminNicks, 151 | 'admin_encrypted_emails' => $adminEncryptedEmails, 152 | 'page' => $pageData 153 | ]); 154 | } 155 | 156 | /** 157 | * Action: CommentGetV2 158 | * Desc : 回复评论获取 159 | */ 160 | public function actionCommentGetV2() 161 | { 162 | $type = trim($_POST['type'] ?? ''); 163 | if (!in_array($type, ['all', 'mentions', 'mine', 'pending'])) { 164 | return $this->error('type 未知'); 165 | } 166 | 167 | $nick = $this->getUserNick(); 168 | $email = $this->getUserEmail(); 169 | if ($nick == '') return $this->error('昵称 不能为空'); 170 | if ($email == '') return $this->error('邮箱 不能为空'); 171 | 172 | // 分页 173 | $offset = intval(trim($_POST['offset'] ?? 0)); 174 | $limit = intval(trim($_POST['limit'] ?? 0)); 175 | if ($offset < 0) $offset = 0; 176 | if ($limit <= 0) $limit = 15; 177 | 178 | if ($this->isAdmin($nick, $email)) { 179 | // 管理员 180 | $this->NeedAdmin(); 181 | } 182 | 183 | $condList = [ 184 | 'nick' => $nick, 185 | 'email' => $email, 186 | ]; // default 187 | $queryChildren = true; // 是否查找子评论 188 | 189 | // Type: "all" 全部 190 | if ($type == 'all') { 191 | if ($this->isAdmin($nick, $email)) { // 管理员 192 | $condList = []; 193 | } else { 194 | $condList = [ 195 | 'nick' => $nick, 196 | 'email' => $email, 197 | ]; 198 | } 199 | } 200 | 201 | // Type: "mentions" 提及 202 | if ($type == 'mentions') { 203 | $myComments = self::getCommentsTable() 204 | ->where('nick', '=', $nick) 205 | ->andWhere('email', '=', $email) 206 | ->orderBy('date', 'DESC') 207 | ->findAll() 208 | ->asArray(); 209 | 210 | $idList = []; 211 | foreach ($myComments as $item) { 212 | $idList[] = $item['id']; 213 | } 214 | 215 | $condList = [ 216 | 'rid' => $idList, 217 | 'nick:not' => $nick, 218 | 'email:not' => $email, 219 | ]; 220 | $queryChildren = false; 221 | } 222 | 223 | // Type: "mine" 我的 224 | if ($type == 'mine') { 225 | $condList = [ 226 | 'nick' => $nick, 227 | 'email' => $email 228 | ]; 229 | $queryChildren = false; 230 | } 231 | 232 | // Type: "pending" 待审 233 | if ($type == 'pending') { 234 | $queryChildren = false; 235 | if ($this->isAdmin($nick, $email)) { // 管理员 236 | $condList = []; 237 | } else { 238 | $condList = [ 239 | 'nick' => $nick, 240 | 'email' => $email 241 | ]; 242 | } 243 | 244 | $condList = array_merge($condList, [ 245 | 'is_pending' => 1, 246 | ]); 247 | } 248 | 249 | $comments = $this->getComments($condList, $offset, $limit, $queryChildren); 250 | 251 | return $this->success('获取成功', [ 252 | 'comments' => $comments, 253 | 'total' => $this->countComments($condList), 254 | 'total_parents' => $this->countComments($condList, true), 255 | 'offset' => $offset, 256 | 'limit' => $limit, 257 | ]); 258 | } 259 | 260 | /** 261 | * Action: CaptchaCheck 262 | * Desc : 验证码检验 263 | */ 264 | public function actionCaptchaCheck() 265 | { 266 | if (!empty(trim($_POST['refresh'] ?? ''))) { 267 | $imgData = $this->refreshGetCaptcha(); 268 | return $this->success('验证码刷新成功', ['img_data' => $imgData]); 269 | } 270 | 271 | $captcha = trim($_POST['captcha'] ?? ''); 272 | if ($captcha == '') return $this->error('验证码 不能为空'); 273 | 274 | if ($this->checkCaptcha($captcha)) { 275 | return $this->success('验证码正确'); 276 | } else { 277 | $imgData = $this->refreshGetCaptcha(); 278 | return $this->error('验证码错误', ['img_data' => $imgData]); 279 | } 280 | } 281 | 282 | /** =========================================================== */ 283 | /** ------------------------- Helpers ------------------------- */ 284 | /** =========================================================== */ 285 | private function beautifyCommentData($commentObj) 286 | { 287 | $comment = []; 288 | $showField = ['id', 'content', 'nick', 'link', 'page_key', 'rid', 'ua', 'date', 'is_collapsed']; 289 | foreach ($showField as $field) { 290 | if (isset($commentObj->{$field})) 291 | $comment[$field] = $commentObj->{$field}; 292 | } 293 | 294 | $comment['email_encrypted'] = md5(strtolower(trim($commentObj->email))); 295 | $findAdminUser = $this->findAdminUser($commentObj->nick ?? null, $commentObj->email ?? null); 296 | if (!empty($findAdminUser)) { 297 | $comment['badge'] = []; 298 | $comment['badge']['name'] = $findAdminUser['badge_name'] ?? '管理员'; 299 | $comment['badge']['color'] = $findAdminUser['badge_color'] ?? '#ffa928'; 300 | $comment['is_admin'] = true; 301 | } 302 | 303 | return $comment; 304 | } 305 | 306 | /** 获取评论 */ 307 | private function getComments($condList, $offset, $limit, $queryChildren = true) { 308 | $comments = []; 309 | $QueryAllChildren = function ($parentId) use (&$pageKey, &$comments, &$QueryAllChildren) { 310 | $rawComments = self::getCommentsTable() 311 | ->where('rid', '=', $parentId) 312 | ->orderBy('date', 'ASC') 313 | ->findAll(); 314 | 315 | foreach ($rawComments as $item) { 316 | $comments[] = $this->beautifyCommentData($item); 317 | $QueryAllChildren($item->id); 318 | } 319 | }; 320 | 321 | if ($queryChildren) { 322 | $commentsRaw = self::getCommentsTable()->where('rid', '=', 0); 323 | } else { 324 | $commentsRaw = self::getCommentsTable(); 325 | } 326 | 327 | $this->applyCondList($commentsRaw, $condList); 328 | 329 | $commentsRaw = $commentsRaw 330 | ->orderBy('date', 'DESC') 331 | ->limit($limit, $offset) 332 | ->findAll(); 333 | 334 | foreach ($commentsRaw as $item) { 335 | $comments[] = $this->beautifyCommentData($item); 336 | 337 | // Child Comments 338 | if ($queryChildren) 339 | $QueryAllChildren($item->id); 340 | } 341 | 342 | return $comments; 343 | } 344 | 345 | /** 获取评论数 */ 346 | private function countComments($condList, $onlyParent = false) { 347 | $comments = self::getCommentsTable(); 348 | $this->applyCondList($comments, $condList); 349 | if ($onlyParent) 350 | $comments = $comments->where('rid', '=', 0); 351 | return $comments->findAll()->count(); 352 | } 353 | 354 | private function applyCondList(&$query, $condList) { 355 | if (empty($condList)) return; 356 | foreach ($condList as $key => $val) { 357 | $w = '='; 358 | $keyParse = explode(':', $key); 359 | $realKey = reset($keyParse); 360 | 361 | if (end($keyParse) == 'not') 362 | $w = '!='; 363 | if (is_array($val)) 364 | $w = 'IN'; 365 | 366 | $query = $query 367 | ->where($realKey, $w, $val); 368 | } 369 | } 370 | 371 | /** 父评论是否有被折叠 */ 372 | private function isParentCommentCollapsed($srcComment) 373 | { 374 | if ($srcComment->is_collapsed ?? false) return true; 375 | if ($srcComment->rid === 0) return false; 376 | 377 | $pComment = self::getCommentsTable()->where('id', '=', $srcComment->rid)->find(); 378 | if ($pComment->count() === 0) return false; 379 | if ($pComment->is_collapsed) return true; 380 | else return $this->isParentCommentCollapsed($pComment); // 继续寻找 381 | } 382 | 383 | private function getRootComment($srcComment) 384 | { 385 | if ($srcComment->rid === 0) return $srcComment; 386 | 387 | $pComment = self::getCommentsTable()->where('id', '=', $srcComment->rid)->find(); 388 | if ($pComment->count() === 0) return null; 389 | 390 | if ($pComment->rid === 0) return $pComment; // root comment 391 | else return $this->getRootComment($pComment); // 继续寻找 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------