Basics
I created a simple comment system. My goal was it to create a system that can easily be used on everyone's server without having to install a load of programs. I also tried to create it as privacy-friendly as possible (no email-address, no cookies). I also need to solve this problem without databases.
Functionality
- Basic form to submit new comments
- Flag-functionality (with simple email send to the owner of the website)
- Answer functionality with indented answers
Code
simpleComments.php
This script provides the main functionality: Spam-protection (with suggestions from here and here), sending, answering and flagging comments. I think that especially the function save()
looks is a rather hacky solution. If you know a better alternative (without databases), I would be happy to hear it.
//The password for the AES-Encryption (has to be length=16) $encryptionPassword = "****************"; //============================================================================================ //============================================================================================ // == // FROM HERE ON NO ADJUSTMENT NECESSARY == // == //============================================================================================ //============================================================================================ /** * Creates image * * This function creates a black image with the random exercise created by randText() on it. * Additionally the function adds some random lines to make it more difficult for bots to read * the text via OCR. The result (for example) looks like this: https://imgur.com/a/6imIE73 * * @author Philipp Wilhelm * * @since 1.0 * * @param string $rand Random exercise created by randText() * @param int $width Width of the image (default = 200) * @param int $height Height of the image (default = 50) * @param int $textColorRed R-RGB value for the textcolor (0-255) (default = 255) * @param int $textColorGreen G-RGB value for the textcolor (0-255) (default = 255) * @param int $textColorBlue B-RGB value for the textcolor (0-255) (default = 255) * @param int $linesColorRed R-RGB value for the random lines (0-255) (default = 192) * @param int $linesColorGreen G-RGB value for the random lines (0-255) (default = 192) * @param int $linesColorBlue B-RGB value for the random lines (0-255) (default = 192) * @param int $fontSize font size of the text on the image (1-5) (default = 5) * @param int $upperLeftCornerX x-coordinate of upper-left corner of the first char (default = 18) * @param int $upperLeftCornerY y-coordinate of the upper-left corner of the first char (default = 18) * @param int $angle angle the text will be rotated by (default = 10) * * @return string created image surrounded by <img> */ function randExer($rand, $width = 200, $height = 50, $textColorRed = 255, $textColorGreen = 255, $textColorBlue = 255, $linesColorRed = 192, $linesColorGreen = 192, $linesColorBlue = 192, $fontSize = 5, $upperLeftCornerX = 18, $upperLeftCornerY = 18, $angle = 10) { global $encryptionPassword; $random = openssl_decrypt($rand,"AES-128-ECB", $encryptionPassword); $random = substr($random, 0, -40); //Creates a black picture $img = imagecreatetruecolor($width, $height); //uses RGB-values to create a useable color $textColor = imagecolorallocate($img, $textColorRed, $textColorGreen, $textColorBlue); $linesColor = imagecolorallocate($img, $linesColorRed, $linesColorGreen, $linesColorBlue); //Adds text imagestring($img, $fontSize, $upperLeftCornerX, $upperLeftCornerY, $random . " = ?", $textColor); //Adds random lines to the images for($i = 0; $i < 5; $i++) { imagesetthickness($img, rand(1, 3)); $x1 = rand(0, $width / 2); $y1 = rand(0, $height / 2); $x2 = $x1 + rand(0, $width / 2); $y2 = $y1 + rand(0, $height / 2); imageline($img, $x1, $x2, $x2, $y2, $linesColor); } $rotate = imagerotate($img, $angle, 0); //Attribution: https://stackoverflow.com/a/22266437/13634030 ob_start(); imagejpeg($rotate); $contents = ob_get_contents(); ob_end_clean(); $imageData = base64_encode($contents); $src = "data:" . mime_content_type($contents) . ";base64," . $imageData; return "<img alt='' src='" . $src . "'/>"; }; /** * Returns time stamp * * This function returns the current time stamp, encrypted with AES, by using the standard function time(). * * @author Philipp Wilhelm * * @since 1.0 * * @return int time stamp */ function getTime() { global $encryptionPassword; return openssl_encrypt(time() . bin2hex(random_bytes(20)),"AES-128-ECB", $encryptionPassword); } /** * Creates random exercise * * This function creates a random simple math-problem, by choosing two random numbers between "zero" and "ten". * The result looks like this: "three + seven" * * @author Philipp Wilhelm * * @since 1.0 * * @return string random exercise */ function randText() { global $encryptionPassword; //Creating random (simple) math problem $arr = array("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"); $item1 = $arr[array_rand($arr)]; $item2 = $arr[array_rand($arr)]; $random = $item1 . " + " . $item2; $encrypted = openssl_encrypt($random . bin2hex(random_bytes(20)),"AES-128-ECB", $encryptionPassword); return $encrypted; } /** * flags comment * * This function sends an email to the specified adress containing the id of the flagged comment * * @author Philipp Wilhelm * * @since 1.0 * * @param string $to Email-adress the mail will be send to * @param string $url URL of the site the comment was flagged on * */ function flag($to, $url) { //Which comment was flagged? $id = $_POST["comment"]; //At what side was the comment flagged? $referer = $_SERVER["HTTP_REFERER"]; $subject = "FLAG"; $body = $id . " was flagged at " . $referer . "."; //Send the mail mail($to, $subject, $body); //Redirect to what page after flag? //(In this case to the same page) header("Location:" . $url); exit(); } /** * redirects to the same page, but with the added parameter to specify to which * comment will be answered and jumps right to the comment-form * * * @author Philipp Wilhelm * * @since 1.0 * * @param string $url the url of the current page * @param string $buttonName URL of the site the comment was flagged on * @param string $urlName the "id-name" * */ function answer($url, $buttonName, $urlName) { header("Location:" . $url . "?" . $urlName . "=" . $_POST["comment"] . "#" . $buttonName); exit(); } /** * error message * * Redirects to the specified url to tell the user that something went wrong * e.g. entered wrong solution to math-exercise * * @author Philipp Wilhelm * * @since 1.0 * * @param string $urlError The specified url * */ function error($urlError) { header("Location:" . $urlError); die(); } /** * Redirects to specified url when user enters words that are on the "blacklist" * * @author Philipp Wilhelm * * @since 1.0 * * @param string $urlBadWords The specified url to which will be redirected * */ function badWords($urlBadWords) { header("Location:" . $urlBadWords); die(); } /** * Redirects to same url after comment is successfully submitted - comment will be visible * immediately * * @author Philipp Wilhelm * * @since 1.0 * * @param string $url URL of the site * */ function success($url) { header("Location:" . $url); die(); } /** * checks if user enters any words that are on the "blacklist" * * @author Philipp Wilhelm * * @since 1.0 * * @param string $text The user-entered text * @param string $blackList filename of the "blacklist" * * @return boolean true if user entered a word that is on the "blacklist" * */ function isForbidden($text, $blackList) { //gets content of the blacklist-file $content = file_get_contents($blackList); $text = strtolower($text); //Creates an array with all the words from the blacklist $explode = explode(",", $content); foreach($explode as &$value) { //Pattern checks for whole words only ('hell' in 'hello' will not count) $pattern = sprintf("/\b(%s)\b/",$value); if(preg_match($pattern, $text) == 1) { return true; } } return false; } /** * saves a new comment or an answer to a comment * * @author Philipp Wilhelm * * @since 1.0 * * @param string $url Email-adress the mail will be send to * @param string $urlError URL to the "error"-page * @param string $urlBadWords URL to redirect to , when user uses words on the "blacklist" * @param string $blacklist filename of the blacklist * @param string $fileName filename of the file the comments are stored in * @param string $nameInputTagName name of the input-field for the "name" * @param string $messageInputTagName name of the input-field for the "message" * @param string exerciseInputTagName name of the input-field the math-problem is stored in * @param string solutionInputTagName name of the input-field the user enters the solution in * @param string $answerInputTagName in this field the id of the comment the user answers to is saved * (if answering to a question) * @param string $timeInputTagName name of the input-field the timestamp is stored in * */ function save($url, $urlError, $urlBadWords, $blacklist, $fileName, $nameInputTagName, $messageInputTagName, $exerciseInputTagName, $solutionInputTagName, $answerInputTagName, $timeInputTagName) { global $encryptionPassword; $solution = filter_input(INPUT_POST, $solutionInputTagName, FILTER_VALIDATE_INT); $exerciseText = filter_input(INPUT_POST, $exerciseInputTagName); if ($solution === false || $exerciseText === false) { error($urlError); } $time = openssl_decrypt($_POST[$timeInputTagName], "AES-128-ECB", $encryptionPassword); if(!$time) { error($urlError); } $time = substr($time, 0, -40); $t = intval($time); if(time() - $t > 300) { error($urlError); } //Get simple math-problem (e.g. four + six) $str = openssl_decrypt($_POST[$exerciseInputTagName], "AES-128-ECB", $encryptionPassword); $str = substr($str, 0, -40); if (!$str) { error($urlError); } $arr = array("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"); //gets array with written numbers $words = array_map("trim", explode("+", $str)); //gets the numbers as ints $numbers = array_intersect($arr, $words); if (count($numbers) != 2) { error($urlError); } $sum = array_sum(array_keys($numbers)); $urlPicture = "identicon.php/?size=24&hash=" . md5($_POST[$nameInputTagName]); //Did user enter right solution? if ($solution == $sum) { $name = $_POST[$nameInputTagName]; $comment = htmlspecialchars($_POST[$messageInputTagName]); $content = file_get_contents($fileName); if(strcmp($content, "<p>No comments yet!</p>") == 0 || strcmp($content, "<p>No comments yet!</p>\n") == 0) { $content = "<p>Identicons created with <a href='https://github.com/timovn/identicon'>identicon.php</a> (licensed under <a href='http://www.gnu.org/licenses/gpl-3.0.en.html'>GPL-3.0</a>).</p>"; } $id = bin2hex(random_bytes(20)); $answerID = $_POST[$answerInputTagName]; //Checks if user used any words from the blacklist if(isForbidden($comment, $blacklist)) { badWords($urlBadWords); } //Case the user writes a new comment (not an answer) if(strlen($answerID) < 40) { file_put_contents($fileName, //Needed styles "<style>" . ".commentBox {" . "display: block;" . "background: LightGray;" . "width: 90%;" . "border-radius: 10px;" . "padding: 10px;" . "margin-bottom: 5px;" . "} " . "input[name='flag'], input[name='answer'] {" . "border: none;" . "padding: 0;" . "margin: 0;" . "margin-top: 5px;" . "padding: 2px;" . "background: transparent;" . "}" . "</style>" . //get random avatar "<img class='icon' style='vertical-align:middle;' src='" . $urlPicture . "'/>" . //Displaying user name "<span><b> " . $name . "</b></span> says:<br>" . //Current UTC-time and -date "<span style='font-size: small'>" . gmdate("d-m-Y H:i") . " UTC</span><br>" . //The main comment "<div class='commentBox'>" . $comment . "<br>" . "</div>". "<div style='width: 90%; font-size: small; float: left'>" . //Flag-button "<form style='margin: 0; padding: 0; float: left;' method='POST' action='simpleComments.php'>" . "<input style='display: none;' name='comment' type='text' value='" . $id . "'/>" . "<input style='color: red;' type='submit' name='flag' value='Flag'/>" . "</form>" . //Answer-button "<form id='answer' style='margin-left: 0; padding: 0; float: left;' method='POST' action='simpleComments.php'>" . "<input style='display: none;' name='comment' type='text' value='" . $id . "'/>" . "<input style='color: green;' type='submit' name='answer' value='Answer'/>" . "</form>" . "<!-- " . $id . " -->" . "</div>" . "<br><br>" . $content); success($url); } //Case that user writes an answer else { if(strpos($content, $answerID) !== false) { $explode = explode("<!-- " . $answerID . " -->", $content); file_put_contents($fileName, $explode[0] . "</div>" . "<br><br>" . //Needed styles "<style>" . ".answerBox {" . "display: block;" . "background: LightGray;" . "width: 90%;" . "border-radius: 10px;" . "padding: 10px;" . "margin-bottom: 5px;" . "} " . "input[name='flag'] {" . "border: none;" . "padding: 0;" . "margin: 0;" . "margin-top: 5px;" . "padding: 2px;" . "background: transparent;" . "}" . "</style>" . "<div style='margin-left: 50px'>" . //get random avatar "<img class='icon' style='vertical-align:middle;' src='" . $urlPicture . "'/>" . //Displaying user name "<span><b> " . $name . "</b></span> says:<br>" . //Current UTC-time and -date "<span style='font-size: small'>" . gmdate("d-m-Y H:i") . " UTC</span><br>" . //The main comment "<div class='answerBox'>" . $comment . "<br>" . "</div>". //Flag-button "<div style='width: 90%; font-size: small; float: left'>" . "<form style='margin: 0; padding: 0; float: left;' method='POST' action='simpleComments.php'>" . "<input style='display: none;' name='comment' type='text' value='" . $id . "'/>" . "<input style='color: red;' type='submit' name='flag' value='Flag'/>" . "</form><br><br>" . "</div>" . "<!-- " . $answerID . " -->" . $explode[1]); success($url); } } } error($urlError); } //============================================================================================ //============================================================================================ // == // FROM HERE ON ADJUSTMENT ARE NECESSARY == // == //============================================================================================ //============================================================================================ /** * start point of the script * * @author Philipp Wilhelm * * @since 1.0 * * */ function start() { //To what email-adress should the flag-notification be send? $to = "[email protected]"; //What's the url you are using this system for? (exact link to e.g. the blog-post) $url = "https://example.com/post001.html"; //Which page should be loaded when something goes wrong? $urlError = "https://example.com/messageError.html"; //What page should be loaded when user submits words from your "blacklist"? $urlBadWords = "https://example.com/badWords.html"; //In which file are the comments saved? $fileName = "testComments.php"; //What's the filename of your "blacklist"? $blackList = "blacklist.txt"; //Replace with the name-attribute of the respective input-field //No action needed here, if you didn't update form.php $nameInputTagName = "myName"; $messageInputTagName = "myMessage"; $exerciseInputTagName = "exerciseText"; $solutionInputTagName = "solution"; $answerInputTagName = "answerID"; $timeInputTagName = "time"; $buttonName = "postComment"; $urlName = "id"; if (isset($_POST["flag"])) { flag($to, $url); } if (isset($_POST["answer"])) { answer($url, $buttonName, $urlName); } if (isset($_POST[$buttonName])) { save($url, $urlError, $urlBadWords, $blackList, $fileName, $nameInputTagName, $messageInputTagName, $exerciseInputTagName, $solutionInputTagName, $answerInputTagName, $timeInputTagName); } } start(); ?>
The code was checked with phpcodechecker.com and it didn't find any problems.
The other files are not really worth reviewing, so I will leave it here.
Links
For those who are nevertheless interested in the other files and a how-to, please see the repository for this project.
There also is a live-demo for those of you who want to test it.
Question
Every suggestions are welcome. As mentioned before, I would be especially interested in a more elegant solution for the save()
-function.
$encrypted = ...; return $encrypted;
). Ask yourself why$value
needs to be modifiable by reference if you never modify it.preg_match()
returns a truthy/falsey value. If+
has a space on both sides, why notexplode()
on 3 characters instead of 1? (avoiding iteratedtrim()
calls) You must not like my previous suggestion codereview.stackexchange.com/questions/248695/…\$\endgroup\$