Skip to content

Commit

Permalink
Re-introduce check for comment similarity to annoy spammers. Do not a…
Browse files Browse the repository at this point in the history
…llow comments with email addresses.
  • Loading branch information
thisismeonmounteverest committed May 12, 2024
1 parent 0d7ac8c commit 51edc14
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 25 deletions.
49 changes: 28 additions & 21 deletions src/Controller/CommentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ public function addComment(
$memberPreference = $loggedInMember->getMemberPreference($preference);
$showCommentGuideline = ('0' === $memberPreference->getValue());

/** @var Member $loggedInMember */
$form = $this->createForm(CommentType::class, null, [
'to_member' => $member,
'show_comment_guideline' => $showCommentGuideline,
Expand All @@ -162,30 +161,38 @@ public function addComment(
if ($form->isSubmitted() && $form->isValid()) {
/** @var Comment $comment */
$comment = $form->getData();
$comment->setToMember($member);
$comment->setFromMember($loggedInMember);
if (CommentQualityType::NEGATIVE === $comment->getQuality()) {
$comment->setAdminAction(CommentAdminActionType::ADMIN_CHECK);
$comment->setEditingAllowed(false);
}
$entityManager->persist($comment);
if (
$commentModel->checkCommentSpam($loggedInMember, $comment)
|| $commentModel->checkForEmailAddress($comment)
) {
$form->addError(new FormError($this->translator->trans('commentsomethingwentwrong')));
} else {
$comment->setToMember($member);
$comment->setFromMember($loggedInMember);

// Mark comment guidelines as read and hide the checkbox for the future
$memberPreference->setValue('1');
$entityManager->persist($memberPreference);
$entityManager->flush();
if (CommentQualityType::NEGATIVE === $comment->getQuality()) {
$comment->setAdminAction(CommentAdminActionType::ADMIN_CHECK);
$comment->setEditingAllowed(false);
}
$entityManager->persist($comment);

$mailer->sendNewCommentNotification($comment);
// Mark comment guidelines as read and hide the checkbox for the future
$memberPreference->setValue('1');
$entityManager->persist($memberPreference);
$entityManager->flush();

$this->addTranslatedFlash(
'notice',
'flash.comment.added',
[
'username' => $member->getUsername(),
]
);
$mailer->sendNewCommentNotification($comment);

return $this->redirectToRoute('profile_comments', ['username' => $member->getUsername()]);
$this->addTranslatedFlash(
'notice',
'flash.comment.added',
[
'username' => $member->getUsername(),
]
);

return $this->redirectToRoute('profile_comments', ['username' => $member->getUsername()]);
}
}

return $this->render('/profile/comment.add.html.twig', [
Expand Down
107 changes: 103 additions & 4 deletions src/Model/CommentModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,127 @@ public function checkIfNewExperience(Comment $original, Comment $updated): bool
$newExperience = false;
try {
$maxlen = max(strlen($updatedText), strlen($originalText));
$calculator = new LevenshteinDistance(false, 0, 1000**2);
$calculator = new LevenshteinDistance(false, 0, 1000 ** 2);
$iteration = 0;
$maxIteration = $maxlen / 1000;
while ($iteration < $maxIteration && !$newExperience) {

$currentUpdatedText = substr($updatedText, $iteration * 1000, 1000);
$currentOriginalText = substr($originalText, $iteration * 1000, 1000);
$levenshteinDistance = ($calculator->calculate(
$currentUpdatedText,
$currentOriginalText)
$currentOriginalText
)
)['distance'];

if ($levenshteinDistance >= max(strlen($currentUpdatedText), strlen($currentOriginalText)) / 7) {
$newExperience = true;
}
$iteration++;
}
} catch(Throwable $e) {
} catch (Throwable $e) {
// ignore exception and just return false (likely consumed too much memory)
return $newExperience;
}

return $newExperience;
}

public function checkCommentSpam(Member $loggedInMember, Comment $comment): bool
{
$spamCheckParams = [
['duration' => '00:02:00', 'count' => 1],
['duration' => '00:20:00', 'count' => 5],
['duration' => '06:00:00', 'count' => 25],
];

$check1 = $this->checkCommentsDuration($loggedInMember, $comment, $spamCheckParams[0]);
$check2 = $this->checkCommentsDuration($loggedInMember, $comment, $spamCheckParams[1]);
$check3 = $this->checkCommentsDuration($loggedInMember, $comment, $spamCheckParams[2]);

return $check1 || $check2 || $check3;
}

private function checkCommentsDuration(Member $member, Comment $comment, array $params): bool
{
$duration = $params['duration'];
$count = $params['count'];

$result = false;
$commentCount = $this->entityManager
->getConnection()
->executeQuery(
"
SELECT
COUNT(*) as cnt
FROM
comments c
WHERE
c.IdFromMember = :memberId
AND TIMEDIFF(NOW(), created) < :duration
",
[ ':memberId' => $member->getId(), ':duration' => $duration]
)
->fetchOne()
;

if ($commentCount >= $count) {
// Okay limit was hit, check for comment quality
// Get all comments written during the given duration
$comments = $this->entityManager
->getConnection()
->executeQuery(
"
SELECT
c.TextFree
FROM
comments c
WHERE
c.IdFromMember = :memberId
AND TIMEDIFF(NOW(), created) < :duration
",
[ ':memberId' => $member->getId(), ':duration' => $duration]
)
->fetchAllAssociative()
;
$result = $this->checkCommentSimilarity($comments, $comment);
}

return $result;
}

private function checkCommentSimilarity(array $comments, Comment $comment): bool
{
$similar = 0;
$comments[count($comments)] = ['TextFree' => $comment->getTextfree()];
$count = count($comments);
for ($i = 0; $i < $count - 1; $i++) {
for ($j = $i + 1; $j < $count; $j++) {
similar_text(
$comments[$i]['TextFree'],
$comments[$j]['TextFree'],
$percent
);
if ($percent > 95) {
$similar++;
}
}
}
return $similar != $count * ($count - 1);
}

public function checkForEmailAddress(Comment $comment): bool
{
$commentText = $comment->getTextfree();
$atPos = strpos($commentText, '@');
$whiteSpaceBefore = strrpos(substr($commentText, 0, $atPos), ' ');
$whiteSpaceAfter = strpos($commentText, ' ', $atPos);
if (false === $whiteSpaceAfter) {
$whiteSpaceAfter = strlen($commentText);
}
$potentialEmailAddress =
substr($commentText, $whiteSpaceBefore + 1, $whiteSpaceAfter - $whiteSpaceBefore - 1);
$emailAddressFound = filter_var($potentialEmailAddress, FILTER_VALIDATE_EMAIL) !== false;

return $emailAddressFound;
}
}
1 change: 1 addition & 0 deletions templates/profile/comment.form.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<h2>{{ 'commentheading'|trans|format(member.username) }}</h2>
<div class="alert alert-info">{{ 'followcommentguidelines'|trans|raw }}</div>
{{ form_start(form, { 'attr': { 'novalidate': 'novalidate' } }) }}
{{ form_errors(form) }}
{{ form_row(form.quality) }}
{{ form_errors(form.quality) }}
{{ form_row(form.relations) }}
Expand Down
49 changes: 49 additions & 0 deletions tests/Model/CommentModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,55 @@ public function testLongTextWithLowNumberUpdatesIsNotANewExperience()

$this->assertFalse($this->commentModel->checkIfNewExperience($original, $updated));
}

public function testCommentWithEmailAddressInTextIsRecognized()
{
$comment = new Comment();
$comment->setTextfree('This is an@email.address in the middle of a text.');

$emailAddressFound = $this->commentModel->checkForEmailAddress($comment);

$this->assertTrue($emailAddressFound);
}
public function testCommentWithEmailAddressAtStartOfTextIsRecognized()
{
$comment = new Comment();
$comment->setTextfree('an@email.address at the start of a text.');

$emailAddressFound = $this->commentModel->checkForEmailAddress($comment);

$this->assertTrue($emailAddressFound);
}
public function testCommentWithEmailAddressAtTheEndOfTextIsRecognized()
{
$comment = new Comment();
$comment->setTextfree('At the end of this text, there is an@email.address');

$emailAddressFound = $this->commentModel->checkForEmailAddress($comment);

$this->assertTrue($emailAddressFound);
}

public function testCommentWithTwoEmailAddressesInTextIsRecognized()
{
$comment = new Comment();
$comment->setTextfree('This is an@email.address in the middle of a text. And another one at the end. another@email.net');

$emailAddressFound = $this->commentModel->checkForEmailAddress($comment);

$this->assertTrue($emailAddressFound);
}

public function testCommentWithoutEmailAddressInTextIsRecognized()
{
$comment = new Comment();
$comment->setTextfree('This is an @instagram username.');

$emailAddressFound = $this->commentModel->checkForEmailAddress($comment);

$this->assertFalse($emailAddressFound);
}

private function buildRelations(array $relations): string
{
return implode(',', $relations);
Expand Down

0 comments on commit 51edc14

Please sign in to comment.