Sistema di commenti PHP

Oct 07 2020

Nozioni di base

Ho creato un semplice sistema di commenti. Il mio obiettivo era creare un sistema che potesse essere facilmente utilizzato sul server di tutti senza dover installare un carico di programmi. Ho anche provato a crearlo il più rispettoso della privacy possibile (nessun indirizzo email, nessun cookie). Devo anche risolvere questo problema senza database.

Funzionalità

  • Modulo di base per inviare nuovi commenti
  • Funzionalità di flag (con semplice invio di email al proprietario del sito web)
  • Funzionalità di risposta con risposte rientrate

Codice

simpleComments.php

Questo script fornisce le funzionalità principali: protezione dallo spam (con suggerimenti da qui e qui ), invio, risposta e contrassegno di commenti. Penso che soprattutto l' save()aspetto della funzione sia una soluzione piuttosto hacker. Se conosci un'alternativa migliore (senza database), sarei felice di ascoltarla.

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

Il codice è stato verificato con phpcodechecker.com e non ha riscontrato alcun problema.

Gli altri file non meritano davvero di essere esaminati, quindi lo lascerò qui.

Collegamenti

Per coloro che sono comunque interessati agli altri file e alle istruzioni, consultare il repository di questo progetto.

C'è anche un live-demo per quelli di voi che vogliono provarlo.

Domanda

Tutti i suggerimenti sono i benvenuti. Come accennato prima, sarei particolarmente interessato a una soluzione più elegante per la funzione- save().

Risposte

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

Feedback iniziale

Mi piace l'utilizzo di docblock sopra le funzioni. La save()funzione fa buon uso del ritorno anticipato per limitare i livelli di indentazione, eccetto per l'ultimo controllo: quando $solutionnon corrisponde $sumpuò chiamare error()subito. Nel complesso, tale funzione è piuttosto lunga: viola il principio di responsabilità unica . La funzionalità per scrivere sul file potrebbe essere spostata in funzioni separate per ogni caso (commento vs risposta). I fogli di stile possono essere spostati in un file CSS.

Come ho detto in questa risposta, i token CSRF potrebbero sostituire la necessità di creare, codificare e decodificare le immagini.

Suggerimenti

Variabili globali

Come altri hanno suggerito, le variabili globali hanno più aspetti negativi che positivi . È possibile passare la password di crittografia a ciascuna funzione che ne ha bisogno, ma ciò richiederebbe l'aggiornamento della firma di ciascuna funzione che ne ha bisogno. Un'altra opzione è creare una costante con nome utilizzando define().

define('ENCRYPTION_PASSWORD', 'xyz');

Questo potrebbe essere fatto in un file separato incluso tramite include()(o include_once()) o require()(o require_once()), che potrebbe essere separato dal controllo della versione (ad esempio un file .env)

Le costanti possono anche essere create utilizzando la constparola chiave - al di fuori di una classe a partire da PHP 5.3.0 1 .

const ENCRYPTION_PASSWORD = 'xyz';

Come già suggerito, usare una classe con uno spazio dei nomi è un'ottima idea. Una classe consentirebbe l'uso di una costante di classe che avrebbe uno spazio dei nomi rispetto alla classe e avrebbe una visibilità specifica a partire da PHP 7.1 2 .

Si spera che il tuo codice sia in esecuzione su PHP 7.2 o successivo, poiché quelle versioni sono ufficialmente supportate 3 .

Iterazione per riferimento

La funzione isForbiddenitera sul contenuto del file a cui si punta $blacklistnell'assegnazione del valore per riferimento:

 foreach($explode as &$value) {

Ciò non sembra necessario perché $valuenon viene modificato all'interno del ciclo. Potrebbe essere meglio evitare tale pratica a meno che non si sia certi che gli elementi dell'array debbano essere modificati.

Rigorosa uguaglianza

Potresti averlo già sentito: è una buona abitudine usare operatori di confronto rigorosi, cioè ===e !==quando possibile, ad esempio per questo confronto all'interno di save():

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

count()restituisce un inted 2è un intquindi !==può essere utilizzato poiché non è necessaria la conversione del tipo.

Ingressi nascosti

L'HTML generato per i moduli contiene:

<input style='display: none;' 

Questo potrebbe essere leggermente semplificato usando il tipo di input nascosto :

<input type="hidden"

Sebbene qualsiasi input possa essere visualizzato dall'utente modificando la pagina tramite la console del browser o altri mezzi, l'input nascosto è stato creato allo scopo di nascondere i valori del modulo.

4 KIKOSoftware Oct 07 2020 at 06:22

Questo è solo un singolo suggerimento e non una revisione completa del codice.

La maggior parte dei sistemi di commento viene utilizzata per commentare qualcosa che non è di per sé un commento. Come nella tua pagina demo. Ciò significa che il tuo codice verrà incluso nella pagina di qualcun altro. Questa potrebbe essere una pagina molto complessa. In altre parole, il tuo codice dovrà convivere con una varietà possibilmente infinita di altro codice. E se quel codice include funzioni come getTime(), error()o save()? Quindi il tuo codice interromperà quella pagina.

Questo è il motivo per cui il codice che vogliamo condividere con altri sviluppatori è quasi sempre scritto in uno stile di programmazione orientata agli oggetti (OOP). Gli oggetti e gli spazi dei nomi vengono utilizzati per isolare il codice dal codice di coloro che utilizzano il codice.

Alcuni link:

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

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

https://phptherightway.com

Anche se lasci il codice, così com'è ora, ti consiglio di essere più creativo con i nomi che scegli. Ad esempio, il nome della funzione non randExer()significa nulla per me. Un nome migliore sarebbe qualcosa di simile getCaptchaImageHtml(). Questo nome descrive effettivamente cosa fa la funzione e cosa restituisce. Lo stesso vale per le altre funzioni. È mia opinione che le abbreviazioni non comuni dovrebbero essere evitate nei nomi delle funzioni.

2 slepic Oct 07 2020 at 17:20

Evita di usare variabili globali.

La variabile $encryptionPassworddeve essere passato come argomento a tutte le funzioni che ne hanno bisogno ( randExer, getTime, randTexte save).

Ci sono diversi motivi per farlo:

  • Eviti conflitti con altre variabili globali (non che dovrebbero essercene)
  • Eviti l'accesso accidentale a quella variabile da parte di chi non dovrebbe accedervi.
  • Le firme delle funzioni nascondono la dipendenza da una password di crittografia, il che rende più difficile la comprensione del codice da parte dei lettori, perché devono leggere il corpo della funzione per capire che esiste una tale dipendenza. Leggere solo la firma della funzione dovrebbe essere sufficiente.
  • Le funzioni sono più facili da testare
  • e probabilmente di più ...

Una funzione che utilizza una variabile globale non può essere pura per definizione. Le funzioni pure sono generalmente preferite per i motivi sopra menzionati.

EDIT: un modo possibile per risolvere il problema della variabile globale è promuovere le funzioni ai metodi di classe e passare la password di crittografia al suo costruttore:

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