PHP yorum sistemi

Oct 07 2020

Temel bilgiler

Basit bir yorum sistemi oluşturdum. Amacım, bir sürü program yüklemeye gerek kalmadan herkesin sunucusunda kolayca kullanılabilecek bir sistem oluşturmaktı. Ayrıca olabildiğince gizlilik dostu oluşturmaya çalıştım (e-posta adresi yok, çerez yok). Bu sorunu veritabanları olmadan da çözmem gerekiyor.

İşlevsellik

  • Yeni yorum göndermek için temel form
  • Bayrak işlevselliği (web sitesinin sahibine gönderilen basit e-posta ile)
  • Girintili yanıtlarla yanıt işlevi

Kod

simpleComments.php

Bu komut dosyası ana işlevi sağlar: Spam koruması ( buradan ve buradan önerilerle ), yorumları gönderme, yanıtlama ve işaretleme. Özellikle işlev save()görünümünün oldukça hilekar bir çözüm olduğunu düşünüyorum . Daha iyi bir alternatif biliyorsanız (veritabanları olmadan), bunu duymaktan mutluluk duyarım.

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

Kod phpcodechecker.com ile kontrol edildi ve herhangi bir sorun bulamadı.

Diğer dosyalar gerçekten incelemeye değmez, bu yüzden burada bırakacağım.

Bağlantılar

Yine de diğer dosyalar ve nasıl yapılır ile ilgilenenler için, lütfen bu projenin havuzuna bakın .

Ayrıca Orada canlı demo test etmek istiyorum olanlarınız için.

Soru

Her öneriye açığız. Daha önce de belirtildiği gibi, özellikle save()-fonksiyon için daha zarif bir çözümle ilgilenirim .

Yanıtlar

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

İlk Geri Bildirim

İşlevlerin üzerinde docblock kullanımını seviyorum. save()Fonksiyon son onay haricinde, sınır girinti seviyelere erken dönen en iyi şekilde yapar - zaman $solutionuymuyor $sumo zaman çağırabilirsiniz error()hemen. Genel olarak bu işlev oldukça uzundur - Tek Sorumluluk İlkesini ihlal eder . Dosyaya yazma işlevi, her durum için ayrı işlevlere taşınabilir (yorum ve yanıt). Stil sayfaları bir CSS dosyalarına taşınabilir.

Bu cevapta bahsettiğim gibi, CSRF tokenleri görüntü oluşturma, kodlama ve kod çözme ihtiyacının yerini alabilir.

Öneriler

Global değişkenler

Başkalarının da önerdiği gibi, küresel değişkenlerin olumlu yönlerden daha olumsuz yönleri vardır . Sen olabilir ihtiyacı her fonksiyona şifreleme şifresini geçmesi, ama bu ihtiyaçlar bunu her işlevin imza güncelleme gerektirecektir. Diğer bir seçenek, kullanarak adlandırılmış bir sabit oluşturmaktır define().

define('ENCRYPTION_PASSWORD', 'xyz');

Bu , sürüm kontrolünden ayrı olabilen include()(veya include_once()) veya require()(veya require_once()) aracılığıyla dahil edilen ayrı bir dosyada yapılabilir (ör. Bir .env dosyası)

Sabitler, constPHP 5.3.0'dan itibaren bir sınıfın dışında - anahtar sözcüğü kullanılarak da oluşturulabilir 1 .

const ENCRYPTION_PASSWORD = 'xyz';

Daha önce önerildiği gibi, bir ad alanına sahip bir sınıf kullanmak harika bir fikirdir. A sınıfı bir kullanımını sağlayacak sınıf sabiti sınıfına isim alanlı olacak ve belirli sahip görünürlük PHP 7.1 itibariyle 2 .

Umarım kodunuz PHP 7.2 veya sonrasında çalışmaktadır, çünkü bu sürümler resmi olarak desteklenmektedir 3 .

Referansla yineleme

