Sistem komentar PHP

Oct 07 2020

Dasar

Saya membuat sistem komentar sederhana. Tujuan saya adalah membuat sistem yang dapat dengan mudah digunakan di server semua orang tanpa harus menginstal banyak program. Saya juga mencoba membuatnya seramah mungkin dengan privasi (tanpa alamat email, tanpa cookie). Saya juga perlu menyelesaikan masalah ini tanpa database.

Kegunaan

  • Formulir dasar untuk mengirimkan komentar baru
  • Fungsi bendera (dengan email sederhana yang dikirim ke pemilik situs web)
  • Fungsi jawaban dengan jawaban yang dijorokkan

Kode

simpleComments.php

Skrip ini menyediakan fungsi utama: Perlindungan terhadap spam (dengan saran dari sini dan di sini ), mengirim, menjawab, dan menandai komentar. Saya pikir terutama fungsinya save()terlihat adalah solusi yang agak hacky. Jika Anda mengetahui alternatif yang lebih baik (tanpa database), saya akan senang mendengarnya.

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

Kode tersebut telah diperiksa dengan phpcodechecker.com dan tidak menemukan masalah apa pun.

File lainnya tidak terlalu berharga untuk ditinjau, jadi saya akan meninggalkannya di sini.

Tautan

Bagi mereka yang masih tertarik dengan file lain dan bagaimana caranya, silakan lihat repositori untuk proyek ini.

Ada juga live-demo untuk kamu yang ingin mengujinya.

Pertanyaan

Setiap saran diterima. Seperti disebutkan sebelumnya, saya akan sangat tertarik pada solusi yang lebih elegan untuk save()fungsi-.

Jawaban

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

Umpan Balik Awal

Saya suka penggunaan docblocks di atas fungsinya. The save()Fungsi membuat baik penggunaan kembali awal ke tingkat batas lekukan, kecuali untuk cek terakhir - ketika $solutiontidak sesuai $summaka dapat menghubungi error()segera. Secara keseluruhan, fungsi tersebut cukup panjang - melanggar Prinsip Tanggung Jawab Tunggal . Fungsionalitas untuk menulis ke file dapat dipindahkan ke fungsi terpisah untuk setiap kasus (komentar vs jawaban). Stylesheet dapat dipindahkan ke file CSS.

Seperti yang saya sebutkan dalam jawaban ini, token CSRF dapat menggantikan kebutuhan untuk pembuatan gambar, pengkodean dan dekode.

Saran

Variabel global

Seperti yang disarankan orang lain, variabel global memiliki lebih banyak aspek negatif daripada positif . Anda dapat memberikan kata sandi enkripsi ke setiap fungsi yang membutuhkannya, tetapi itu akan memerlukan pembaruan tanda tangan dari setiap fungsi yang membutuhkannya. Pilihan lainnya adalah membuat konstanta bernama menggunakan define().

define('ENCRYPTION_PASSWORD', 'xyz');

Ini dapat dilakukan dalam file terpisah yang disertakan melalui include()(atau include_once()) atau require()(atau require_once()), yang dapat dipisahkan dari kontrol versi (misalnya file .env)

Konstanta juga dapat dibuat menggunakan constkata kunci - di luar kelas pada PHP 5.3.0 1 .

const ENCRYPTION_PASSWORD = 'xyz';

Seperti yang sudah disarankan, menggunakan kelas dengan namespace adalah ide bagus. Kelas akan mengizinkan penggunaan konstanta kelas yang akan diberi spasi nama ke kelas dan memiliki visibilitas khusus mulai PHP 7.1 2 .

Mudah-mudahan kode Anda berjalan pada PHP 7.2 atau yang lebih baru, karena versi tersebut secara resmi didukung 3 .

Iterasi dengan referensi

