Sistema de comentarios PHP
Lo esencial
Creé un sistema de comentarios simple. Mi objetivo era crear un sistema que se pueda usar fácilmente en el servidor de todos sin tener que instalar una gran cantidad de programas. También intenté crearlo lo más amigable posible con la privacidad (sin dirección de correo electrónico, sin cookies). También necesito resolver este problema sin bases de datos.
Funcionalidad
- Formulario básico para enviar nuevos comentarios
- Función de bandera (con un simple correo electrónico enviado al propietario del sitio web)
- Función de respuesta con respuestas sangradas
Código
simpleComments.php
Este script proporciona la funcionalidad principal: protección contra correo no deseado (con sugerencias aquí y aquí ), envío, respuesta y marcación de comentarios. Creo que especialmente la función save()parece ser una solución bastante hacky. Si conoce una alternativa mejor (sin bases de datos), me encantaría escucharla.
//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();
?>
El código fue verificado con phpcodechecker.com y no encontró ningún problema.
No vale la pena revisar los otros archivos, así que los dejaré aquí.
Enlaces
Para aquellos que, no obstante, estén interesados en los otros archivos y en un procedimiento, consulte el repositorio de este proyecto.
También hay una demostración en vivo para aquellos de ustedes que quieran probarlo.
Pregunta
Todas las sugerencias son bienvenidas. Como se mencionó anteriormente, estaría especialmente interesado en una solución más elegante para la save()función-.
Respuestas
Comentarios iniciales
Me gusta el uso de docblocks por encima de las funciones. La save()función hace un buen uso de regresar temprano para limitar los niveles de sangría, excepto en la última verificación: cuando $solutionno coincide $sum, puede llamar de error()inmediato. En general, esa función es bastante prolongada: viola el principio de responsabilidad única . La funcionalidad para escribir en el archivo se podría mover a funciones separadas para cada caso (comentario vs respuesta). Las hojas de estilo se pueden mover a un archivo CSS.
Como mencioné en esta respuesta, los tokens CSRF podrían reemplazar la necesidad de creación, codificación y decodificación de imágenes.
Sugerencias
Variables globales
Como han sugerido otros, las variables globales tienen más aspectos negativos que positivos . Usted podría pasar la contraseña de cifrado para cada función que lo necesita, pero que requeriría una actualización de firmas de cada función que lo requiera. Otra opción es crear una constante con nombre usando define().
define('ENCRYPTION_PASSWORD', 'xyz');
Esto se puede hacer en un archivo separado que se incluye a través de include()(o include_once()) o require()(o require_once()), que podría estar separado del control de versiones (por ejemplo, un archivo .env)
Las constantes también se pueden crear usando la constpalabra clave - fuera de una clase a partir de PHP 5.3.0 1 .
const ENCRYPTION_PASSWORD = 'xyz';
Como ya se sugirió, usar una clase con un espacio de nombres es una gran idea. Una clase permitiría el uso de una constante de clase que tendría un espacio de nombres en la clase y tendría una visibilidad específica a partir de PHP 7.1 2 .
Es de esperar que su código se ejecute en PHP 7.2 o posterior, ya que esas versiones son oficialmente compatibles 3 .
Iteración por referencia
La función isForbiddenitera sobre el contenido del archivo al que se apunta al $blacklistasignar el valor por referencia:
foreach($explode as &$value) {
Esto parece innecesario porque $valueno se modifica dentro del ciclo. Podría ser mejor evitar esta práctica a menos que esté seguro de que los elementos de la matriz deben modificarse.
Igualdad estricta
Es posible que ya haya escuchado esto: es un buen hábito usar operadores de comparación estrictos , es decir, ===y !==cuando sea posible, por ejemplo, para esta comparación dentro de save():
if (count($numbers) != 2) {
count()devuelve un inty 2es un intpor lo que !==se puede utilizar ya que no hay necesidad de conversión de tipos.
Entradas ocultas
El HTML generado para los formularios contiene:
<input style='display: none;'
Esto podría simplificarse ligeramente usando el tipo de entrada oculta :
<input type="hidden"
Si bien el usuario puede mostrar cualquier entrada modificando la página a través de la consola del navegador u otros medios, la entrada oculta se creó con el propósito de ocultar los valores del formulario.
Esta es solo una sugerencia y no una revisión completa de su código.
La mayoría de los sistemas de comentarios se utilizan para comentar algo que no es en sí mismo un comentario. Como en su página de demostración. Esto significa que su código se incluirá en la página de otra persona. Esta puede ser una página muy compleja. En otras palabras, su código tendrá que convivir con una variedad posiblemente interminable de otros códigos. ¿Qué pasa si ese código incluye funciones como getTime(), error()o save()? Entonces su código romperá esa página.
Por eso, el código que queremos compartir con otros desarrolladores, casi siempre está escrito en un estilo de programación orientada a objetos (OOP). Los objetos y espacios de nombres se utilizan para aislar su código del código de aquellos que usan su código.
Algunos enlaces:
https://phpenthusiast.com/object-oriented-php-tutorials
https://www.thoughtfulcode.com/a-complete-guide-to-php-namespaces
https://phptherightway.com
Incluso si deja su código, como está ahora, le aconsejaría que sea más creativo con los nombres que elija. Por ejemplo, el nombre de la función randExer()no significa nada para mí. Un mejor nombre sería algo así como getCaptchaImageHtml(). Este nombre describe realmente lo que hace la función y lo que devuelve. Lo mismo se aplica a las demás funciones. En mi opinión, se deben evitar las abreviaturas poco comunes en los nombres de las funciones.
Evite el uso de variables globales.
La variable $encryptionPasswordse debe pasar como argumento a todas las funciones que lo necesiten ( randExer, getTime, randTexty save).
Hay varias razones para hacerlo:
- Evitas conflictos con otras variables globales (no es que deba haber ninguna)
- Evitas el acceso accidental a esa variable por parte de quienes no deberían acceder a ella.
- Las firmas de las funciones ocultan la dependencia de una contraseña de cifrado, lo que dificulta la comprensión del código por parte de los lectores, ya que tienen que leer el cuerpo de la función para comprender que existe tal dependencia. Leer solo la firma de la función debería ser suficiente.
- Las funciones son más fáciles de probar
- y probablemente más ...
Una función que usa una variable global no puede ser pura por definición. Las funciones puras se prefieren generalmente por las razones mencionadas anteriormente.
EDITAR: Una forma posible de resolver el problema de la variable global es promover las funciones a métodos de clase y pasar la contraseña de cifrado a su constructor:
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();