content/blog/2021-04-23-php-comment-system.org
273 lines · 10392 bytes
1#+date: <2021-04-23 Fri 00:00:00>
2#+title: Development of a PHP-Based Static Commenting System
3#+description: Technical walkthrough for constructing a lightweight, privacy-oriented static comment system using PHP, with no client-side scripting or external dependencies.
4#+slug: php-comment-system
5#+filetags: :php:comments:static:
6
7* The Terrible-ness of Commenting Systems
8
9The current state of affairs regarding interactive comment systems is,
10well, terrible. It is especially awful if you're a privacy conscious
11person who does not generally load third-party scripts or frames on the
12websites you visit.
13
14Even further, many comment systems are charging exorbitant fees for
15something that should be standard.
16
17Of course, there are some really terrible options:
18
19- Facebook Comments
20- Discourse
21
22There are some options that are better but still use too many scripts,
23frames, or social integrations on your web page that could impact some
24users:
25
26- Disqus
27- Isso
28- Remark42
29
30Lastly, I looked into a few unique ways of generating blog comments,
31such as using Twitter threads or GitHub issues to automatically post
32issues. However, these both rely on external third-party sites that I
33don't currently use.
34
35* Stay Static with Server-Side Comments
36
37The main issue for my personal use-case is that my blog is completely,
38100% static. I use PHP on the back-end but website visitors only see
39HTML and a single CSS file. No external javascript and no embedded
40frames.
41
42So, how do we keep a site static and still allow users to interact with
43blog posts? The key actually pretty simple - I'm already using PHP, so
44why not rely on the classic HTML =<form>= and a PHP script to save the
45comments somewhere? As it turns out, this was a perfect solution for me.
46
47The second issue for my personal use-case is that I am trying to keep
48the contents of my website accessible over time, as described by
49[cite/t:@brandur], in his post entitled
50[[https://brandur.org/fragments/graceful-degradation-time][Blog with
51Markdown + Git, and degrade gracefully through time]] .
52
53This means I cannot rely on a database for comments, since I do not rely
54on a database for any other part of my websites.
55
56I blog in plain Markdown files, commit all articles to Git, and ensure
57that future readers will be able to see the source data long after I'm
58gone, or the website has gone offline. However, I still haven't
59committed any images served on my blog to Git, as I'm not entirely sold
60on Git LFS yet - for now, images can be found at
61[[https://img.cleberg.net][img.cleberg.net]].
62
63Saving my comments back to the Git repository ensures that another
64aspect of my site will degrade gracefully.
65
66* Create a Comment Form
67
68Okay, let's get started. The first step is to create an HTML form that
69users can see and utilize to submit comments. This is fairly easy and
70can be changed depending on your personal preferences.
71
72Take a look at the code block below for the form I currently use. Note
73that =<current-url>= is replaced automatically in PHP with the current
74post's URL, so that my PHP script used later will know which blog post
75the comment is related to.
76
77The form contains the following structure:
78
791. =<form>= - This is the form and will determine which PHP script to
80 send the comment to.
812. =<section hidden>= - This section is hidden from the user and is used
82 to ensure that we know which blog post sent the comment.
833. =<section>= Display Name (Optional) - Used to accept a display name,
84 if entered.
854. =<section>= Comment (Required) - Used to accept the user's full
86 comment. Markdown is allowed.
875. =<button>= - A button to submit the form.
88
89#+begin_src html
90<form action="/comment.php" method="POST">
91 <h3>Leave a Comment</h3>
92 <section hidden>
93 <label class="form-label" for="postURL">Post URL</label>
94 <input
95 class="form-control"
96 id="postURL"
97 name="postURL"
98 type="text"
99 value="<current-url>"
100 />
101 </section>
102 <section>
103 <label class="form-label" for="userName">Display Name</label>
104 <input
105 class="form-control"
106 id="userName"
107 name="userName"
108 placeholder="John Doe"
109 type="text"
110 />
111 </section>
112 <section>
113 <label class="form-label" for="userContent">Your Comment</label>
114 <textarea
115 class="form-control"
116 id="userContent"
117 name="userContent"
118 rows="3"
119 placeholder="# Feel free to use Markdown"
120 aria-describedby="commentHelp"
121 required
122 ></textarea>
123 <div id="commentHelp" class="form-text">
124 Comments are saved as Markdown and cannot be edited or deleted.
125 </div>
126 </section>
127 <button type="submit">Submit</button>
128</form>
129#+end_src
130
131* Handle Comments via POST
132
133Now that we have a form and can submit comments, we need to create a PHP
134script so that the server can fetch the comment data and save it. Make
135sure your PHP script name matches the name you entered in the =action=
136field in your form.
137
138See the code block below for the contents of my =comment.php= script. We
139perform the following tasks in this script:
140
1411. Grab the POST data from the HTML form.
1422. Sanitize the comment data with =htmlentities=.
1433. Set the display name to =Anonymous= if it was left blank.
1444. Create a PHP object that combines all of this data.
1455. Check if our destination file =comments.json= exists.
1466. If so, convert the PHP object to JSON and save it to the file.
1477. If the =comments.json= file does not exist, the script will exit with
148 an error. You can alter this to ensure it creates the script, but my
149 source code includes the file by default, so it will always exist.
1508. Finally, send the user back to the =#comments= section of the blog
151 post they just read.
152
153#+begin_src php
154// Get the content sent from the comment form
155$comment = htmlentities($_POST['userContent']);
156$post_url = $_POST['postURL'];
157
158// Set default values if blank
159if (isset($_POST['userName']) && trim($_POST['userName']) !== "") {
160 $username = $_POST['userName'];
161} else {
162 $username = 'Anonymous';
163}
164
165// Create an empty PHP object
166$user_object = new stdClass();
167
168// Add object content
169$user_object->timestamp = date('Y-m-d H:i:s');
170$user_object->username = $username;
171$user_object->comment = $comment;
172$user_object->post_url = $post_url;
173
174// Append JSON to file
175$file_name = 'comments.json';
176if (file_exists($file_name)) {
177 $source_data = file_get_contents($file_name);
178 $temp_array = json_decode($source_data);
179 array_push($temp_array, $user_object);
180 $json_data = json_encode($temp_array, JSON_PRETTY_PRINT);
181 file_put_contents($file_name, $json_data);
182} else {
183 die('Error: The "comments.json" file does not exist.');
184}
185
186// Send the user back
187header('Location: ' . $post_url . '#comments');
188#+end_src
189
190If you're using Apache, make sure the =www-data= user on your server has
191the correct permissions to your website directory or else it will not be
192able to write to =comments.json=.
193
194#+begin_src sh
195chgrp -R www-data /path/to/website/
196chmod -R g+w comments.json
197#+end_src
198
199* Display User Comments
200
201Now that we can submit comments, and they are saved to the
202=comments.json= file, let's make sure we can show those comments in each
203blog post.
204
205The code block below shows the function I use to decode my
206=comments.json= file, check if the comments apply to the current post,
207and then display them.
208
209This piece of code should *really* be inside a function (or at least in
210an organized PHP workflow). Don't just copy-and-paste and expect it to
211work. You need to at least supply a =$query= variable depending on the
212page visited.
213
214#+begin_src php
215$query = 'your-blog-post.html';
216
217// Load saved comments
218$comments_file = 'comments.json';
219$comments_raw = file_get_contents($comments_file);
220$comments = json_decode($comments_raw);
221$comment_section = '<section id="comments" class="comments"><h3>Comments</h3>';
222foreach ($comments as $comment) {
223 if ($comment->post_url == "https://example.com/post/" . $query) {
224 // Assign metadata to variables
225 $comment_timestamp = $comment->timestamp;
226 $comment_username = $comment->username;
227 $comment_content = $comment->comment;
228
229 // Parse the comment, in case it contains Markdown
230 $comment_md = Parsedown::instance()->text($comment_content);
231 $comment_html = new DOMDocument();
232 $comment_html->loadHTML($comment_md);
233 $comment_html_links = $comment_html->getElementsByTagName('a');
234 foreach ($comment_html_links as $comment_html_link) {
235 $comment_html_link->setAttribute('rel', 'noreferrer');
236 $comment_html_link->setAttribute('target', '_blank');
237 }
238 $comment_secured_html = $comment_html->saveHTML();
239
240 // Apply metadata to comments section
241 $comment_section .= '<div class="user-comment"><div class="row"><label>Timestamp:</label><p>' . $comment_timestamp . '</p></div><div class="row"><label>Name:</label><p>' . $comment_username . '</p></div><div class="row markdown"><label>Comment:</label><div class="comment-markdown">' . $comment_secured_html . '</div></div></div>';
242 }
243}
244
245echo $comment_section;
246#+end_src
247
248* Bonus: Create a 'Recent Comments' Page
249
250Finally, the last part of my current system is to create a Recent
251Comments page so that I can easily check-in on my blog and moderate any
252spam. As an alternative, you could use PHP's =mail()= function to email
253you for each blog comment.
254
255The code to do this is literally the same code as the previous section,
256I just make sure it is printed when someone visits
257=https://example.com/comments/=.
258
259* Possible Enhancements
260
261This comment system is by no means a fully-developed system. I have
262noted a few possible enhancements here that I may implement in the
263future:
264
265- Create a secure moderator page with user authentication at
266 =https://blog.example.com/mod/=. This page could have the option to
267 edit or delete any comment found in =comments.json=.
268- Create a temporary file, such as =pending_comments.json=, that will
269 store newly-submitted comments and won't display on blog posts until
270 approved by a moderator.
271- Create a =/modlog/= page with a chronological log, showing which
272 moderator approved which comments and why certain comments were
273 rejected.