Fungsi ini isForbiddenmengulangi konten file yang ditunjuk dalam $blacklistmenetapkan nilai dengan referensi:

 foreach($explode as &$value) {

Ini tampaknya tidak perlu karena $valuetidak diubah di dalam loop. Sebaiknya hindari praktik seperti itu kecuali Anda yakin elemen array perlu dimodifikasi.

Kesetaraan yang ketat

Anda mungkin sudah pernah mendengar ini: merupakan kebiasaan yang baik untuk menggunakan operator perbandingan yang ketat - yaitu ===dan !==jika memungkinkan - misalnya untuk perbandingan ini dalam save():

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

count()mengembalikan intdan 2merupakan intsehingga !==dapat digunakan karena tidak diperlukan konversi jenis.

Input tersembunyi

HTML yang dihasilkan untuk formulir berisi:

<input style='display: none;' 

Ini bisa sedikit disederhanakan dengan menggunakan tipe input tersembunyi :

<input type="hidden"

Meskipun masukan apa pun dapat ditampilkan oleh pengguna dengan memodifikasi halaman melalui konsol browser atau cara lain, masukan tersembunyi dibuat untuk tujuan menyembunyikan nilai formulir.

4 KIKOSoftware Oct 07 2020 at 06:22

Ini hanya satu saran, dan bukan tinjauan lengkap kode Anda.

Kebanyakan sistem komentar digunakan untuk mengomentari sesuatu yang bukan merupakan komentar. Seperti di halaman demo Anda. Artinya, kode Anda akan dimasukkan ke halaman orang lain. Ini mungkin halaman yang sangat kompleks. Dengan kata lain, kode Anda harus hidup berdampingan, berbagai macam kode lain yang mungkin tak ada habisnya. Bagaimana jika kode itu menyertakan fungsi seperti getTime(), error()atau save()? Kemudian kode Anda akan merusak halaman itu.

Inilah sebabnya, kode yang ingin kami bagikan dengan pengembang lain, hampir selalu ditulis dalam gaya pemrograman berorientasi objek (OOP). Objek, dan spasi nama, digunakan untuk memisahkan kode Anda dari kode orang-orang yang menggunakan kode Anda.

Beberapa tautan:

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

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

https://phptherightway.com

Bahkan jika Anda meninggalkan kode Anda, seperti sekarang, saya akan menyarankan untuk lebih kreatif dengan nama yang Anda pilih. Misalnya, nama fungsi randExer()tidak ada artinya bagi saya. Nama yang lebih baik akan seperti itu getCaptchaImageHtml(). Nama ini sebenarnya menjelaskan apa yang dilakukan fungsi dan apa yang dikembalikannya. Hal yang sama berlaku untuk fungsi lainnya. Ini adalah pendapat saya bahwa singkatan jarang harus dihindari dalam nama fungsi.

2 slepic Oct 07 2020 at 17:20

Hindari menggunakan variabel global.

Variabel $encryptionPasswordharus lulus sebagai argumen untuk semua fungsi yang membutuhkannya ( randExer, getTime, randTextdan save).

Ada beberapa alasan untuk melakukannya:

  • Anda menghindari konflik dengan variabel global lainnya (bukan seharusnya ada)
  • Anda menghindari akses tidak disengaja ke variabel itu oleh mereka yang seharusnya tidak mengaksesnya.
  • Tanda tangan fungsi menyembunyikan ketergantungan pada kata sandi enkripsi, yang membuatnya lebih sulit untuk memahami kode oleh pembaca, karena mereka harus membaca badan fungsi untuk memahami bahwa ada ketergantungan semacam itu. Membaca hanya tanda tangan fungsi sudah cukup.
  • Fungsinya lebih mudah untuk diuji
  • dan mungkin lebih ...

Fungsi yang menggunakan variabel global tidak bisa murni menurut definisi. Fungsi murni umumnya lebih disukai karena alasan yang disebutkan di atas.

EDIT: Cara yang mungkin untuk memecahkan masalah variabel global adalah dengan mempromosikan fungsi ke metode kelas dan meneruskan sandi enkripsi ke konstruktornya:

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