├── 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 |
--------------------------------------------------------------------------------