Choose a version here. If you've not already started the tutorial, just go with the latest one.

30 Aug 2014 Preview version Choose
5 Oct 2014 Explicitly adds a specific charset for htmlspecialchars(), and wraps it with a custom function Choose
16 Oct 2014 Updated redirect function to work with vhost subfolders Choose
4 Nov 2014 Minor improvements: added missing docblock, fixed security issue, CSS tweak Choose
25 Nov 2014 Improve the notes on getting started, in particular choosing a programmer's editor. Added introduction to mod_rewrite rules. Choose
OK
NB: There are several versions of this tutorial, each successive one containing additional improvements. If you're in the middle of working through it, please check the versions panel above, to ensure you're not mixing code from different versions.

Make your own blog

Comment admin

In this section, we'll allow authorised users to delete comments from posts. To kick us off, here's a nice item of refactoring — the comments list within article pages is quite large, and it would be more modular to store it in a separate file.

Expand/contract code area Select previous tab
Select next tab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* @var $pdo PDO
* @var $postId integer
*/
?>
<div class="comment-list">
<h3><?php echo countCommentsForPost($pdo, $postId) ?> comments</h3>
<?php foreach (getCommentsForPost($pdo, $postId) as $comment): ?>
<div class="comment">
<div class="comment-meta">
Comment from
<?php echo htmlspecialchars($comment['name']) ?>
on
<?php echo convertSqlDate($comment['created_at']) ?>
</div>
<div class="comment-body">
<?php // This is already escaped ?>
<?php echo convertNewlinesToParagraphs($comment['text']) ?>
</div>
</div>
<?php endforeach ?>
</div>
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php echo convertNewlinesToParagraphs($row['body']) ?>
</div>
<div class="comment-list">
<h3><?php echo countCommentsForPost($pdo, $postId) ?> comments</h3>
<?php foreach (getCommentsForPost($pdo, $postId) as $comment): ?>
<div class="comment">
<div class="comment-meta">
Comment from
<?php echo htmlspecialchars($comment['name']) ?>
on
<?php echo convertSqlDate($comment['created_at']) ?>
</div>
<div class="comment-body">
<?php // This is already escaped ?>
<?php echo convertNewlinesToParagraphs($comment['text']) ?>
</div>
</div>
<?php endforeach ?>
</div>
<?php require 'templates/comment-form.php' ?>
</body>
79
80
81
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
84
85
<?php echo convertNewlinesToParagraphs($row['body']) ?>
</div>
<?php require 'templates/list-comments.php' ?>
<?php require 'templates/comment-form.php' ?>
</body>

Let us now add a delete button next to comments on blog posts, for authorised users only. I've added a bit of extra CSS to get it to float to the right-hand side.

Expand/contract code area Select previous tab
Select next tab
69
70
71
 
 
 
 
72
73
74
80
81
82
 
