content/blog/2021-04-23-php-comment-system.org

e8577d98faff1bed72ba6f2d204b8b44c6827c23
cleberg.net/content/blog/2021-04-23-php-comment-system.org rendered · source · history · blame · raw

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.