1
\$\begingroup\$

So few days ago, I had created another review request for sqlite database schema for a comment hosting system.

Now that the app development is complete, I have pushed the initial version on github. As suggested in that post, I am making another request here for review of this app as a whole. This is a complete web stack written in PHP and for frontend, I have used HTML+Bootstrap+ECMAScript+jquery. The core directory contains routing and other utilities, lib/auth has logic for user management, lib/PHPMailer is a third party library for future use of sending email notifications. The static is for static files and partials used to build the frontend HTML for the comments block.

Other than that, most of the business logic is contained in index.php which has most of the controller part of the MVC. I have also written a detailed tutorial on installing and using this app.

Please let me know if there is anything that could be improved here, and feel free to create issues on the github tracker. The project is under active development and more changes are coming soon.

Here is the source code for index.php, the primary ingredient in this mix:

<?php
/**
* index.php
*/
header('Access-Control-Allow-Origin: *');
header("Access-Control-Allow-Methods: GET, POST");

require('core/router.php');
require('core/util.php');
require("lib/auth.php");
require("config.php");
session_start();

//@todo: initialize constants and vars

//for easy access to your static paths with base_url()
//Router::$base_url = "http://localhost:8000/src/"; 

//set blank if you rewrite index.php in .htaccess
//Router::$index_file = 'index.php'; 

const VERSION = "1.0";
const APP_NAME = "Comment Monk";

//@todo: initialize database
$dbh = new PDO("sqlite:cm.db");

function create_post ($uri, $user) {
    global $dbh;
    $data= ['uri'=>$uri, 'user_id'=>$user['id']];
    [$sql, $values] = build_insert_query("posts", $data);
    $sth = $dbh->prepare($sql);
    $sth->execute($values);
    return $dbh->lastInsertId();
};


Router::$pre_dispatch = function() use ($dbh) {
    //check install and any other common initialization or redirection
    try {
        $dbh->query("select id from users limit 1;");
    } catch (PDOException $e) {
        //echo "uri_segment:" . uri_segment(-2);
        if (uri_segment(-1) !== "install") {
            header("Location: " . site_url("install"));
            exit();
        }
    }
    error_log("get_uri():" . get_uri());
    if (get_user()) return;
    if (uri_segment(-2) === 'auth') return;
    if (uri_segment(-2) === 'api') return;
    if (uri_segment(-1) === 'install') return; 
    if (strpos(get_uri(), '/fetch_static/') !== false) return;
    header("Location: " . site_url("auth/login"));
    exit();
};

Router::post('/api/fetch_comments', function() use ($dbh) {
    $comments = [];
    $sth = $dbh->prepare("select a.* from comments a, posts b where a.post_id=b.id and b.uri=:uri");
    $sth->execute(['uri'=>$_POST['uri']]);
    foreach($sth->fetchAll() as $row) {
        $row['email_hash'] = md5($row['email']);
        $comments[] = $row;
    }
    echo json_encode($comments);
});

Router::post("/action/delete_comment", function() use ($dbh) {
    //error_log("POST[id]: " . print_r($_POST['id'],true));
    //error_log("GETTYPE:" . print_r(gettype($_POST['id']), true));
    //explode(',', $_POST['id'][0])
    foreach($_POST['id'] as $tid) {
        //error_log("DELETING: $tid...");
        $sth = $dbh->prepare("delete from comments where id=:id");
        $sth->execute(array(':id'=>$tid));
        //$sth->fetch();
        //error_log("error: " . print_r($dbh->errorInfo(), true));
    }
    echo "Record(s) Deleted.";
});