83
84
85
padding-top: 8px;
}
.comment-body p {
margin: 8px 4px;
}
.comment-list {
border-bottom: 1px dotted silver;
margin-bottom: 12px;
}
.comment-margin {
69
70
71
72
73
74
75
76
77
78
80
81
82
83
84
85
86
padding-top: 8px;
}
.comment .comment-meta input {
float: right;
}
.comment-body p {
margin: 8px 4px;
}
.comment-list {
border-bottom: 1px dotted silver;
margin-bottom: 12px;
max-width: 900px;
}
.comment-margin {
14
15
16
 
 
 
 
 
 
 
17
18
19
<?php echo htmlspecialchars($comment['name']) ?>
on
<?php echo convertSqlDate($comment['created_at']) ?>
</div>
<div class="comment-body">
<?php // This is already escaped ?>
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php echo htmlspecialchars($comment['name']) ?>
on
<?php echo convertSqlDate($comment['created_at']) ?>
<?php if (isLoggedIn()): ?>
<input
type="submit"
name="delete-comment[<?php echo $comment['id'] ?>]"
value="Delete"
/>
<?php endif ?>
</div>
<div class="comment-body">
<?php // This is already escaped ?>

Since we want to do more than one thing with comments (add and delete), we need to make a change to explain what we are doing within the form submission. To do this, I've used the query string, which sends these items of information:

Expand/contract code area Select previous tab
Select next tab
18
19
20
21
 
 
 
 
22
23
24
<h3>Add your comment</h3>
<form method="post" class="comment-form user-form">
<div>
<label for="comment-name">
Name:
18
19
20
21
22
23
24
25
26
27
28
<h3>Add your comment</h3>
<form
action="view-post.php?action=add-comment&amp;post_id=<?php echo $postId?>"
method="post"
class="comment-form user-form"
>
<div>
<label for="comment-name">
Name:
4
5
6
7
 
 
 
 
8
9
10
32
33
34
35
* @var $postId integer
*/
?>
<div class="comment-list">
<h3><?php echo countCommentsForPost($pdo, $postId) ?> comments</h3>
<?php foreach (getCommentsForPost($pdo, $postId) as $comment): ?>
</div>
</div>
<?php endforeach ?>
</div>
4
5
6
7
8
9
10
11
12
13
14
32
33
34
35
* @var $postId integer
*/
?>
<form
action="view-post.php?action=delete-comment&amp;post_id=<?php echo $postId?>&amp;"
method="post"
class="comment-list"
>
<h3><?php echo countCommentsForPost($pdo, $postId) ?> comments</h3>
<?php foreach (getCommentsForPost($pdo, $postId) as $comment): ?>
</div>
</div>
<?php endforeach ?>
</form>

Let us now make some room in our view post page, by moving some logic to a new function.

Expand/contract code area Select previous tab
Select next tab
1
2
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
5
6
<?php
/**
* Retrieves a single post
*
* @param PDO $pdo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
/**
* Called to handle the comment form, redirects upon success
*
* @param PDO $pdo
* @param integer $postId
* @param array $commentData
*/
function handleAddComment(PDO $pdo, $postId, array $commentData)
{
$errors = addCommentToPost(
$pdo,
$postId,
$commentData
);
// If there are no errors, redirect back to self and redisplay
if (!$errors)
{
redirectAndExit('view-post.php?post_id=' . $postId);
}
return $errors;
}
/**
* Retrieves a single post
*
* @param PDO $pdo
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
71
72
73
 
74
75
76
'website' => $_POST['comment-website'],
'text' => $_POST['comment-text'],
);
$errors = addCommentToPost(
$pdo,
$postId,
$commentData
);
// If there are no errors, redirect back to self and redisplay
if (!$errors)
{
redirectAndExit('view-post.php?post_id=' . $postId);
}
}
else
{
<?php require 'templates/list-comments.php' ?>
<?php require 'templates/comment-form.php' ?>
</body>
</html>
33
34
35
36
 
 
 
 
 
 
 
 
 
 
37
38
39
71
72
73
74
75
76
77
'website' => $_POST['comment-website'],
'text' => $_POST['comment-text'],
);
$errors = handleAddComment($pdo, $postId, $commentData);
}
else
{
<?php require 'templates/list-comments.php' ?>
<?php // We use $commentData in this HTML fragment ?>
<?php require 'templates/comment-form.php' ?>
</body>
</html>

We now need to read the action key we set up, so we can decide what feature to call. If the user is deleting a comment, the new function deleteComment in the next set of changes is called; although we could just delete by comment.id, to be sure of deleting the right thing we filter additionally by comment.post_id.

Note that we also add an if() clause to ensure that the delete operation is skipped if the user is not logged in. Although we do not render delete buttons for anonymous users, it is still possible for security crackers to fake a form submission that contains a valid deletion request — this clause prevents that.

Expand/contract code area Select previous tab
Select next tab
25
26
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
29
30
}
/**
* Retrieves a single post
*
* @param PDO $pdo
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
}
/**
* Delete the specified comment on the specified post
*
* @param PDO $pdo
* @param integer $postId
* @param integer $commentId
* @return boolean True if the command executed without errors
* @throws Exception
*/
function deleteComment(PDO $pdo, $postId, $commentId)
{
// The comment id on its own would suffice, but post_id is a nice extra safety check
$sql = "
DELETE FROM
comment
WHERE
post_id = :post_id
AND id = :comment_id
";
$stmt = $pdo->prepare($sql);
if ($stmt === false)
{
throw new Exception('There was a problem preparing this query');
}
$result = $stmt->execute(
array(
'post_id' => $postId,
'comment_id' => $commentId,
)
);
return $result !== false;
}
/**
* Retrieves a single post
*
* @param PDO $pdo
28
29
30
31
32
33
34
35
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
38
39
$errors = null;
if ($_POST)
{
$commentData = array(
'name' => $_POST['comment-name'],
'website' => $_POST['comment-website'],
'text' => $_POST['comment-text'],
);
$errors = handleAddComment($pdo, $postId, $commentData);
}
else
{
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$errors = null;
if ($_POST)
{
switch ($_GET['action'])
{
case 'add-comment':
$commentData = array(
'name' => $_POST['comment-name'],
'website' => $_POST['comment-website'],
'text' => $_POST['comment-text'],
);
$errors = handleAddComment($pdo, $postId, $commentData);
break;
case 'delete-comment':
// Don't do anything if the user is not authorised
if (isLoggedIn())
{
$deleteResponse = $_POST['delete-comment'];
$keys = array_keys($deleteResponse);
$deleteCommentId = $keys[0];
deleteComment($pdo, $postId, $deleteCommentId);
redirectAndExit('view-post.php?post_id=' . $postId);
}
break;
}
}
else
{

Finally, I noticed that the logic to add a comment is contained within a function, but the logic to delete one is still in the switch() block in view-post.php. The following change just tidies that up:

Expand/contract code area Select previous tab
Select next tab
25
26
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
29
30
}
/**
* Delete the specified comment on the specified post
*
* @param PDO $pdo
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
}
/**
* Called to handle the delete comment form, redirects afterwards
*
* The $deleteResponse array is expected to be in the form:
*
* Array ( [6] => Delete )
*
* which comes directly from input elements of this form:
*
* name="delete-comment[6]"
*
* @param PDO $pdo
* @param integer $postId
* @param array $deleteResponse
*/
function handleDeleteComment(PDO $pdo, $postId, array $deleteResponse)
{
if (isLoggedIn())
{
$keys = array_keys($deleteResponse);
$deleteCommentId = $keys[0];
if ($deleteCommentId)
{
deleteComment($pdo, $postId, $deleteCommentId);
}
redirectAndExit('view-post.php?post_id=' . $postId);
}
}
/**
* Delete the specified comment on the specified post
*
* @param PDO $pdo
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$errors = handleAddComment($pdo, $postId, $commentData);
break;
case 'delete-comment':
// Don't do anything if the user is not authorised
if (isLoggedIn())
{
$deleteResponse = $_POST['delete-comment'];
$keys = array_keys($deleteResponse);
$deleteCommentId = $keys[0];
deleteComment($pdo, $postId, $deleteCommentId);
redirectAndExit('view-post.php?post_id=' . $postId);
}
break;
}
}
39
40
41
42
43
 
 
 
 
 
 
 
44
45
46
$errors = handleAddComment($pdo, $postId, $commentData);
break;
case 'delete-comment':
$deleteResponse = $_POST['delete-comment'];
handleDeleteComment($pdo, $postId, $deleteResponse);
break;
}
}

As ever, test everything to check it works, before we proceed to the final chapter!