From 503b9458e0900cedaea899190689896ecc87868a Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 7 Aug 2025 23:13:35 +0200 Subject: [PATCH] Template/Update (Part 15) * convert comment/reply ajax (add, edit, delete, vote, report and management) and redirects (comment/reply > db-page) * update roles when updating own comment/reply --- endpoints/admin/comment.php | 51 ++ endpoints/admin/out-of-date.php | 34 ++ endpoints/comment/add-reply.php | 51 ++ endpoints/comment/add.php | 79 +++ endpoints/comment/delete-reply.php | 44 ++ endpoints/comment/delete.php | 53 ++ endpoints/comment/detach-reply.php | 30 ++ endpoints/comment/downvote-reply.php | 61 +++ endpoints/comment/edit-reply.php | 62 +++ endpoints/comment/edit.php | 63 +++ endpoints/comment/flag-reply.php | 45 ++ endpoints/comment/out-of-date.php | 58 +++ endpoints/comment/rating.php | 31 ++ endpoints/comment/show-replies.php | 24 + endpoints/comment/sticky.php | 34 ++ endpoints/comment/undelete.php | 48 ++ endpoints/comment/upvote-reply.php | 61 +++ endpoints/comment/vote.php | 84 +++ endpoints/go-to-comment/go-to-comment.php | 42 ++ endpoints/go-to-reply/go-to-reply.php | 42 ++ includes/ajaxHandler/comment.class.php | 485 ------------------ includes/ajaxHandler/gotocomment.class.php | 40 -- .../components/communitycontent.class.php | 30 +- static/js/locale_dede.js | 1 - static/js/locale_enus.js | 1 - static/js/locale_eses.js | 1 - static/js/locale_frfr.js | 1 - static/js/locale_ruru.js | 1 - template/listviews/commentAdminCol.tpl | 4 +- 29 files changed, 1020 insertions(+), 541 deletions(-) create mode 100644 endpoints/admin/comment.php create mode 100644 endpoints/admin/out-of-date.php create mode 100644 endpoints/comment/add-reply.php create mode 100644 endpoints/comment/add.php create mode 100644 endpoints/comment/delete-reply.php create mode 100644 endpoints/comment/delete.php create mode 100644 endpoints/comment/detach-reply.php create mode 100644 endpoints/comment/downvote-reply.php create mode 100644 endpoints/comment/edit-reply.php create mode 100644 endpoints/comment/edit.php create mode 100644 endpoints/comment/flag-reply.php create mode 100644 endpoints/comment/out-of-date.php create mode 100644 endpoints/comment/rating.php create mode 100644 endpoints/comment/show-replies.php create mode 100644 endpoints/comment/sticky.php create mode 100644 endpoints/comment/undelete.php create mode 100644 endpoints/comment/upvote-reply.php create mode 100644 endpoints/comment/vote.php create mode 100644 endpoints/go-to-comment/go-to-comment.php create mode 100644 endpoints/go-to-reply/go-to-reply.php delete mode 100644 includes/ajaxHandler/comment.class.php delete mode 100644 includes/ajaxHandler/gotocomment.class.php diff --git a/endpoints/admin/comment.php b/endpoints/admin/comment.php new file mode 100644 index 00000000..08d4f8ba --- /dev/null +++ b/endpoints/admin/comment.php @@ -0,0 +1,51 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'status' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 1]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'status')) + { + trigger_error('AdminCommentResponse - malformed request received', E_USER_ERROR); + $this->result = self::ERR_MISCELLANEOUS; + return; + } + + // check if is marked as outdated CC_FLAG_OUTDATED? + + $ok = false; + if ($this->_post['status']) // outdated, mark as deleted and clear other flags (sticky + outdated) + { + if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = ?d, `deleteUserId` = ?d, `deleteDate` = ?d WHERE `id` = ?d', CC_FLAG_DELETED, User::$id, time(), $this->_post['id'])) + if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) + $rep->close(Report::STATUS_CLOSED_SOLVED); + } + else // up to date + { + if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id'])) + if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) + $rep->close(Report::STATUS_CLOSED_WONTFIX); + } + + $this->result = $ok ? self::ERR_NONE : self::ERR_WRITE_DB; + } +} + +?> diff --git a/endpoints/admin/out-of-date.php b/endpoints/admin/out-of-date.php new file mode 100644 index 00000000..4f0f1b00 --- /dev/null +++ b/endpoints/admin/out-of-date.php @@ -0,0 +1,34 @@ + Content > Out of Date Comments + + protected function generate() : void + { + $this->h1 = 'Out of Date Comments'; + array_unshift($this->title, $this->h1); + + $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); + + parent::generate(); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => CommunityContent::getCommentPreviews(['flags' => CC_FLAG_OUTDATED]), + 'extraCols' => '$_' + ), 'commentpreview', 'commentAdminCol')); + } +} + +?> diff --git a/endpoints/comment/add-reply.php b/endpoints/comment/add-reply.php new file mode 100644 index 00000000..ab2ab71d --- /dev/null +++ b/endpoints/comment/add-reply.php @@ -0,0 +1,51 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'replyId' => ['filter' => FILTER_VALIDATE_INT ], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + protected function generate(): void + { + if (!$this->assertPOST('commentId', 'replyId', 'body')) + { + trigger_error('CommentAddreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + if (!User::canReply()) + $this->generate404(Lang::main('cannotComment')); + + if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE `id` = ?d', $this->_post['commentId'])) + { + trigger_error('CommentAddreplyResponse - parent comment #'.$this->_post['commentId'].' does not exist', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + + if (mb_strlen($this->_post['body']) < CommunityContent::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > CommunityContent::REPLY_LENGTH_MAX) + $this->generate404(Lang::main('textLength', [mb_strlen($this->_post['body']), CommunityContent::REPLY_LENGTH_MIN, CommunityContent::REPLY_LENGTH_MAX])); + + if (!DB::Aowow()->query('INSERT INTO ?_comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (?d, ?d, ?, UNIX_TIMESTAMP(), ?d)', User::$id, User::$groups, $this->_post['body'], $this->_post['commentId'])) + { + trigger_error('CommentAddreplyResponse - write to db failed', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + + $this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); + } +} + +?> diff --git a/endpoints/comment/add.php b/endpoints/comment/add.php new file mode 100644 index 00000000..3cc8201e --- /dev/null +++ b/endpoints/comment/add.php @@ -0,0 +1,79 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + protected array $expectedGET = array( + 'type' => ['filter' => FILTER_VALIDATE_INT], + 'typeid' => ['filter' => FILTER_VALIDATE_INT] + ); + + // i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: ), yet, thats how it is + protected function generate() : void + { + if (!$this->assertGET('type', 'typeid') || !$this->assertPOST('commentbody') || !Type::validateIds($this->_get['type'], $this->_get['typeid'])) + { + trigger_error('CommentAddResponse - malforemd request received', E_USER_ERROR); + return; // whatever, we cant even send him back + } + + // we now have a valid return target + $idOrUrl = $this->_get['typeid']; + if ($this->_get['type'] == Type::GUIDE) + if ($_ = DB::Aowow()->selectCell('SELECT `url` FROM ?_guides WHERE `id` = ?d', $this->_get['typeid'])) + $idOrUrl = $_; + + $this->redirectTo = '?'.Type::getFileString($this->_get['type']).'='.$idOrUrl.'#comments'; + + // this type cannot be commented on + if (!Type::checkClassAttrib($this->_get['type'], 'contribute', CONTRIBUTE_CO)) + { + trigger_error('CommentAddResponse - tried to comment on unsupported type: '.Type::getFileString($this->_get['type']), E_USER_ERROR); + $_SESSION['error']['co'] = Lang::main('intError'); + return; + } + + if (!User::canComment()) + { + $_SESSION['error']['co'] = Lang::main('cannotComment'); + return; + } + + $len = mb_strlen($this->_post['commentbody']); + + if ((!User::isInGroup(U_GROUP_MODERATOR) && $len < CommunityContent::COMMENT_LENGTH_MIN) || ($len > CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))) + { + $_SESSION['error']['co'] = Lang::main('textLength', [$len, CommunityContent::COMMENT_LENGTH_MIN, CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1)]); + return; + } + + if ($postId = DB::Aowow()->query('INSERT INTO ?_comments (`type`, `typeId`, `userId`, `roles`, `body`, `date`) VALUES (?d, ?d, ?d, ?d, ?, UNIX_TIMESTAMP())', $this->_get['type'], $this->_get['typeid'], User::$id, User::$groups, $this->_post['commentbody'])) + { + Util::gainSiteReputation(User::$id, SITEREP_ACTION_COMMENT, ['id' => $postId]); + + // every comment starts with a rating of +1 and i guess the simplest thing to do is create a db-entry with the system as owner + DB::Aowow()->query('INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, 0, 1)', RATING_COMMENT, $postId); + + // flag target with hasComment + if ($tbl = Type::getClassAttrib($this->_get['type'], 'dataTable')) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $this->_get['typeid']); + + return; + } + + trigger_error('CommentAddResponse - write to db failed', E_USER_ERROR); + $_SESSION['error']['co'] = Lang::main('intError'); + } +} + +?> diff --git a/endpoints/comment/delete-reply.php b/endpoints/comment/delete-reply.php new file mode 100644 index 00000000..52b24b27 --- /dev/null +++ b/endpoints/comment/delete-reply.php @@ -0,0 +1,44 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDeletereplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + // flag as deleted (unset sticky (can a reply even be sticky?) + $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` = ?d { AND `userId` = ?d }', + CC_FLAG_STICKY, CC_FLAG_DELETED, + User::$id, + $this->_post['id'], + User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id + ); + + if ($ok) + DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_COMMENT, $this->_post['id']); + else + { + trigger_error('CommentDeletereplyResponse - deleting reply #'.$this->_post['id'].' by user #'.User::$id.' from db failed', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + } +} + +?> diff --git a/endpoints/comment/delete.php b/endpoints/comment/delete.php new file mode 100644 index 00000000..3893c308 --- /dev/null +++ b/endpoints/comment/delete.php @@ -0,0 +1,53 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + // 'username' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDeleteResponse - malformed request received', E_USER_ERROR); + return; + } + + // in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js) + $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` IN (?a) { AND `userId` = ?d }', + CC_FLAG_DELETED, + User::$id, + $this->_post['id'], + User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id + ); + + // unflag subject: hasComment + if ($ok) + { + $coInfo = DB::Aowow()->select( + 'SELECT IF(BIT_OR(~b.`flags`) & ?d, 1, 0) AS "0", b.`type` AS "1", b.`typeId` AS "2" FROM ?_comments a JOIN ?_comments b ON a.`type` = b.`type` AND a.`typeId` = b.`typeId` WHERE a.`id` IN (?a) GROUP BY b.`type`, b.`typeId`', + CC_FLAG_DELETED, $this->_post['id'] + ); + + foreach ($coInfo as [$hasMore, $type, $typeId]) + if (!$hasMore && ($tbl = Type::getClassAttrib($type, 'dataTable'))) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $typeId); + + return; + } + + trigger_error('CommentDeleteResponse - user #'.User::$id.' could not flag comment(s) #'.implode(', ', $this->_post['id']).' as deleted', E_USER_ERROR); + } +} + +?> diff --git a/endpoints/comment/detach-reply.php b/endpoints/comment/detach-reply.php new file mode 100644 index 00000000..6512a2fc --- /dev/null +++ b/endpoints/comment/detach-reply.php @@ -0,0 +1,30 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDetachreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + DB::Aowow()->query('UPDATE ?_comments c1, ?_comments c2 SET c1.`replyTo` = 0, c1.`type` = c2.`type`, c1.`typeId` = c2.`typeId` WHERE c1.`replyTo` = c2.`id` AND c1.`id` = ?d', $this->_post['id']); + } +} + +?> diff --git a/endpoints/comment/downvote-reply.php b/endpoints/comment/downvote-reply.php new file mode 100644 index 00000000..541a0d9b --- /dev/null +++ b/endpoints/comment/downvote-reply.php @@ -0,0 +1,61 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentDownvotereplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + if (!User::canDownvote()) + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot downvote' : ''); + + $comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']); + if (!$comment) + { + trigger_error('CommentDownvotereplyResponse - comment #'.$this->_post['id'].' not found in db', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'replyID not found' : ''); + } + + if (User::$id == $comment['userId']) // not worth logging? + $this->generate404('LANG.voteself_tip'); + + if ($comment['deleted']) + $this->generate404('LANG.votedeleted_tip'); + + $ok = DB::Aowow()->query( + 'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', + RATING_COMMENT, + $this->_post['id'], + User::$id, + User::canSupervote() ? -2 : -1 + ); + + if (!$ok) + { + trigger_error('CommentDownvotereplyResponse - write to db failed', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : ''); + } + + Util::gainSiteReputation($comment['userId'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_post['id'], 'voterId' => User::$id]); + User::decrementDailyVotes(); + } +} + +?> diff --git a/endpoints/comment/edit-reply.php b/endpoints/comment/edit-reply.php new file mode 100644 index 00000000..fa56e999 --- /dev/null +++ b/endpoints/comment/edit-reply.php @@ -0,0 +1,62 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'replyId' => ['filter' => FILTER_VALIDATE_INT ], + 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + + protected function generate() : void + { + if (!$this->assertPOST('commentId', 'replyId', 'body')) + { + trigger_error('CommentEditreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + $ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ?_comments WHERE `id` = ?d AND `replyTo` = ?d', $this->_post['replyId'], $this->_post['commentId']); + + if (!User::canReply() || (User::$id != $ownerId && !User::isInGroup(U_GROUP_MODERATOR))) + $this->generate404(Lang::main('cannotComment')); + + if (!$ownerId) + { + trigger_error('CommentEditreplyResponse - comment #'.$this->_post['commentId'].' or reply #'.$this->_post['replyId'].' does not exist', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + + if (mb_strlen($this->_post['body']) < CommunityContent::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > CommunityContent::REPLY_LENGTH_MAX) + $this->generate404(Lang::main('textLength', [mb_strlen($this->_post['body']), CommunityContent::REPLY_LENGTH_MIN, CommunityContent::REPLY_LENGTH_MAX])); + + $update = array( + 'body' => $this->_post['body'], + 'editUserId' => User::$id, + 'editDate' => time() + ); + if (User::$id == $ownerId) + $update['roles'] = User::$groups; + + if (!DB::Aowow()->query('UPDATE ?_comments SET `editCount` = `editCount` + 1, ?a WHERE `id` = ?d AND `replyTo` = ?d { AND `userId` = ?d }', + $update, $this->_post['replyId'], $this->_post['commentId'], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) + { + trigger_error('CommentEditreplyResponse - write to db failed', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + + $this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); + } +} + +?> diff --git a/endpoints/comment/edit.php b/endpoints/comment/edit.php new file mode 100644 index 00000000..4969990d --- /dev/null +++ b/endpoints/comment/edit.php @@ -0,0 +1,63 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']], + 'response' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']] + ); + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id') || !$this->assertPOST('body')) + { + trigger_error('CommentEditResponse - malforemd request received', E_USER_ERROR); + return; + } + + $ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ?_comments WHERE `id` = ?d', $this->_get['id']); + + if (!User::canComment() || (User::$id != $ownerId && !User::isInGroup(U_GROUP_MODERATOR))) + { + trigger_error('CommentEditResponse - user #'.User::$id.' not allowed to edit comment #'.$this->_get['id'], E_USER_ERROR); + return; + } + + if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['body']) < CommunityContent::COMMENT_LENGTH_MIN) + return; // no point in reporting this trifle + + // trim to max length + if (!User::isInGroup(U_GROUP_MODERATOR)) + $this->_post['body'] = mb_substr($this->_post['body'], 0, (CommunityContent::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))); + + $update = array( + 'body' => $this->_post['body'], + 'editUserId' => User::$id, + 'editDate' => time() + ); + if (User::$id == $ownerId) + $update['roles'] = User::$groups; + + if (User::isInGroup(U_GROUP_MODERATOR)) + { + $update['responseBody'] = $this->_post['response'] ?? ''; + $update['responseUserId'] = User::$id; + $update['responseRoles'] = User::$groups; + } + + DB::Aowow()->query('UPDATE ?_comments SET `editCount` = `editCount` + 1, ?a WHERE `id` = ?d', $update, $this->_get['id']); + } +} + +?> diff --git a/endpoints/comment/flag-reply.php b/endpoints/comment/flag-reply.php new file mode 100644 index 00000000..91044e50 --- /dev/null +++ b/endpoints/comment/flag-reply.php @@ -0,0 +1,45 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentFlagreplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + $replyOwner = DB::Aowow()->selectCell('SELECT `userId` FROM ?_commments WHERE `id` = ?d', $this->_post['id']); + if (!$replyOwner) + { + trigger_error('CommentFlagreplyResponse - reply not found', E_USER_ERROR); + $this->generate404(Lang::main('intError')); + } + + // ui element should not be present + if ($replyOwner == User::$id) + $this->generate404(); + + $report = new Report(Report::MODE_COMMENT, Report::CO_INAPPROPRIATE, $this->_post['id']); + if (!$report->create('Report Reply Button Click')) + $this->generate404('LANG.ct_resp_error'.$report->getError()); + else if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_DELETE) + DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']); + } +} + +?> diff --git a/endpoints/comment/out-of-date.php b/endpoints/comment/out-of-date.php new file mode 100644 index 00000000..d1c35432 --- /dev/null +++ b/endpoints/comment/out-of-date.php @@ -0,0 +1,58 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'remove' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 1]], + 'reason' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentOutofdateResponse - malformed request received', E_USER_ERROR); + if (User::isInGroup(U_GROUP_STAFF)) + $this->result = 'malformed request received'; + } + + $ok = false; + if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated + { + if (!$this->_post['remove']) + $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']); + else + $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']); + } + else // try to report as outdated + { + $report = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id']); + if (!$report->create($this->_post['reason'])) + $this->result = Lang::main('intError'); + + if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_OUT_OF_DATE) + $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']); + } + + if (!$ok) + { + trigger_error('CommentOutofdateResponse - failed to update comment in db', E_USER_ERROR); + $this->result = Lang::main('intError'); + return; + } + + $this->result = 'ok'; // the js expects the actual characters 'ok' on success, not some json string like '"ok"' + } +} + +?> diff --git a/endpoints/comment/rating.php b/endpoints/comment/rating.php new file mode 100644 index 00000000..bf69da13 --- /dev/null +++ b/endpoints/comment/rating.php @@ -0,0 +1,31 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + $this->result = Util::toJSON(['success' => 0]); + return; + } + + if ($votes = DB::Aowow()->selectRow('SELECT 1 AS "success", SUM(IF(`value` > 0, `value`, 0)) AS "up", SUM(IF(`value` < 0, -`value`, 0)) AS "down" FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` <> 0 GROUP BY `entry`', RATING_COMMENT, $this->_get['id'])) + $this->result = Util::toJSON($votes); + else + $this->result = Util::toJSON(['success' => 1, 'up' => 0, 'down' => 0]); + } +} + +?> diff --git a/endpoints/comment/show-replies.php b/endpoints/comment/show-replies.php new file mode 100644 index 00000000..2de0ed02 --- /dev/null +++ b/endpoints/comment/show-replies.php @@ -0,0 +1,24 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + $this->result = Util::toJSON([]); + else + $this->result = Util::toJSON(CommunityContent::getCommentReplies($this->_get['id'])); + } +} + +?> diff --git a/endpoints/comment/sticky.php b/endpoints/comment/sticky.php new file mode 100644 index 00000000..f2ecd1e9 --- /dev/null +++ b/endpoints/comment/sticky.php @@ -0,0 +1,34 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'sticky' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 1]] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id', 'sticky')) + { + trigger_error('CommentStickyResponse - malformed request received', E_USER_ERROR); + return; + } + + if ($this->_post['sticky']) + DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_STICKY, $this->_post['id']); + else + DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_STICKY, $this->_post['id']); + } +} + +?> diff --git a/endpoints/comment/undelete.php b/endpoints/comment/undelete.php new file mode 100644 index 00000000..eb56df49 --- /dev/null +++ b/endpoints/comment/undelete.php @@ -0,0 +1,48 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']], + // 'username' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentUndeleteResponse - malformed request received', E_USER_ERROR); + return; + } + + // in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js) + $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` IN (?a) { AND `userId` = `deleteUserId` AND `deleteUserId` = ?d }', + CC_FLAG_DELETED, + $this->_post['id'], + User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id + ); + + // unflag subject: hasComment + if ($ok) + { + $coInfo = DB::Aowow()->select('SELECT `type` AS "0", `typeId` AS "1" FROM ?_comments WHERE `id` IN (?a) GROUP BY `type`, `typeId`', $this->_post['id']); + foreach ($coInfo as [$type, $typeId]) + if ($tbl = Type::getClassAttrib($type, 'dataTable')) + DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $typeId); + + return; + } + + trigger_error('CommentUndeleteResponse - user #'.User::$id.' could not unflag comment(s) #'.implode(', ', $this->_post['id']).' from deleted', E_USER_ERROR); + } +} + +?> diff --git a/endpoints/comment/upvote-reply.php b/endpoints/comment/upvote-reply.php new file mode 100644 index 00000000..d40b5b75 --- /dev/null +++ b/endpoints/comment/upvote-reply.php @@ -0,0 +1,61 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertPOST('id')) + { + trigger_error('CommentUpvotereplyResponse - malformed request received', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); + } + + if (!User::canUpvote()) + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot upvote' : ''); + + $comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']); + if (!$comment) + { + trigger_error('CommentUpvotereplyResponse - comment #'.$this->_post['id'].' not found in db', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'replyID not found' : ''); + } + + if (User::$id == $comment['userId']) // not worth logging? + $this->generate404('LANG.voteself_tip'); + + if ($comment['deleted']) + $this->generate404('LANG.votedeleted_tip'); + + $ok = DB::Aowow()->query( + 'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', + RATING_COMMENT, + $this->_post['id'], + User::$id, + User::canSupervote() ? 2 : 1 + ); + + if (!$ok) + { + trigger_error('CommentUpvotereplyResponse - write to db failed', E_USER_ERROR); + $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : ''); + } + + Util::gainSiteReputation($comment['userId'], SITEREP_ACTION_UPVOTED, ['id' => $this->_post['id'], 'voterId' => User::$id]); + User::decrementDailyVotes(); + } +} + +?> diff --git a/endpoints/comment/vote.php b/endpoints/comment/vote.php new file mode 100644 index 00000000..7f459253 --- /dev/null +++ b/endpoints/comment/vote.php @@ -0,0 +1,84 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'rating' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => -2, 'max_range' => 2]] + ); + + protected function generate(): void + { + if (!$this->assertGET('id', 'rating')) + { + trigger_error('CommentVoteResponse - malformed request received', E_USER_ERROR); + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); + return; + } + + if (User::getCurrentDailyVotes() <= 0) + { + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('tooManyVotes')]); + return; + } + + $target = DB::Aowow()->selectRow( + 'SELECT c.`userId` AS "owner", ur.`value`, IF(c.`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments c LEFT JOIN ?_user_ratings ur ON ur.`type` = ?d AND ur.`entry` = c.id AND ur.`userId` = ?d WHERE c.id = ?d', + CC_FLAG_DELETED, RATING_COMMENT, User::$id, $this->_get['id'] + ); + if (!$target) + { + trigger_error('CommentVoteResponse - target comment #'.$this->_get['id'].' not found', E_USER_ERROR); + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); + return; + } + + $val = User::canSupervote() ? 2 : 1; + if ($this->_get['rating'] < 0) + $val *= -1; + + if (User::$id == $target['owner'] || $val != $this->_get['rating'] || $target['deleted']) + { + // circumvented the checks in JS + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); + return; + } + + if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote())) + { + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('bannedRating')]); + return; + } + + $ok = false; + // old and new have same sign; undo vote (user may have gained/lost access to superVote in the meantime) + if ($target['value'] && ($target['value'] < 0) == ($val < 0)) + $ok = DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_COMMENT, $this->_get['id'], User::$id); + else // replace, because we may be overwriting an old, opposing vote + if ($ok = DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_COMMENT, $this->_get['id'], User::$id, $val)) + User::decrementDailyVotes(); // do not refund retracted votes! + + if ($ok) + { + if ($val > 0) // gain rep + Util::gainSiteReputation($target['owner'], SITEREP_ACTION_UPVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); + else if ($val < 0) + Util::gainSiteReputation($target['owner'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); + + $this->result = Util::toJSON(['error' => 0]); + } + else + $this->result = Util::toJSON(['error' => 1, 'message' => Lang::main('intError')]); + } +} + +?> diff --git a/endpoints/go-to-comment/go-to-comment.php b/endpoints/go-to-comment/go-to-comment.php new file mode 100644 index 00000000..16112048 --- /dev/null +++ b/endpoints/go-to-comment/go-to-comment.php @@ -0,0 +1,42 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('GotocommentBaseResponse - malformed request received', E_USER_ERROR); + return; + } + + // type <> 0 AND typeId <> 0 AND replyTo = 0 for comments + $comment = DB::Aowow()->selectRow('SELECT `id`, `type`, `typeId` FROM ?_comments WHERE `replyTo` = 0 AND `id` = ?d', $this->_get['id']); + if (!$comment) + { + trigger_error('GotocommentBaseResponse - comment #'.$this->_get['id'].' not found', E_USER_ERROR); + return; + } + + if (!Type::validateIds($comment['type'], $comment['typeId'])) + { + trigger_error('GotocommentBaseResponse - comment #'.$this->_get['id'].' belongs to nonexistent type/typeID combo '.$comment['type'].'/'.$comment['typeId'], E_USER_ERROR); + return; + } + + $this->redirectTo = sprintf('?%s=%d#comments:id=%d', Type::getFileString($comment['type']), $comment['typeId'], $comment['id']); + } +} + +?> diff --git a/endpoints/go-to-reply/go-to-reply.php b/endpoints/go-to-reply/go-to-reply.php new file mode 100644 index 00000000..c13fdebd --- /dev/null +++ b/endpoints/go-to-reply/go-to-reply.php @@ -0,0 +1,42 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + protected function generate() : void + { + if (!$this->assertGET('id')) + { + trigger_error('GotoreplyBaseResponse - malformed request received', E_USER_ERROR); + return; + } + + // type = typeId = 0 AND replyTo <> 0 for replies + $reply = DB::Aowow()->selectRow('SELECT c.`id`, r.`id` AS "reply", c.`type`, c.`typeId` FROM ?_comments r JOIN ?_comments c ON r.`replyTo` = c.`id` WHERE r.`id` = ?d', $this->_get['id']); + if (!$reply) + { + trigger_error('GotoreplyBaseResponse - reply #'.$this->_get['id'].' not found', E_USER_ERROR); + return; + } + + if (!Type::validateIds($reply['type'], $reply['typeId'])) + { + trigger_error('GotoreplyBaseResponse - parent comment #'.$reply['id'].' belongs to nonexistent type/typeID combo '.$reply['type'].'/'.$reply['typeId'], E_USER_ERROR); + return; + } + + $this->redirectTo = sprintf('?%s=%d#comments:id=%d:reply=%d', Type::getFileString($reply['type']), $reply['typeId'], $reply['id'], $reply['reply']); + } +} + +?> diff --git a/includes/ajaxHandler/comment.class.php b/includes/ajaxHandler/comment.class.php deleted file mode 100644 index 0e943f01..00000000 --- a/includes/ajaxHandler/comment.class.php +++ /dev/null @@ -1,485 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkIdListUnsigned'], - 'body' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'commentbody' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'response' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'reason' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextBlob' ], - 'remove' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'commentId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'replyId' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - 'sticky' => ['filter' => FILTER_SANITIZE_NUMBER_INT ], - // 'username' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkTextLine' ] - ); - - protected $_get = array( - 'id' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'], - 'type' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'], - 'typeid' => ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'], - 'rating' => ['filter' => FILTER_SANITIZE_NUMBER_INT ] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - if (!$this->params || count($this->params) != 1) - return; - - // note: return values must be formated as STRICT json! - - // select handler - if ($this->params[0] == 'add') - $this->handler = 'handleCommentAdd'; - else if ($this->params[0] == 'edit') - $this->handler = 'handleCommentEdit'; - else if ($this->params[0] == 'delete') - $this->handler = 'handleCommentDelete'; - else if ($this->params[0] == 'undelete') - $this->handler = 'handleCommentUndelete'; - else if ($this->params[0] == 'rating') // up/down - distribution - $this->handler = 'handleCommentRating'; - else if ($this->params[0] == 'vote') // up, down and remove - $this->handler = 'handleCommentVote'; - else if ($this->params[0] == 'sticky') // toggle flag - $this->handler = 'handleCommentSticky'; - else if ($this->params[0] == 'out-of-date') // toggle flag - $this->handler = 'handleCommentOutOfDate'; - else if ($this->params[0] == 'show-replies') - $this->handler = 'handleCommentShowReplies'; - else if ($this->params[0] == 'add-reply') // also returns all replies on success - $this->handler = 'handleReplyAdd'; - else if ($this->params[0] == 'edit-reply') // also returns all replies on success - $this->handler = 'handleReplyEdit'; - else if ($this->params[0] == 'detach-reply') - $this->handler = 'handleReplyDetach'; - else if ($this->params[0] == 'delete-reply') - $this->handler = 'handleReplyDelete'; - else if ($this->params[0] == 'flag-reply') - $this->handler = 'handleReplyFlag'; - else if ($this->params[0] == 'upvote-reply') - $this->handler = 'handleReplyUpvote'; - else if ($this->params[0] == 'downvote-reply') - $this->handler = 'handleReplyDownvote'; - } - - // i .. have problems believing, that everything uses nifty ajax while adding comments requires a brutal header(Loacation: ), yet, thats how it is - protected function handleCommentAdd() : string - { - if (!$this->_get['typeid'] || !$this->_get['type'] || !Type::exists($this->_get['type'])) - { - trigger_error('AjaxComment::handleCommentAdd - malforemd request received', E_USER_ERROR); - return ''; // whatever, we cant even send him back - } - - // this type cannot be commented on - if (!Type::checkClassAttrib($this->_get['type'], 'contribute', CONTRIBUTE_CO)) - { - trigger_error('AjaxComment::handleCommentAdd - tried to comment on unsupported type #'.$this->_get['type'], E_USER_ERROR); - return ''; - } - - // trim to max length - if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['commentbody']) > (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))) - $this->_post['commentbody'] = mb_substr($this->_post['commentbody'], 0, (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))); - - if (User::canComment()) - { - if (!empty($this->_post['commentbody']) && mb_strlen($this->_post['commentbody']) >= self::COMMENT_LENGTH_MIN) - { - if ($postIdx = DB::Aowow()->query('INSERT INTO ?_comments (type, typeId, userId, roles, body, date) VALUES (?d, ?d, ?d, ?d, ?, UNIX_TIMESTAMP())', $this->_get['type'], $this->_get['typeid'], User::$id, User::$groups, $this->_post['commentbody'])) - { - Util::gainSiteReputation(User::$id, SITEREP_ACTION_COMMENT, ['id' => $postIdx]); - - // every comment starts with a rating of +1 and i guess the simplest thing to do is create a db-entry with the system as owner - DB::Aowow()->query('INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, 0, 1)', RATING_COMMENT, $postIdx); - - // flag target with hasComment - if ($tbl = Type::getClassAttrib($this->_get['type'], 'dataTable')) - DB::Aowow()->query('UPDATE '.$tbl.' SET cuFlags = cuFlags | ?d WHERE id = ?d', CUSTOM_HAS_COMMENT, $this->_get['typeid']); - } - else - { - $_SESSION['error']['co'] = Lang::main('intError'); - trigger_error('AjaxComment::handleCommentAdd - write to db failed', E_USER_ERROR); - } - } - else - $_SESSION['error']['co'] = Lang::main('textLength', [mb_strlen($this->_post['commentbody']), self::COMMENT_LENGTH_MIN, self::COMMENT_LENGTH_MAX]); - } - else - $_SESSION['error']['co'] = Lang::main('cannotComment'); - - $this->doRedirect = true; - - $idOrUrl = $this->_get['typeid']; - if ($this->_get['type'] == Type::GUIDE) - if ($_ = DB::Aowow()->selectCell('SELECT `url` FROM ?_guides WHERE `id` = ?d', $this->_get['typeid'])) - $idOrUrl = $_; - - return '?'.Type::getFileString($this->_get['type']).'='.$idOrUrl.'#comments'; - } - - protected function handleCommentEdit() : void - { - if (!User::canComment() && !User::isInGroup(U_GROUP_MODERATOR)) - { - trigger_error('AjaxComment::handleCommentEdit - user #'.User::$id.' not allowed to edit', E_USER_ERROR); - return; - } - - if (!$this->_get['id'] || !$this->_post['body']) - { - trigger_error('AjaxComment::handleCommentEdit - malforemd request received', E_USER_ERROR); - return; - } - - if (mb_strlen($this->_post['body']) < self::COMMENT_LENGTH_MIN) - return; // no point in reporting this trifle - - // trim to max length - if (!User::isInGroup(U_GROUP_MODERATOR) && mb_strlen($this->_post['body']) > (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))) - $this->_post['body'] = mb_substr($this->_post['body'], 0, (self::COMMENT_LENGTH_MAX * (User::isPremium() ? 3 : 1))); - - $update = array( - 'body' => $this->_post['body'], - 'editUserId' => User::$id, - 'editDate' => time() - ); - - if (User::isInGroup(U_GROUP_MODERATOR)) - { - $update['responseBody'] = !$this->_post['response'] ? '' : $this->_post['response']; - $update['responseUserId'] = !$this->_post['response'] ? 0 : User::$id; - $update['responseRoles'] = !$this->_post['response'] ? 0 : User::$groups; - } - - DB::Aowow()->query('UPDATE ?_comments SET editCount = editCount + 1, ?a WHERE id = ?d', $update, $this->_get['id']); - } - - protected function handleCommentDelete() : void - { - if (!$this->_post['id'] || !User::isLoggedIn()) - { - trigger_error('AjaxComment::handleCommentDelete - commentId empty or user not logged in', E_USER_ERROR); - return; - } - - // in theory, there is a username passed alongside... lets just use the current user (see user.js) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` IN (?a){ AND `userId` = ?d}', - CC_FLAG_DELETED, - User::$id, - $this->_post['id'], - User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id - ); - - // deflag hasComment - if ($ok) - { - $coInfo = DB::Aowow()->select('SELECT IF(BIT_OR(~b.`flags`) & ?d, 1, 0) AS hasMore, b.`type`, b.`typeId` FROM ?_comments a JOIN ?_comments b ON a.`type` = b.`type` AND a.`typeId` = b.`typeId` WHERE a.`id` IN (?a) GROUP BY b.`type`, b.`typeId`', - CC_FLAG_DELETED, - $this->_post['id'] - ); - - foreach ($coInfo as $co) - if (!$co['hasMore'] && ($tbl = Type::getClassAttrib($co['type'], 'dataTable'))) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $co['typeId']); - } - else - trigger_error('AjaxComment::handleCommentDelete - user #'.User::$id.' could not flag comment #'.$this->_post['id'].' as deleted', E_USER_ERROR); - } - - protected function handleCommentUndelete() : void - { - if (!$this->_post['id'] || !User::isLoggedIn()) - { - trigger_error('AjaxComment::handleCommentUndelete - commentId empty or user not logged in', E_USER_ERROR); - return; - } - - // in theory, there is a username passed alongside... lets just use the current user (see user.js) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` IN (?a){ AND `userId` = `deleteUserId` AND `deleteUserId` = ?d}', - CC_FLAG_DELETED, - $this->_post['id'], - User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id - ); - - // reflag hasComment - if ($ok) - { - $coInfo = DB::Aowow()->select('SELECT `type`, `typeId` FROM ?_comments WHERE `id` IN (?a) GROUP BY `type`, `typeId`', $this->_post['id']); - foreach ($coInfo as $co) - if ($tbl = Type::getClassAttrib($co['type'], 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $co['typeId']); - } - else - trigger_error('AjaxComment::handleCommentUndelete - user #'.User::$id.' could not unflag comment #'.$this->_post['id'].' as deleted', E_USER_ERROR); - } - - protected function handleCommentRating() : string - { - if (!$this->_get['id']) - return Util::toJSON(['success' => 0]); - - if ($votes = DB::Aowow()->selectRow('SELECT 1 AS success, SUM(IF(`value` > 0, `value`, 0)) AS up, SUM(IF(`value` < 0, -`value`, 0)) AS down FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND userId <> 0 GROUP BY `entry`', RATING_COMMENT, $this->_get['id'])) - return Util::toJSON($votes); - else - return Util::toJSON(['success' => 1, 'up' => 0, 'down' => 0]); - } - - protected function handleCommentVote() : string - { - if (!User::isLoggedIn() || !$this->_get['id'] || !$this->_get['rating']) - return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); - - $target = DB::Aowow()->selectRow('SELECT c.`userId` AS owner, ur.`value` FROM ?_comments c LEFT JOIN ?_user_ratings ur ON ur.`type` = ?d AND ur.`entry` = c.id AND ur.`userId` = ?d WHERE c.id = ?d', RATING_COMMENT, User::$id, $this->_get['id']); - $val = User::canSupervote() ? 2 : 1; - if ($this->_get['rating'] < 0) - $val *= -1; - - if (User::getCurrentDailyVotes() <= 0) - return Util::toJSON(['error' => 1, 'message' => Lang::main('tooManyVotes')]); - else if (!$target || $val != $this->_get['rating']) - return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); - else if (($val > 0 && !User::canUpvote()) || ($val < 0 && !User::canDownvote())) - return Util::toJSON(['error' => 1, 'message' => Lang::main('bannedRating')]); - - $ok = false; - // old and new have same sign; undo vote (user may have gained/lost access to superVote in the meantime) - if ($target['value'] && ($target['value'] < 0) == ($val < 0)) - $ok = DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_COMMENT, $this->_get['id'], User::$id); - else // replace, because we may be overwriting an old, opposing vote - if ($ok = DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_COMMENT, (int)$this->_get['id'], User::$id, $val)) - User::decrementDailyVotes(); // do not refund retracted votes! - - if (!$ok) - return Util::toJSON(['error' => 1, 'message' => Lang::main('genericError')]); - - if ($val > 0) // gain rep - Util::gainSiteReputation($target['owner'], SITEREP_ACTION_UPVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); - else if ($val < 0) - Util::gainSiteReputation($target['owner'], SITEREP_ACTION_DOWNVOTED, ['id' => $this->_get['id'], 'voterId' => User::$id]); - - return Util::toJSON(['error' => 0]); - } - - protected function handleCommentSticky() : void - { - if (!$this->_post['id'] || !User::isInGroup(U_GROUP_MODERATOR)) - { - trigger_error('AjaxComment::handleCommentSticky - commentId empty or user #'.User::$id.' not moderator', E_USER_ERROR); - return; - } - - if ($this->_post['sticky']) - DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_STICKY, $this->_post['id'][0]); - else - DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_STICKY, $this->_post['id'][0]); - } - - protected function handleCommentOutOfDate() : string - { - $this->contentType = MIME_TYPE_TEXT; - - if (!$this->_post['id']) - { - trigger_error('AjaxComment::handleCommentOutOfDate - commentId empty', E_USER_ERROR); - return Lang::main('intError'); - } - - $ok = false; - if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated - { - if (!$this->_post['remove']) - $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]); - else - $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags & ~?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]); - } - else // try to report as outdated - { - $report = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'][0]); - if ($report->create($this->_post['reason'])) - $ok = true; // the script expects the actual characters 'ok' not some json string like "ok" - else - return Lang::main('intError'); - - if (count($report->getSimilar()) >= 5) // 5 or more reports on the same comment: trigger flag - $ok = DB::Aowow()->query('UPDATE ?_comments SET flags = flags | ?d WHERE id = ?d', CC_FLAG_OUTDATED, $this->_post['id'][0]); - } - - if ($ok) - return 'ok'; - else - trigger_error('AjaxComment::handleCommentOutOfDate - failed to update comment in db', E_USER_ERROR); - - return Lang::main('intError'); - } - - protected function handleCommentShowReplies() : string - { - return Util::toJSON(!$this->_get['id'] ? [] : CommunityContent::getCommentReplies($this->_get['id'])); - } - - protected function handleReplyAdd() : string - { - $this->contentType = MIME_TYPE_TEXT; - - if (!User::canComment()) - return Lang::main('cannotComment'); - - if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE id = ?d', $this->_post['commentId'])) - { - trigger_error('AjaxComment::handleReplyAdd - comment #'.$this->_post['commentId'].' does not exist', E_USER_ERROR); - return Lang::main('intError'); - } - - if (!$this->_post['body'] || mb_strlen($this->_post['body']) < self::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > self::REPLY_LENGTH_MAX) - return Lang::main('textLength', [mb_strlen($this->_post['body']), self::REPLY_LENGTH_MIN, self::REPLY_LENGTH_MAX]); - - if (DB::Aowow()->query('INSERT INTO ?_comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (?d, ?d, ?, UNIX_TIMESTAMP(), ?d)', User::$id, User::$groups, $this->_post['body'], $this->_post['commentId'])) - return Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); - - trigger_error('AjaxComment::handleReplyAdd - write to db failed', E_USER_ERROR); - return Lang::main('intError'); - } - - protected function handleReplyEdit() : string - { - $this->contentType = MIME_TYPE_TEXT; - - if (!User::canComment()) - return Lang::main('cannotComment'); - - if ((!$this->_post['replyId'] || !$this->_post['commentId']) && DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_comments WHERE id IN (?a)', [$this->_post['replyId'], $this->_post['commentId']])) - { - trigger_error('AjaxComment::handleReplyEdit - comment #'.$this->_post['commentId'].' or reply #'.$this->_post['replyId'].' does not exist', E_USER_ERROR); - return Lang::main('intError'); - } - - if (!$this->_post['body'] || mb_strlen($this->_post['body']) < self::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > self::REPLY_LENGTH_MAX) - return Lang::main('textLength', [mb_strlen($this->_post['body']), self::REPLY_LENGTH_MIN, self::REPLY_LENGTH_MAX]); - - if (DB::Aowow()->query('UPDATE ?_comments SET body = ?, editUserId = ?d, editDate = UNIX_TIMESTAMP(), editCount = editCount + 1 WHERE id = ?d AND replyTo = ?d{ AND userId = ?d}', - $this->_post['body'], User::$id, $this->_post['replyId'], $this->_post['commentId'], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) - return Util::toJSON(CommunityContent::getCommentReplies($this->_post['commentId'])); - - trigger_error('AjaxComment::handleReplyEdit - write to db failed', E_USER_ERROR); - return Lang::main('intError'); - } - - protected function handleReplyDetach() : void - { - if (!$this->_post['id'] || !User::isInGroup(U_GROUP_MODERATOR)) - { - trigger_error('AjaxComment::handleReplyDetach - commentId empty or user #'.User::$id.' not moderator', E_USER_ERROR); - return; - } - - DB::Aowow()->query('UPDATE ?_comments c1, ?_comments c2 SET c1.replyTo = 0, c1.type = c2.type, c1.typeId = c2.typeId WHERE c1.replyTo = c2.id AND c1.id = ?d', $this->_post['id'][0]); - } - - protected function handleReplyDelete() : void - { - if (!User::isLoggedIn() || !$this->_post['id']) - { - trigger_error('AjaxComment::handleReplyDelete - commentId empty or user not logged in', E_USER_ERROR); - return; - } - - if (DB::Aowow()->query('DELETE FROM ?_comments WHERE id = ?d{ AND userId = ?d}', $this->_post['id'][0], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) - DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_COMMENT, $this->_post['id'][0]); - else - trigger_error('AjaxComment::handleReplyDelete - deleting comment #'.$this->_post['id'][0].' by user #'.User::$id.' from db failed', E_USER_ERROR); - } - - protected function handleReplyFlag() : void - { - if (!User::isLoggedIn() || !$this->_post['id']) - { - trigger_error('AjaxComment::handleReplyFlag - commentId empty or user not logged in', E_USER_ERROR); - return; - } - - $report = new Report(Report::MODE_COMMENT, Report::CO_INAPPROPRIATE, $this->_post['id'][0]); - $report->create('Report Reply Button Click'); - } - - protected function handleReplyUpvote() : void - { - if (!$this->_post['id'] || !User::canUpvote()) - { - trigger_error('AjaxComment::handleReplyUpvote - commentId empty or user not allowed to vote', E_USER_ERROR); - return; - } - - $owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->_post['id'][0]); - if (!$owner) - { - trigger_error('AjaxComment::handleReplyUpvote - comment #'.$this->_post['id'][0].' not found in db', E_USER_ERROR); - return; - } - - $ok = DB::Aowow()->query( - 'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', - RATING_COMMENT, - $this->_post['id'][0], - User::$id, - User::canSupervote() ? 2 : 1 - ); - - if ($ok) - { - Util::gainSiteReputation($owner, SITEREP_ACTION_UPVOTED, ['id' => $this->_post['id'][0], 'voterId' => User::$id]); - User::decrementDailyVotes(); - } - else - trigger_error('AjaxComment::handleReplyUpvote - write to db failed', E_USER_ERROR); - } - - protected function handleReplyDownvote() : void - { - if (!$this->_post['id'] || !User::canDownvote()) - { - trigger_error('AjaxComment::handleReplyDownvote - commentId empty or user not allowed to vote', E_USER_ERROR); - return; - } - - $owner = DB::Aowow()->selectCell('SELECT userId FROM ?_comments WHERE id = ?d', $this->_post['id'][0]); - if (!$owner) - { - trigger_error('AjaxComment::handleReplyDownvote - comment #'.$this->_post['id'][0].' not found in db', E_USER_ERROR); - return; - } - - $ok = DB::Aowow()->query( - 'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', - RATING_COMMENT, - $this->_post['id'][0], - User::$id, - User::canSupervote() ? -2 : -1 - ); - - if ($ok) - { - Util::gainSiteReputation($owner, SITEREP_ACTION_DOWNVOTED, ['id' => $this->_post['id'][0], 'voterId' => User::$id]); - User::decrementDailyVotes(); - } - else - trigger_error('AjaxComment::handleReplyDownvote - write to db failed', E_USER_ERROR); - } -} - -?> diff --git a/includes/ajaxHandler/gotocomment.class.php b/includes/ajaxHandler/gotocomment.class.php deleted file mode 100644 index 915c9102..00000000 --- a/includes/ajaxHandler/gotocomment.class.php +++ /dev/null @@ -1,40 +0,0 @@ - ['filter' => FILTER_CALLBACK, 'options' => 'Aowow\AjaxHandler::checkInt'] - ); - - public function __construct(array $params) - { - parent::__construct($params); - - // always this one - $this->handler = 'handleGoToComment'; - $this->doRedirect = true; - } - - /* responses - header() - */ - protected function handleGoToComment() : string - { - if (!$this->_get['id']) - return '.'; // go home - - if ($_ = DB::Aowow()->selectRow('SELECT IFNULL(c2.`id`, c1.`id`) AS "id", IFNULL(c2.`type`, c1.`type`) AS "type", IFNULL(c2.`typeId`, c1.`typeId`) AS "typeId" FROM ?_comments c1 LEFT JOIN ?_comments c2 ON c1.`replyTo` = c2.`id` WHERE c1.`id` = ?d', $this->_get['id'])) - return '?'.Type::getFileString(intVal($_['type'])).'='.$_['typeId'].'#comments:id='.$_['id'].($_['id'] != $this->_get['id'] ? ':reply='.$this->_get['id'] : null); - else - trigger_error('AjaxGotocomment::handleGoToComment - could not find comment #'.$this->_get['id'], E_USER_ERROR); - - return '.'; - } -} - -?> diff --git a/includes/components/communitycontent.class.php b/includes/components/communitycontent.class.php index 8655d4f9..14d039b0 100644 --- a/includes/components/communitycontent.class.php +++ b/includes/components/communitycontent.class.php @@ -20,6 +20,14 @@ if (!defined('AOWOW_REVISION')) class CommunityContent { + public const /* int */ COMMENT_LENGTH_MIN = 10; + public const /* int */ COMMENT_LENGTH_MAX = 7500; + public const /* int */ REPLY_LENGTH_MIN = 15; + public const /* int */ REPLY_LENGTH_MAX = 600; + + public const /* int */ REPORT_THRESHOLD_AUTO_DELETE = 10; + public const /* int */ REPORT_THRESHOLD_AUTO_OUT_OF_DATE = 5; + private static array $jsGlobals = []; private static array $subjCache = []; @@ -63,7 +71,7 @@ class CommunityContent FROM ?_videos v LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id` WHERE { v.`userIdOwner` = ?d AND }{ v.`type` = ? AND }{ v.`typeId` = ? AND } v.`status` & ?d AND (v.`status` & ?d) = 0 - { ORDER BY ?# DESC } + { ORDER BY ?# ASC } { LIMIT ?d }'; private static string $previewQuery = @@ -117,17 +125,19 @@ class CommunityContent */ // add default values - $opt += ['user' => 0, 'unrated' => 0, 'comments' => 0, 'replies' => 0]; + $opt += ['user' => 0, 'unrated' => 0, 'comments' => 0, 'replies' => 0, 'flags' => 0]; $w = []; if ($opt['user']) - $w[] = sprintf('c.userId = %d AND', $opt['user']); + $w[] = sprintf('c.`userId` = %d AND', $opt['user']); if ($opt['unrated']) - $w[] = 'ur.entry IS NULL AND'; + $w[] = 'ur.`entry` IS NULL AND'; + if ($opt['flags']) + $w[] = sprintf('(c.`flags` & %d) > 0 AND', $opt['flags']); if ($opt['comments'] && !$opt['replies']) - $w[] = 'c.replyTo = 0 AND'; + $w[] = 'c.`replyTo` = 0 AND'; else if (!$opt['comments'] && $opt['replies']) - $w[] = 'c.replyTo <> 0 AND'; + $w[] = 'c.`replyTo` <> 0 AND'; // else // pick both and no extra constraint needed for that @@ -178,7 +188,7 @@ class CommunityContent } else { - trigger_error('Comment '.$c['id'].' belongs to nonexistant subject.', E_USER_NOTICE); + trigger_error('Comment '.$c['id'].' belongs to nonexistent subject.', E_USER_NOTICE); unset($comments[$idx]); } } @@ -335,7 +345,7 @@ class CommunityContent { if (empty($p['name'])) { - trigger_error('Screenshot linked to nonexistant type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); + trigger_error('Screenshot linked to nonexistent type/typeId combination: '.$p['type'].'/'.$p['typeId'], E_USER_NOTICE); unset($p); } else @@ -417,7 +427,7 @@ class CommunityContent $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, CC_FLAG_APPROVED, CC_FLAG_DELETED, - !$typeOrUser ? 'date' : DBSIMPLE_SKIP, + !$typeOrUser ? 'date' : 'pos', !$typeOrUser ? Cfg::get('SQL_LIMIT_SEARCH') : DBSIMPLE_SKIP ); @@ -431,7 +441,7 @@ class CommunityContent $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, CC_FLAG_APPROVED, CC_FLAG_DELETED, - !$typeOrUser ? 'date' : DBSIMPLE_SKIP, + !$typeOrUser ? 'date' : 'pos', DBSIMPLE_SKIP ); diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 53b0745c..68a054cb 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -3244,7 +3244,6 @@ var LANG = { upvote_tip: "Dieser Kommentat ist hilfreich (klickt, um rückgängig zu machen)", upvoted_tip: "Ihr habt diesen Kommentar positiv bewertet; klickt, um dies rückgängig zu machen", downvote_tip: "Dieser Kommentar ist nicht hilfreich (klickt, um rückgängig zu machen)", - downvote_tip: "Dieser Kommentar ist nicht hilfreich (klickt, um rückgängig zu machen)", commentdeleted_tip: "Dieser Kommentar wurde gelöscht; er kann nur noch von Euch und Morderatoren gesehen werden.", addreply: "Antwort hinzufügen", replylength1_format: "Gebt mindestens $1 Zeichen ein", diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 7c260ecf..c60dbdbf 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -3293,7 +3293,6 @@ var LANG = { upvote_tip: "This comment is helpful (click again to undo)", upvoted_tip: "You have upvoted this comment; click again to undo", downvote_tip: "This comment is not helpful (click again to undo)", - downvote_tip: "This comment is not helpful (click again to undo)", commentdeleted_tip: "This comment has been deleted; it is now visible only to you and to moderators.", addreply: "Add reply", replylength1_format: "Enter at least $1 characters", diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index b720a44e..8cabdf1f 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -3244,7 +3244,6 @@ var LANG = { upvote_tip: "Este comentario es útil (haz clic de nuevo para deshacer)", upvoted_tip: "Has dado este comentario una valoración positiva; haz clic de nuevo para deshacerla.", downvote_tip: "Este comentario no es útil (haz clic de nuevo para deshacer)", - downvote_tip: "Este comentario no es útil (haz clic de nuevo para deshacer)", commentdeleted_tip: "Este comentario ha sido borrado; ahora sólo les es visible a ti y a los moderadores.", addreply: "Añadir respuesta", replylength1_format: "Introduce al menos $1 caracteres", diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 99d18899..b2f16454 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -3245,7 +3245,6 @@ var LANG = { upvote_tip: "Ce commentaire est utile (cliquez à nouveau pour annuler)", upvoted_tip: "Vous avez émis un vote positif pour ce commentaire ; cliquez à nouveau pour annuler", downvote_tip: "Ce commentaire n'est pas utile (cliquez à nouveau pour annuler)", - downvote_tip: "Ce commentaire n'est pas utile (cliquez à nouveau pour annuler)", commentdeleted_tip: "Ce commentaire a été effacé ; il n'est plus visible que par vous et par les modérateurs.", addreply: "Ajouter une réponse", replylength1_format: "Entrez au moins $1 caractères", diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index a9fa0801..bb57d724 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -3245,7 +3245,6 @@ var LANG = { upvote_tip: "Этот комментарий полезен (нажмите еще раз, чтобы отменить)", upvoted_tip: "Вы проголосовали за повышение этого комментария, нажмите снова для отмены", downvote_tip: "Этот комментарий бесполезен (нажмите снова для отмены)", - downvote_tip: "Этот комментарий бесполезен (нажмите снова для отмены)", commentdeleted_tip: "Этот комментарий был удален, теперь он видим только вам и модераторам.", addreply: "Ответить", replylength1_format: "Введите еще по меньшей мере $1 символов", diff --git a/template/listviews/commentAdminCol.tpl b/template/listviews/commentAdminCol.tpl index f6b6908a..40360178 100644 --- a/template/listviews/commentAdminCol.tpl +++ b/template/listviews/commentAdminCol.tpl @@ -27,7 +27,9 @@ var _ = [ return true; }; - let a = $WH.ce('a'); + td.onclick = $WH.sp; + + let a = $WH.ce('span'); a.style.fontFamily = 'Verdana, sans-serif'; a.style.marginLeft = '10px'; a.href = '#';