Router::post("/api/comment", function() use ($dbh) {
    $action = uri_segment(-1);
    $uri = $_POST["cm_uri"];
    $parts = parse_url($uri); //validate first
    $host = $parts['host'];
    $host .= ($parts['port']==80 ? "" : ":".$parts['port']);
    $host = $parts['scheme'].'://'.$host.'/';
    $sth = $dbh->prepare("select id from users where website=:host");
    $sth->execute(["host"=> $host]);
    $user = $sth->fetch();
    if (!$user) die("Unrecognized host: ". $host);
    
    $sth = $dbh->prepare("select id from posts where user_id=:user_id and uri=:uri");
    $sth->execute(["user_id"=>$user['id'],
        "uri"=>$parts['path'],
    ]);
    $row = $sth->fetch();
    if ($row) {
        $post_id = $row['id'];
        //echo "Post found.\n";
    }
    else {
        $post_id = create_post($parts['path'], $user);
        //echo "Created post $post_id.\n";
    }
    $data= [];
    $data['post_id'] = $post_id;
    $data['name'] = $_POST["cm_name"];
    $data['email'] = $_POST["cm_email"];
    $data['message'] = $_POST["cm_message"];
    $data['ip'] = $_SERVER["REMOTE_ADDR"];
    $data['website'] = $_POST["cm_website"];
    [$sql, $values] = build_insert_query("comments", $data);
    $sth= $dbh->prepare($sql);
    $sth->execute($values);
    //sleep(2);
    echo "Comment added!";
});

Router::get("/fetch_static/*", function(){
    //$fname = uri_segment(-1);
    $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    $fname =  substr($uri, strpos($uri, "fetch_static/") + 13);
    echo file_get_contents("static/$fname");
});

Router::get("/install", function() use ($dbh) {
    $vars = default_vars("Install");
    try {
        //echo 'now querying..';
        $dbh->query("select id from users limit 1;");
        $vars['messages'][] = "Application is already installed..";
        $vars['installed'] = true;
    } catch (PDOException $e) {
        $vars['installed'] = false;
    }
    
    load_template("templates/install.php", $vars);
});

Router::post("/install", function() {
    global $dbh;
    $vars = default_vars("Install");
    $log = '';
    try {
        $dbh->query("select id from users limit 1;");
        $log .= "Application already installed..";
    } catch (PDOException $e) {
        $sql = clean_sql( file_get_contents("init.sql") );
        //file_put_contents("init-clean.sql", $sql);
        $parts = explode(";", $sql);
        for($i=0; $i<count($parts); $i++) {
            $ss = $parts[$i];
            if ($ss === "") continue;
            $log.= "$ss;\n\n";
            try{
                $dbh->query($ss);
            } catch (PDOException $e) { $log.=$e->getMessage() . "\n\n"; }
        }
        $log .= "creating user:\n\n";
        if ( substr($_POST['website'], -1) !== '/' )
            $_POST['website'] .= '/';
        
        $data=  array(
            "email"=>$_POST['email'],
            'pwd_hash' => password_hash($_POST['password'], PASSWORD_BCRYPT ),
            "name"=>$_POST['name'],
            "website"=>$_POST['website'],
        );
        $q = build_insert_query("users", $data);
        [$sql, $values] = $q;
        $sth = $dbh->prepare($sql);
        $sth->execute($values);
        $log .= "$sql\n";
        //$log .= $sth::rowCount . " rows affected.\n";
        //$log .= print_r($values, true) . "\n";
        $log .= "lastInsertId: ".$dbh->lastInsertId()."\n";
        $log .= "Install complete.\n";      
    }
    $vars['install_log'] = $log;
    $vars['installed'] = true;
    load_template("templates/install.php", $vars);
});
// home
Router::get("/", function(){
    global $dbh;
    $vars = default_vars("Home");
    $sth = $dbh->prepare("select a.id, b.uri, name, message, email, website, ip, approved from comments a, posts b where a.post_id=b.id and b.user_id=:user_id order by a.created_at desc;");
    $sth->execute(['user_id' => get_user()['id']]);
    $vars['comments'] = $sth->fetchAll();
    load_template('templates/home.php', $vars);
});
Router::get("/pref", function(){
    $vars = default_vars("Preferences");
    //$sth = $dbh->prepare("select * ");
    //$row = $sth->fetch();
    $vars['item'] = $_SESSION["user"]; //$row;
    load_template("templates/pref.php", $vars);
});
Router::post("/pref", function() use ($dbh) {
    $vars = default_vars("Preferences");
    //error_log("WEBSITE is ". $_POST['website'] );
    //error_log("last char is ".substr($_POST['website'], -1));
    if ( substr($_POST['website'], -1) !== '/' )
        $_POST['website'] .= '/';
    $_SESSION['user']['name'] = $_POST['name'];
    $_SESSION['user']['website'] = $_POST['website'];
    $data = ['name'=>$_POST['name'],
        'website'=>$_POST['website'],
    ];
    [$sql, $vals] = build_update_query("users", $data, $_SESSION['user']['id']);
    $sth = $dbh->prepare($sql);
    $sth->execute($vals);
    $vars['item'] = $_SESSION["user"]; //$row;
    $vars['messages'][] = "Record updated.";
    load_template("templates/pref.php", $vars);
});

