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);
});
```
data = JSON.parse(data);
. You have at_url
and aturl
, 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\$