Skip to content

Commit 414b04e

Browse files
committed
feat: email verification
improvement: table handling in panel view
1 parent ea4a407 commit 414b04e

33 files changed

+954
-173
lines changed

index.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
'snippets' => require_once(__DIR__ . '/plugin/snippets.php'),
1414
'templates' => [
1515
'emails/newcomments' => __DIR__ . '/templates/emails/newComments.php',
16-
'komment-response' => __DIR__ . '/templates/pages/response.php'
16+
'emails/mailverification' => __DIR__ . '/templates/emails/mailverification.php',
17+
'komment-response' => __DIR__ . '/templates/pages/response.php',
18+
'comment-verified' => __DIR__ . '/templates/pages/comment-verified.php',
1719
],
1820
'blueprints' => require_once(__DIR__ . '/plugin/blueprints.php'),
1921
'pageMethods' => require_once(__DIR__ . '/plugin/page-methods.php'),

lib/CommentVerification.php

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
namespace mauricerenck\Komments;
4+
5+
use Kirby\Toolkit\Collection;
6+
7+
class CommentVerification
8+
{
9+
10+
public function __construct(
11+
private ?bool $verificationEnabled = null,
12+
private ?bool $verificationTtl = null,
13+
private ?bool $verificationSecret = null,
14+
private ?bool $verificationAutoPublish = null,
15+
private ?string $verificationDeletionMode = null,
16+
) {
17+
$this->verificationEnabled = $verificationEnabled ?? option('mauricerenck.komments.spam.verification.enabled', false);
18+
$this->verificationTtl = $verificationTtl ?? option('mauricerenck.komments.spam.verification.ttl', '48');
19+
$this->verificationSecret = $verificationSecret ?? option('mauricerenck.komments.spam.verification.secret', false);
20+
$this->verificationAutoPublish = $verificationAutoPublish ?? option('mauricerenck.komments.spam.verification.autoPublish', false);
21+
$this->verificationDeletionMode = $verificationDeletionMode ?? option('mauricerenck.komments.spam.verification.deletionMode', 'spam');
22+
}
23+
24+
public function isVerificationEnabled(): bool
25+
{
26+
if (!$this->verificationEnabled || !$this->verificationSecret) {
27+
return false;
28+
}
29+
30+
return true;
31+
}
32+
33+
public function generateToken(string $email, string $commentId): string | false
34+
{
35+
$expiresAt = time() + $this->verificationTtl * 60 * 60;
36+
$data = $email . '|' . $commentId . '|' . $expiresAt;
37+
$hash = hash_hmac('sha256', $data, $this->verificationSecret);
38+
39+
$storage = StorageFactory::create();
40+
41+
$result = $storage->saveVerificationToken(
42+
hash: $hash,
43+
commentId: $commentId,
44+
expiresAt: $expiresAt,
45+
);
46+
47+
if (!$result) {
48+
return false;
49+
}
50+
51+
// Return token with expiration timestamp encoded
52+
return $hash . '.' . base64_encode($expiresAt);
53+
}
54+
55+
public function getVerificationUrl(string $email, string $commentId): string | false
56+
{
57+
$token = $this->generateToken(email: $email, commentId: $commentId);
58+
59+
if (!$token) {
60+
return false;
61+
}
62+
63+
return kirby()->url() . '/komments/verify-comment/' . $token;
64+
}
65+
66+
public function verifyToken(string $token): string | bool
67+
{
68+
// Parse token and expiration
69+
$parts = explode('.', $token);
70+
if (count($parts) !== 2) {
71+
return false;
72+
}
73+
74+
[$hash, $encodedExpiration] = $parts;
75+
$expiresAt = base64_decode($encodedExpiration);
76+
77+
// Check expiration
78+
if (time() > $expiresAt) {
79+
return false;
80+
}
81+
82+
$storage = StorageFactory::create();
83+
$tokenData = $storage->getVerificationToken(hash: $hash);
84+
85+
if (is_null($tokenData) || $tokenData->count() === 0) {
86+
return false;
87+
}
88+
89+
if ($tokenData->first()->expires_at() < time()) {
90+
return false;
91+
}
92+
93+
$updateValues = [];
94+
$updateValues['status'] = ($this->verificationAutoPublish === true) ? 'PUBLISHED' : 'VERIFIED';
95+
96+
if ($this->verificationAutoPublish === true) {
97+
$updateValues['published'] = true;
98+
}
99+
100+
$storage->updateComment($tokenData->first()->comment_id(), $updateValues);
101+
$storage->deleteVerificationToken($hash);
102+
$storage->cleanupVerificationTokens($this->verificationDeletionMode);
103+
104+
return $tokenData->first()->comment_id();
105+
}
106+
107+
public function cleanupTokens(): void
108+
{
109+
$storage = StorageFactory::create();
110+
$storage->cleanupVerificationTokens($this->verificationDeletionMode);
111+
}
112+
113+
public function getVerficationTokens(): Collection
114+
{
115+
$storage = StorageFactory::create();
116+
return $storage->getVerificationTokens();
117+
}
118+
}