Router::get("/about", function() use($dbh) {
    $vars = default_vars("About");
    $vars['sqlite_ver'] = $dbh->query('select sqlite_version()')->fetch()[0];
    load_template("templates/about.php", $vars);
});


// auth
Router::get("/auth/logout", function(){
    unset($_SESSION['user']);
    header("Location: ". base_url());
});
Router::get("/auth/login", function(){
    $vars = default_vars("Login");
    load_template("templates/login.php", $vars);
});
Router::post("/auth/login", function(){
    global $dbh;
    $vars = default_vars("Login");
    $sth= $dbh->prepare("select * from users where email=:email");
    $sth->execute([$_POST['email']]);
    $rows = $sth->fetchAll();
    if (count($rows) > 0) {
        $pwdh = $rows[0]['pwd_hash'];
        if (password_verify($_POST["password"], $pwdh)) {
            $_SESSION['user'] = $rows[0];
            //@todo: 
            exit( header("Location: ". base_url()) );
        }
        $vars['errors'][] = "Incorrect email or password.";
    } else {
        $vars['errors'][] = "Incorrect email or password.";
    }
    load_template("templates/login.php", $vars);
});


Router::dispatch();

Below is the source code for cm-client.js, the JavaScript for initializing the front-end routing that embeds existing comments for that URI and adds a form to submit any new comments for the reader.

/**
* cm-client.js
*/

// Make sure to place cm-front.js in the base_url
// Make sure to start with "defer" script

var t_url = document.currentScript.src;
var base_url = t_url.substring(0, t_url.lastIndexOf("/static/")) + "/";
var site_url = base_url + "index.php/";
var pause = false; // timer

function dom(sel, elem) { //@todo: move to util
    var items;
    if (!elem) {
        items = document.querySelectorAll(sel);
    } else {
        items = elem.querySelectorAll(sel);
    }
    return items;
}

function fetchCommentBox(node, cb) {
    var html = "<div class='cm-comment-container'></div>";
    node.insertAdjacentHTML('beforeend', html);
    
    var turl = site_url + 'fetch_static/partials/comment-box.html?v=1';
    fetch(turl, {method:'get', action: turl})
    .then(resp => resp.text())
    .then(data => {
        node.insertAdjacentHTML('beforeend', data);
        cb();
    });
}

function refreshComments()  {
    if (pause) return;
    var data = new FormData();
    data.append("uri", location.pathname);
    fetch(site_url + "api/fetch_comments", {'method':'POST',
        'body': data
    })
    .then(resp => resp.text())
    .then(data => {
        data = JSON.parse(data);
        if (data.length == 0) return;
        //console.log("data-received:", data);
        for(var i=0; i<data.length; i++) {
            var cmt; var items;
            items = dom(".comment-box.data-id-" + data[i]['id']);
            if (items.length > 0) continue; //already exists
                
            cmt = dom(".comment-box-init")[0].cloneNode(true);
            cmt.classList.remove("comment-box-init");
            cmt.classList.add("data-id-" + data[i]['id']);
            if (data[i].website == '') {
                dom('.cm-author', cmt)[0].textContent = data[i].name;
            }
            else {
                var html = "<a href='"+data[i].website+"'>"+data[i].name+"</a>";
                dom('.cm-author', cmt)[0].innerHTML = html;
            }
            dom('.cm-message', cmt)[0].textContent = data[i].message;
            //+ data[i].email_hash;
            var d = new Date(data[i].created_at);
            var datestr = d.getFullYear() + "-" + (d.getMonth()+1) + "-" + d.getDate() + " " + d.getHours() + ":" + d.getMinutes()  + " (GMT)";
            dom('.cm-created_at', cmt)[0].textContent = datestr;
            //items = dom(".comment-box")
            dom(".cm-comment-container")[0].appendChild(cmt);
            console.log("added cmt to root node:", cmt);
        }
    })
    ;
}