İşlev , değeri referansa göre atarken isForbiddenişaret edilen dosyanın içeriğini yineler $blacklist:

 foreach($explode as &$value) {

$valueDöngü içinde değiştirilmediğinden bu gereksiz görünüyor . Dizi elemanlarının değiştirilmesi gerektiğinden emin değilseniz, böyle bir uygulamadan kaçınmak en iyisi olabilir.

Kesin eşitlik

Bunu daha önce duymuş olabilirsiniz: katı karşılaştırma operatörleri kullanmak iyi bir alışkanlıktır - yani ===ve !==mümkün olduğunda - örneğin bu karşılaştırma için save():

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

count()Bir döner intve 2bir bir intçok !==tip dönüşüm için gerek yoktur olarak kullanılabilir.

Gizli girişler

Formlar için oluşturulan HTML şunları içerir:

<input style='display: none;' 

Bu, gizli giriş türü kullanılarak biraz basitleştirilebilir :

<input type="hidden"

Sayfayı tarayıcı konsolu veya başka yollarla değiştirerek kullanıcı tarafından herhangi bir girdi görüntülenebilirken, gizli girdi form değerlerini gizlemek amacıyla oluşturuldu.

4 KIKOSoftware Oct 07 2020 at 06:22

Bu yalnızca tek bir öneridir ve kodunuzun tam bir incelemesi değildir.

Çoğu yorum sistemi, kendisi bir yorum olmayan bir şey hakkında yorum yapmak için kullanılır. Demo sayfanızdaki gibi. Bu, kodunuzun başka birinin sayfasına ekleneceği anlamına gelir . Bu çok karmaşık bir sayfa olabilir. Başka bir deyişle, kodunuzun muhtemelen sonsuz çeşitlilikteki başka kodlarla birlikte yaşaması gerekecektir. Ya bu kod getTime(), error()veya gibi işlevler içeriyorsa save()? Sonra kodunuz o sayfayı kırar.

Bu nedenle, diğer geliştiricilerle paylaşmak istediğimiz kod, neredeyse her zaman nesne yönelimli programlama (OOP) tarzında yazılmıştır. Kullanılan nesneler ve ad boşlukları, kodunuzu kodunuzu kullananların kodundan ayırır.

Bazı bağlantılar:

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

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

https://phptherightway.com

Kodunuzu bıraksanız bile şimdi olduğu gibi seçeceğiniz isimler konusunda daha yaratıcı olmanızı tavsiye ederim. Örneğin, işlev adı randExer()benim için hiçbir şey ifade etmiyor. Daha iyi bir isim şöyle bir şey olurdu getCaptchaImageHtml(). Bu ad aslında işlevin ne yaptığını ve ne döndürdüğünü açıklar. Aynısı diğer işlevler için de geçerlidir. Öyle Bence nadir kısaltma fonksiyonu adlarında kaçınılması gerektiğini.

2 slepic Oct 07 2020 at 17:20

Global değişkenler kullanmaktan kaçının.

Değişken $encryptionPasswordlüzum tüm işlevlerine bir argüman olarak iletilmesi gereken ( randExer, getTime, randTextve save).

Bunu yapmanın birkaç nedeni vardır:

  • Diğer global değişkenlerle çatışmalardan kaçınırsınız (olması gerekenden değil)
  • Erişmemesi gerekenlerin bu değişkene yanlışlıkla erişimini önlersiniz.
  • İşlevlerin imzaları, bir şifreleme parolasına olan bağımlılığı gizler, bu da okuyucular tarafından kodu anlamayı zorlaştırır, çünkü böyle bir bağımlılığın olduğunu anlamak için işlev gövdesini okumaları gerekir. Sadece fonksiyonun imzasını okumak yeterli olmalıdır.
  • Fonksiyonların test edilmesi daha kolay
  • ve muhtemelen daha fazlası ...

Global bir değişken kullanan bir işlev, tanımı gereği saf olamaz. Yukarıda belirtilen nedenlerden dolayı genellikle saf fonksiyonlar tercih edilir.

DÜZENLEME: Genel değişken sorununu çözmenin olası bir yolu, işlevleri sınıf yöntemlerine yükseltmek ve şifreleme parolasını kurucusuna iletmektir:

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