lib/DatabaseAbstraction.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function insert(string $table, array $fields, array $values): bool
3939
try {
4040
$values = $this->convertValuesToSaveDbString($values);
4141
$query =
42-
'INSERT INTO ' . $table . '(' . implode(',', $fields) . ') VALUES("' . implode('","', $values) . '")';
42+
'INSERT INTO ' . $table . '(' . implode(',', $fields) . ') VALUES(\'' . implode('\',\'', $values) . '\')';
4343

4444
$this->db->query($query);
4545

@@ -61,13 +61,14 @@ public function update(string $table, array $fields, array $values, string $filt
6161
',',
6262
array_map(
6363
function ($field, $value) {
64-
return $field . '="' . $value . '"';
64+
return $field . '=\'' . $value . '\'';
6565
},
6666
$fields,
6767
$values
6868
)
6969
);
7070
$query .= ' ' . $filters;
71+
7172
$this->db->query($query);
7273

7374
return true;

lib/KommentModeration.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@ public function deleteComment(string $id): mixed
2525
return $result;
2626
}
2727

28-
public function deleteCommentsInBatch(string $type): mixed
28+
public function deleteCommentsInBatch(string $type, array $ids = []): mixed
2929
{
30-
$result = $this->storage->deleteComments($type);
30+
$result = $this->storage->deleteComments($type, $ids);
3131
return $result;
3232
}
3333

34+
3435
public function publishComment(string $id): mixed
3536
{
3637
$comment = $this->storage->getSingleComment($id);
3738
$newStatus = $comment->published()->isTrue() ? false : true;
38-
$result = $this->storage->updateComment($id, ['published' => $newStatus]);
39+
$result = $this->storage->updateComment($id, ['published' => $newStatus, 'status' => $newStatus ? 'PUBLISHED' : 'PENDING']);
3940

4041
kirby()->trigger('komments.comment.published', ['comment' => $comment]);
4142

@@ -69,6 +70,24 @@ public function flagComment(string $id, string $flag): mixed
6970
return false;
7071
}
7172