function import_style() {
    var file = location.pathname.split( "/" ).pop();
    var link = document.createElement( "link" );
    link.href = base_url + "static/css/cm-client.css";
    link.type = "text/css";
    link.rel = "stylesheet";
    link.media = "screen,print";
    document.getElementsByTagName( "head" )[0].appendChild( link ); 
}

function init(node) {
    //add css styling
    import_style(base_url);
    
    refreshComments();
    
    // create a box to submit a comment.
    //var turl = base_url + "static/partials/submit-comment.html";
    var turl = site_url + 'fetch_static/partials/submit-comment.html';
    console.log("turl:", turl);
    fetch(turl, {'method':'GET'}) //'mode': 'no-cors'
    .then(response => response.text())
    .then(data => {
        node.insertAdjacentHTML('beforeend', data);
        var box = dom(".cm-submit-comment")[0];
        dom(".cm-comment-form",box)[0].setAttribute("action", site_url + "api/comment");
        dom("#cm_uri")[0].value = location.href;
        dom(".submit-button",box)[0].addEventListener('click', function() {
            console.log("fired:.submit-button.click()");
            var valid = dom(".cm-comment-form",box)[0].reportValidity();
            if (!valid) return;
            dom(".submit-button", box)[0].setAttribute("disabled", true);
            var formNode = this.parentNode;
            console.log("FORM being sent: ", formNode);
            console.log("ACTION: ", formNode.getAttribute('action'));
            console.log("DATA being sent: ", new FormData(formNode));
            fetch(formNode.getAttribute('action'), {
              method: formNode.getAttribute('method'),
              body: new FormData(formNode)
            }).then(res=>res.text())
              .then(function (data) {
                    //console.log("data received:" + data);
                    dom(".cm-alert",box)[0].textContent = data;
                    refreshComments();
                    dom(".submit-button",box)[0].removeAttribute("disabled");
                    dom(".cm-submit-comment input,.cm-submit-comment textarea", box).forEach((item) => 
                    { 
                    if (item.type !== 'hidden')
                        item.value = "";
                    });
                    setTimeout(()=>{
                        console.log("now calling setTimeout:");
                        dom(".cm-alert", box)[0].textContent = "";
                    }, 10000);
              });
        });
    });
}

var node = document.currentScript.parentNode;

fetchCommentBox(node, function() {
    init(node); 
});

```
\$\endgroup\$
3
  • \$\begingroup\$ If you are concerned about the size of your code, bear in mind that the character limit on Code Review was increased to 65k. If you want more than the code of index.php reviewed then please expand the code here. Please embed the code to be reviewed directly in the question. \$\endgroup\$ Commented Jun 7 at 21:49
  • \$\begingroup\$ Since it seems you're only posting selected portions, consider including something from each element of the MVC. So far it looks like you've only posted controller code. Consider including model and view code as well, especially since you tagged as such. \$\endgroup\$
    – tdy
    Commented Jun 12 at 18:44
  • \$\begingroup\$ I deleted my previous comment because you edited the question, but I still want to point out that your choice of names isn't very good. You use "comment", "cmt" and "cm" to mean the same thing. Things that aren't the same get the same name: data = JSON.parse(data);. You have a t_url and a turl, what does it contain, and what's the difference? The list is endless. I can't teach you to name things properly, but it is important if you want to interest other people in your code. How about a video? \$\endgroup\$ Commented Jun 13 at 9:18

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.