Система комментариев PHP

Oct 07 2020

Основы

Я создал простую систему комментариев. Моей целью было создать систему, которую можно было бы легко использовать на каждом сервере без необходимости установки множества программ. Я также попытался сделать его максимально безопасным для конфиденциальности (без адреса электронной почты, без файлов cookie). Мне тоже нужно решить эту проблему без баз данных.

Функциональность

  • Базовая форма для отправки новых комментариев
  • Флаг-функциональность (с простой отправкой электронной почты владельцу сайта)
  • Функциональность ответов с отступом ответов

Код

simpleComments.php

Этот скрипт обеспечивает основные функции: защиту от спама (с предложениями здесь и здесь ), отправку, ответ и отметку комментариев. Я считаю, что особенно save()внешний вид функции - довольно хакерское решение. Если вы знаете альтернативу получше (без баз данных), я был бы рад ее услышать.

//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();
?>

Код был проверен на phpcodechecker.com и никаких проблем не обнаружил.

Остальные файлы не стоит просматривать, поэтому я оставлю их здесь.

Ссылки

Для тех, кто, тем не менее, интересуется другими файлами и инструкциями, см. Репозиторий для этого проекта.

Также есть живая демонстрация для тех из вас, кто хочет его протестировать.

Вопрос

Любые предложения приветствуются. Как упоминалось ранее, меня особенно заинтересует более элегантное решение для save()-функции.

Ответы

3 SᴀᴍOnᴇᴌᴀ Oct 09 2020 at 05:55

Первоначальный отзыв

Мне нравится использование докблоков над функциями. save()Функция позволяет эффективно использовать возвращения рано предельные уровни отступа, за исключением последней проверки - когда $solutionне совпадает , $sumто он может позвонить error()сразу. В целом эта функция довольно продолжительна - она ​​нарушает принцип единой ответственности . Функциональность записи в файл может быть перенесена в отдельные функции для каждого случая (комментарий против ответа). Таблицы стилей можно переместить в файл (ы) CSS.

Как я уже упоминал в этом ответе, токены CSRF могут заменить необходимость создания, кодирования и декодирования изображений.

Предложения

Глобальные переменные

Как предполагали другие, глобальные переменные имеют больше отрицательных аспектов, чем положительных . Вы можете передать пароль шифрования каждой функции, которая в нем нуждается, но для этого потребуется обновить подпись каждой функции, которая в нем нуждается. Другой вариант - создать именованную константу с помощью define().

define('ENCRYPTION_PASSWORD', 'xyz');

Это можно сделать в отдельном файле, который включается через include()(или include_once()) или require()(или require_once()), который может быть отделен от контроля версий (например, файл .env).

Константы также могут быть созданы с использованием constключевого слова - вне класса, начиная с PHP 5.3.0 1 .

const ENCRYPTION_PASSWORD = 'xyz';

Как уже было предложено, использование класса с пространством имен - отличная идея. Класс позволит использовать константу класса, которая будет помещена в пространство имен для класса и будет иметь определенную видимость, начиная с PHP 7.1 2 .

Надеюсь, ваш код работает на PHP 7.2 или новее, поскольку эти версии официально поддерживаются 3 .

Итерация по ссылке

Функция isForbiddenвыполняет итерацию по содержимому файла, на который указывает при $blacklistприсвоении значения по ссылке:

 foreach($explode as &$value) {

Это кажется ненужным, потому что $valueне изменяется внутри цикла. Возможно, лучше избегать такой практики, если вы не уверены, что элементы массива нужно изменить.

Строгое равенство

Возможно, вы уже слышали об этом: это хорошая привычка использовать строгие операторы сравнения - т.е. ===и !==когда это возможно - например, для этого сравнения в пределах save():

if (count($numbers) != 2) {

count()возвращает intи 2это intтак !==может быть использована как нет необходимости преобразования типов.

Скрытые входы

HTML-код, созданный для форм, содержит:

<input style='display: none;' 

Это можно немного упростить, используя скрытый тип ввода:

<input type="hidden"

Хотя любой ввод может быть отображен пользователем, изменяя страницу через консоль браузера или другими способами, скрытый ввод был создан с целью скрытия значений формы.

4 KIKOSoftware Oct 07 2020 at 06:22

Это всего лишь одно предложение, а не полный обзор вашего кода.

Большинство систем комментариев используются для комментирования того, что само по себе не является комментарием. Как на вашей демонстрационной странице. Это означает, что ваш код будет включен на чужую страницу. Это может быть очень сложная страница. Другими словами, ваш код должен будет существовать рядом с, возможно, бесконечным разнообразием другого кода. Что, если этот код включает такие функции, как getTime(), error()или save()? Тогда ваш код сломает эту страницу.

Вот почему код, которым мы хотим поделиться с другими разработчиками, почти всегда написан в стиле объектно-ориентированного программирования (ООП). Объекты и пространства имен используются для изоляции вашего кода от кода тех, кто использует ваш код.

Некоторые ссылки:

https://phpenthusiast.com/object-oriented-php-tutorials

https://www.thoughtfulcode.com/a-complete-guide-to-php-namespaces

https://phptherightway.com

Даже если вы оставите свой код в том виде, в каком он есть сейчас, я бы посоветовал проявить более творческий подход к выбору имен. Например, название функции randExer()для меня ничего не значит. Лучшее название было бы примерно так getCaptchaImageHtml(). Это имя фактически описывает, что функция делает и что она возвращает. То же самое и с другими функциями. Это мое мнение , что редко аббревиатурой следует избегать имен функций.

2 slepic Oct 07 2020 at 17:20

Избегайте использования глобальных переменных.

Переменная $encryptionPasswordдолжна быть передана в качестве аргумента для всех функций , которые нуждаются в ней ( randExer, getTime, randTextи save).

Для этого есть несколько причин:

  • Вы избегаете конфликтов с другими глобальными переменными (хотя их не должно быть)
  • Вы избегаете случайного доступа к этой переменной со стороны тех, кто не имеет к ней доступа.
  • Сигнатуры функций скрывают зависимость от пароля шифрования, что затрудняет понимание кода читателями, потому что они должны прочитать тело функции, чтобы понять, что существует такая зависимость. Достаточно просто прочитать подпись функции.
  • Функции легче тестировать
  • и возможно больше ...

Функция, использующая глобальную переменную, не может быть чистой по определению. Чистые функции обычно предпочтительны по причинам, указанным выше.

РЕДАКТИРОВАТЬ: Возможный способ решить проблему с глобальной переменной - продвигать функции в методы класса и передавать пароль шифрования его конструктору:

class ASuitableClassName
{
  private string $encryptionPassword;

  public function __construct(string $encryptionPassword) { $this->encryptionPassword = $encryptionPassword; } public function getTime() { return openssl_encrypt(time() . bin2hex(random_bytes(20)),"AES-128-ECB", $this->encryptionPassword);
  }
  
  // ....
}

$obj = new ASuitableClassName("****************"); $obj->getTime();