73+
74+
public function flagCommentsInBatch(string $flag, array $ids = []): mixed
75+
{
76+
switch ($flag) {
77+
case 'spamlevel':
78+
$result = $this->storage->updateCommentsById($ids, ['spamlevel' => 100]);
79+
return $result;
80+
case 'verified':
81+
$result = $this->storage->updateCommentsById($ids, ['verified' => true]);
82+
return $result;
83+
case 'published':
84+
$result = $this->storage->updateCommentsById($ids, ['published' => true, 'status' => 'PUBLISHED']);
85+
return $result;
86+
}
87+
88+
return false;
89+
}
90+
7291
public function replyToComment(string $id, array $formData)
7392
{
7493

@@ -90,6 +109,7 @@ public function replyToComment(string $id, array $formData)
90109
authorAvatar: $avatar,
91110
authorEmail: $author->email(),
92111
authorUrl: site()->url(),
112+
status: $publishResult ? 'PUBLISHED' : 'PENDING',
93113
published: $publishResult,
94114
verified: true,
95115
spamlevel: 0,

lib/KommentReceiver.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,18 @@
99
class KommentReceiver
1010
{
1111

12-
public function __construct(private ?array $autoPublish = null, private ?bool $autoPublishVerified = null, private ?bool $akismet = null, private ?string $akismetApiKey = null, private ?bool $debug = null, private ?array $spamKeywords = null, private ?array $spamPhrases = null)
13-
{
12+
public function __construct(
13+
private ?array $autoPublish = null,
14+
private ?bool $autoPublishVerified = null,
15+
private ?bool $akismet = null,
16+
private ?string $akismetApiKey = null,
17+
private ?bool $debug = null,
18+
private ?array $spamKeywords = null,
19+
private ?array $spamPhrases = null,
20+
private ?bool $verificationEnabled = null,
21+
private ?bool $verificationTtl = null,
22+
private ?bool $verificationSecret = null
23+
) {
1424
$this->autoPublish = $autoPublish ?? option('mauricerenck.komments.moderation.autoPublish', []);
1525
$this->autoPublishVerified = $autoPublishVerified ?? option('mauricerenck.komments.moderation.publish-verified', false);
1626
$this->akismet = $akismet ?? option('mauricerenck.komments.spam.akismet', false);
@@ -195,4 +205,32 @@ public function akismetCheck(array $fields, $page): int
195205
return 0;
196206
}
197207
}
208+
209+
public function sendVerificationMail(string $email, string $username, string $commentId): void
210+
{
211+
212+
$verification = new CommentVerification();
213+
if (!$verification->isVerificationEnabled()) {
214+
return;
215+
}
216+
217+
$verificationUrl = $verification->getVerificationUrl(email: $email, commentId: $commentId);
218+
219+
if (!$verificationUrl) {
220+
return;
221+
}
222+
223+
kirby()->email([
224+
'from' => option('mauricerenck.komments.notifications.email.sender'),
225+
'to' => $email,
226+
'subject' => 'Verify your Comment',
227+
'template' => 'mailverification',
228+
'data' => [
229+
'username' => $username,
230+
'commentId' => $commentId,
231+
'expireHours' => option('mauricerenck.komments.spam.verification.ttl'),
232+
'verificationUrl' => $verificationUrl,
233+
],
234+
]);
235+
}
198236
}

lib/Storage.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function createComment(
4040
string $authorAvatar,
4141
?string $authorEmail,
4242
string $authorUrl,
43+
string $status,
4344
bool $published,
4445
bool $verified,
4546
int $spamlevel,
@@ -59,6 +60,7 @@ public function createComment(
5960
'authorAvatar' => $authorAvatar,
6061
'authorEmail' => $authorEmail,
6162
'authorUrl' => $authorUrl,
63+
'status' => $status,
6264
'published' => $published,
6365
'verified' => $verified,
6466
'spamlevel' => $spamlevel,
@@ -70,4 +72,6 @@ public function createComment(
7072
'permalink' => '/@/comment/' . $id,
7173
]);
7274
}
75+
76+
public function saveVerificationToken(string $hash, string $commentId, string $expiresAt): bool {}
7377
}

lib/StorageMarkdown.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public function convertToStructure(Obj|Collection|Structure $databaseResults): S
172172
authorAvatar: $avatar,
173173
authorEmail: $databaseResult->author_email,
174174
authorUrl: $databaseResult->author_url,
175+
status: $databaseResult->status,
175176
published: $databaseResult->published,
176177
verified: $databaseResult->verified,
177178
spamlevel: $databaseResult->spamlevel,
@@ -197,4 +198,6 @@ public function saveToFile(string $fieldData, $page): void
197198
'kommentsInboxData' => $fieldData
198199
]);
199200
}
201+
202+
public function saveVerificationToken(string $hash, string $commentId, string $expiresAt): bool {}
200203
}

lib/StoragePhpunit.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public function convertToStructure(Obj|Collection $databaseResults): Structure
8080
authorAvatar: $databaseResult->author_avatar,
8181
authorEmail: $databaseResult->author_email,
8282
authorUrl: $databaseResult->author_url,
83+
status: $databaseResult->status,
8384
published: $databaseResult->published,
8485
verified: $databaseResult->verified,
8586
spamlevel: $databaseResult->spamlevel,
@@ -115,6 +116,7 @@ private function getCommentMock(array $comment = []): Obj
115116
'authorEmail' => '[email protected]',
116117
'authorUrl' => 'https://example.com',
117118
'published' => true,
119+
'status' => 'PUBLISHED',
118120
'verified' => false,
119121
'spamlevel' => 0,
120122
'language' => null,
@@ -137,6 +139,7 @@ private function getCommentMock(array $comment = []): Obj
137139
'author_email' => $comment['authorEmail'],
138140
'author_url' => $comment['authorUrl'],
139141
'published' => $comment['published'],
142+
'status' => $comment['status'],
140143
'verified' => $comment['verified'],
141144
'spamlevel' => $comment['spamlevel'],
142145
'language' => $comment['language'],
@@ -159,4 +162,6 @@ private function getCommentCollection(): Collection
159162

160163
return new Collection($comments);
161164
}
165+
166+
public function saveVerificationToken(string $hash, string $commentId, string $expiresAt): bool {}
162167
}

0 commit comments

Comments
 (0)