Swagg::Blogg

mojolicious

So there's these <meta> elements by Meta. Well not the <meta> element itself... But Open Graph. I wanted my links to look like this when I spam them in Discord:

A link preview of an article from web3isgoinggreat.com

Remember that scene from American Psycho where Christian Bale is sweating over a business card?

So anyways, I look at the HTML and see these <meta> tags. That's gotta be it, I mean I see these then some tags that say something about Twitter and I don't want Twitter since I'm really actually not a terrible person once you get to know me so I guess I'll implement these tags. The image seems easy enough, throw it in a Mojo template but what's annoying is I have my <head> entirely in the layout rather than the template itself. I first considered a partial template like I've seen in Rails before but Mojo couldn't find the partial. I think this was due to me just using a single layout for all my templates associated with different controllers. My site is pretty simple so honestly I didn't want to copy the same layout for every controller.

So I was left to hit the docs looking for some sort of feature or something that could help me out here. Finally I settled on the content_for() helper. In my layout I put the following:

<head>
  <!-- Some stuff here... -->
  <%= content 'open_graph' =%>
  <meta property="og:url" content="<%= url_for->to_abs %>">
  <meta property="og:site_name" content="Post::Text">
  <meta property="og:image"
        content="<%= url_for('/images/logo.png')->to_abs %>">
  <meta property="og:image:type" content="image/png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="1200">
  <meta property="og:image:alt"
        content="Post::Text logo; a small nerdy anime girl giving a V sign">
</head>

Now in my templates I simply do:

% layout 'default';
% title 'New Thread';
% content_for open_graph => begin
  <meta property="og:type" content="website">
  <meta property="og:title" content="<%= title %>">
  <meta property="og:description" content="Start a new thread.">
% end
<h2 class="page-title"><%= title %></h2>
<!-- Begin the rest of the page here... -->

Well that oughta do it! I mean I even made my image 1200 pixels wide, this oughta look breathtaking...

A URL preivew of posttext.pl with a smaller image preview

That's... A URL preview but why is my image so smol? It should be 4K ultra-HD like Molly White's site! I mean yeah there's these Twitter tags but why would it use those? There's not even a 'site name' and I clearly see them in both our previews so it must be using Open Graph. Unless... It's using both. But there's no way it'd do that I mean are the Twitter tags even considered a standard? Hell, is Open Graph even a standard? Out of desperation I try...

% content_for twitter_card => begin
  <meta name="twitter:title" content="<%= title %>">
  <meta name="twitter:description" content="Start a new thread.">
% end

And back to the layout:

<%= content 'twitter_card' =%>
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="@swaggboi@eattherich.club">
<meta name="twitter:site"
      content="@posttext@fedi.seriousbusiness.international">
<meta name="twitter:image"
      content="<%= url_for('/images/logo.png')->to_abs %>">
<meta name="twitter:image:alt"
      content="Post::Text logo; a small nerdy anime girl giving a V sign">

Well, what's it gunna be Discord? Make my day...

The same link to posttext.pl but now the image is large

😳

Alright then... It's got the site title from Open Graph... And the image from the stupid Twitter tags. I guess. Cool.

Well anyways that's done and Post::Text is finally live so I'm happy. On to the next harebrained project! 🫡

P.S. I know it's been a while since I've blogged, my last prediction is partially becoming true already 💀

#perl #mojolicious #textboard

I actually normally wouldn't enjoy this but thankfully this Mojo::Pg wrapper makes it easy. I honestly don't have any experience with the original DBD::Pg as I'm still very new to the database world so I hope this doesn't read too much like I'm lost in the sauce. First I took a look at my current tables using the psql tool:

post_text=> \d
                   List of relations
 Schema |         Name          |   Type   |   Owner
--------+-----------------------+----------+-----------
 public | mojo_migrations       | table    | post_text
 public | replies               | table    | post_text
 public | replies_reply_id_seq  | sequence | post_text
 public | threads               | table    | post_text
 public | threads_thread_id_seq | sequence | post_text
(5 rows)

post_text=> \d replies;
                                            Table "public.replies"
     Column     |           Type           | Collation | Nullable |                  Default
----------------+--------------------------+-----------+----------+-------------------------------------------
 reply_id       | integer                  |           | not null | nextval('replies_reply_id_seq'::regclass)
 thread_id      | integer                  |           |          |
 reply_date     | timestamp with time zone |           | not null | now()
 reply_author   | character varying(64)    |           |          |
 reply_body     | character varying(4096)  |           |          |
 hidden_status  | boolean                  |           | not null |
 flagged_status | boolean                  |           | not null |
Indexes:
    "replies_pkey" PRIMARY KEY, btree (reply_id)
Foreign-key constraints:
    "replies_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES threads(thread_id)

These long lines are ugly so here's a pastebin link. Essentially I need to s/reply_/remark_/g; including that sequence, index and foreign-key constraint:

-- This file is migrations/5/up.sql btw

 ALTER TABLE replies
RENAME TO remarks;

 ALTER TABLE remarks
RENAME reply_id
    TO remark_id;

 ALTER TABLE remarks
RENAME reply_date
    TO remark_date;

 ALTER TABLE remarks
RENAME reply_author
   TO remark_author;

 ALTER TABLE remarks
RENAME reply_body
    TO remark_body;

 ALTER TABLE remarks
RENAME CONSTRAINT replies_thread_id_fkey
    TO remarks_thread_id_fkey;

 ALTER INDEX replies_pkey
RENAME TO remarks_pkey;

 ALTER SEQUENCE replies_reply_id_seq
RENAME TO remarks_remark_id_seq;

I'm also going to do the exact opposite for our rolling-back pleasure:

-- This one is migrations/5/down.sql

 ALTER TABLE remarks
RENAME TO replies;

 ALTER TABLE replies
RENAME remark_id
    TO reply_id;

 ALTER TABLE replies
RENAME remark_date
    TO reply_date;

 ALTER TABLE replies
RENAME remark_author
    TO reply_author;

 ALTER TABLE replies
RENAME remark_body
    TO reply_body;

 ALTER TABLE replies
RENAME CONSTRAINT remarks_thread_id_fkey
    TO replies_thread_id_fkey;

 ALTER INDEX remarks_pkey
RENAME TO replies_pkey;

 ALTER SEQUENCE remarks_remark_id_seq
RENAME TO replies_reply_id_seq;

Idk why but I always find this step feels weird to me because it feels like I'm undoing the undo lol. But we will need it later so let's absolutely include it. Now I'm gunna see if it works...

daniel@netburst:~/git/PostText$ ./PostText.pl eval 'app->pg->migrations->from_dir("migrations")->migrate(5);'
daniel@netburst:~/git/PostText$ echo $?
0

No news is good news I guess. Let's whip out psql again and see how it looks:

post_text=> \d
                   List of relations
 Schema |         Name          |   Type   |   Owner
--------+-----------------------+----------+-----------
 public | mojo_migrations       | table    | post_text
 public | remarks               | table    | post_text
 public | remarks_remark_id_seq | sequence | post_text
 public | threads               | table    | post_text
 public | threads_thread_id_seq | sequence | post_text
(5 rows)

post_text=> \d remarks;
                                            Table "public.remarks"
     Column     |           Type           | Collation | Nullable |                  Default
----------------+--------------------------+-----------+----------+--------------------------------------------
 remark_id      | integer                  |           | not null | nextval('remarks_remark_id_seq'::regclass)
 thread_id      | integer                  |           |          |
 remark_date    | timestamp with time zone |           | not null | now()
 remark_author  | character varying(64)    |           |          |
 remark_body    | character varying(4096)  |           |          |
 hidden_status  | boolean                  |           | not null |
 flagged_status | boolean                  |           | not null |
Indexes:
    "remarks_pkey" PRIMARY KEY, btree (remark_id)
Foreign-key constraints:
    "remarks_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES threads(thread_id)

Aaaaaand the pastebin. I think we're in good shape. I'm going to migrate back for now as I still need to make some changes in the controller logic to use the new Remark model instead of my old Reply model.

daniel@netburst:~/git/PostText$ ./PostText.pl eval 'app->pg->migrations->from_dir("migrations")->migrate(4);'
daniel@netburst:~/git/PostText$ echo $?
0

I keep the migration hard-coded in the method call in the app itself just to save myself from accidentally migrating to the latest before it's ready:

# From PostText.pl
app->pg->migrations->from_dir('migrations')->migrate(4);

I know there's some reason I started doing that... I accidentally something but now I can't remember. Gunna just stick with the cargo cult and leave it be.

Next I gotta work on the aforementioned controller logic. And then moar tests.

#database #mojolicious #perl #sql #webdev

What should I do...

Is this dangerous?

So I ran into a funny little snafu last night. I finally implemented my Reply model for my little textboard project but once I did this I noticed the app no longer shows in the snazzy built-in error pages. That's bummer because I find them real helpful when running apps in development mode. I kinda gave up on this last night and looked at it again this morning and noticed the following in the console output (requesting a route that does not exist, expect a 404):

[2022-08-20 13:08:43.53717] [27345] [trace] [CgcDACZgwvD4] GET "/swagg"
Mojo::Reactor::Poll: I/O watcher failed: Can't locate object method "exception" via package "PostText::Model::Reply" at /home/daniel/perl5/lib/perl5/Mojolicious.pm line 200.

It must be trying to dereference exception from an object named reply... Let's see...

[daniel@netburst mojo]$ grep -i -r '\->reply' * | grep -i 'lite'
t/mojolicious/exception_lite_app.t:  $c->reply->exception(undef);
t/mojolicious/exception_lite_app.t:  $c->reply->exception;
t/mojolicious/exception_lite_app.t:  $c->reply->exception(Mojo::Exception->new);
t/mojolicious/lite_app.t:  $c->render_maybe('this_does_not_ever_exist') or $c->reply->static($file);
t/mojolicious/lite_app.t:get '/static' => sub { shift->reply->static('hello.txt') };
t/mojolicious/longpolling_lite_app.t:  Mojo::IOLoop->timer(0.25 => sub { $c->reply->static('hello.txt') });
t/mojolicious/static_lite_app.t:get '/hello3.txt' => sub { shift->reply->static('hello2.txt') };
t/mojolicious/static_lite_app.t:  $c->reply->static('hello2.txt');
t/mojolicious/static_lite_app.t:  $c->reply->asset($mem);
t/mojolicious/static_lite_app.t:  $c->reply->file(curfile->sibling('templates2', '42.html.ep'));

Gah! I think I can work around this I suppose by just renaming the reply helper to something other than reply but for consistency I'm going to go ahead and migrate everything to use the name Remark instead of Reply. 'Comment' would remind me too much of social media so I guess these threads are gettin remarked upon.

This, of course, means another night of databases. 😩

#perl #mojolicious #webdev #database

I used to use the flash() helper in Mojolicious for reporting errors back to the user but recently discovered I can just use Mojolicious::Validator::Validation in my templates which seems like the right tool for the job. I was already using it in my Mojolicious::Lite app like so:

$v->required('name' )->size(1, 63  );
$v->required('title')->size(1, 127 );
$v->required('post' )->size(2, 4000);

But other than changing the status to 400 I didn't realize I could use this to also report to the user that their request was invalid. The methods can be used in the templates themselves:

<form method="post">
  <div class="name field">
    <%= label_for name => 'Author' %>
    <%= text_field name =>'Anonymous', maxlength => 63, minlength => 1 %>
    <% if (my $error = validation->error('name')) { =%>
      <p class="field-with-error">Invalid name: 1 to 63 characters please.</p>
    <% } =%>
  </div>
  <div class="title field">
    <%= label_for title => 'Title' %>
    <%= text_field 'title', maxlength => 127, minlength => 1 %>
    <% if (my $error = validation->error('title')) { =%>
      <p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
    <% } =%>
  </div>
  <div class="text field">
    <%= label_for post => 'Text' %>
    <%= text_area 'post', (
        maxlength => 4000,
        minlength => 2,
        required  => 'true',
        rows      => 6
    ) %>
    <% if (my $error = validation->error('post')) { =%>
      <p class="field-with-error">Invalid post: Up to 4,000 characters only.</p>
    <% } =%>
  </div>
  <%= submit_button 'Post', class => 'post button' %>
</form>

So when the user makes their initial GET request, the following HTML is rendered within the form:

<div class="name field">
  <label for="name">Author</label>
  <input maxlength="63" minlength="1" name="name" type="text" value="Anonymous">
</div>
<div class="title field">
  <label for="title">Title</label>
  <input maxlength="127" minlength="1" name="title" type="text">
</div>
<div class="text field">
  <label for="post">Text</label>
  <textarea maxlength="4000" minlength="2" name="post" required="true" rows="6"></textarea>
</div>
<input class="post button" type="submit" value="Post">

Now let's say you screw up and submit a null value for the Title in your subsequent POST request, in your response the form is rendered again like this:

<div class="name field">
  <label for="name">Author</label>
  <input maxlength="63" minlength="1" name="name" type="text" value="anon">
</div>
<div class="title field">
  <label class="field-with-error" for="title">Title</label>
  <input class="field-with-error" maxlength="127" minlength="1" name="title" type="text">
    <p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
</div>
<div class="text field">
  <label for="post">Text</label>
  <textarea maxlength="4000" minlength="2" name="post" required="true" rows="6">hi</textarea>
</div>
<input class="post button" type="submit" value="Post">

The class attribute field-with-error was added to the invalid fields allowing me to decorate this to hint to the user. Then I added another little paragraph (with the same class) to make sure accessibility isn't an issue. These are definitely scenarios where I choose TagHelpers over vanilla HTML in my templates.

Also while testing all of this, I was resorting to using curl to submit invalid input since my browser won't let me thanks to attributes such as maxlength and minlength being present. Finally I realized I could do the same with Mojo's built-in commands which is awesome:

$ ./PostText.pl get -M POST -f 'name=anon' -f 'title=' -f 'post=hi' '/post'
[2022-08-15 19:28:16.84229] [76893] [trace] [jJBy5DsZGrMq] POST "/post"
[2022-08-15 19:28:16.84266] [76893] [trace] [jJBy5DsZGrMq] Routing to a callback
[2022-08-15 19:28:16.84279] [76893] [trace] [jJBy5DsZGrMq] Routing to a callback
[2022-08-15 19:28:16.84359] [76893] [trace] [jJBy5DsZGrMq] Rendering template "post.html.ep"
[2022-08-15 19:28:16.84564] [76893] [trace] [jJBy5DsZGrMq] Rendering template "layouts/main.html.ep"
[2022-08-15 19:28:16.84741] [76893] [trace] [jJBy5DsZGrMq] 400 Bad Request (0.005116s, 195.465/s)
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Post::Text - New Thread</title>
  <link href="/asset/942e7be1d2/PostText.css" rel="stylesheet">
</head>
<body>
<h1>Post::Text</h1>
<nav>
  <a href="/view">View</a>
  <a href="/post">New</a>
</nav>
<hr>
<h2>New Thread</h2>
<form method="post">
  <div class="name field">
    <label for="name">Author</label>
    <input maxlength="63" minlength="1" name="name" type="text" value="anon">
  </div>
  <div class="title field">
    <label class="field-with-error" for="title">Title</label>
    <input class="field-with-error" maxlength="127" minlength="1" name="title" type="text">
      <p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
  </div>
  <div class="text field">
    <label for="post">Text</label>
    <textarea maxlength="4000" minlength="2" name="post" required="true" rows="6">hi</textarea>
  </div>
  <input class="post button" type="submit" value="Post">
</form>
<footer>
  <p>In UTF-8 we trust.</p>
</footer>
</body>
</html>

I see two ways forward on this little project. I can maybe pause now and blow this up into a full-structure Mojolicious app or I can implement my next model (involving new-to-me SQL stuff like FOREIGN KEY and JOIN). Really either of these will be new-to-me and will take some time so I'll probably try to pick the lowest hanging fruit of the two... Once I figure out what the hell that is.

#perl #mojolicious

So I used to read data into an array of arrays like so:

sub get_threads($self) {
    $self->pg->db->query(<<~'END_SQL')->arrays()
        SELECT thread_id,
               TO_CHAR(thread_date, 'Dy Mon DD HH:MI:SS AM TZ YYYY'),
               thread_author,
               thread_title,
               thread_body
          FROM threads
         WHERE NOT hidden_status
         ORDER BY thread_date DESC;
       END_SQL
}

Then I'd plop that data into templates like so:

<% for my $thread (@$threads) { =%>
  <article class="thread">
    <h3 class="title"><%= @$thread[3] %></h3>
    <h4 class="date"><%= @$thread[1] %></h4>
    <h5 class="author"><%= @$thread[2] %></h5>
    <p class="body"><%= @$thread[0] %></p>
  </article>
<% } =%>

This is already pretty cool but I kept losing track of what index number went to which data field. Then I saw that there is a hashes() method in Mojo::Pg::Results and thought... What if we used 100% of the brain? (Edit: Formatting turned out bad for this one)

sub get_threads($self) {
    $self->pg->db->query(<<~'END_SQL')->hashes()
        SELECT thread_id                                             AS id,
               TO_CHAR(thread_date, 'Dy Mon DD HH:MI:SS AM TZ YYYY') AS date,
               thread_author                                         AS author,
               thread_title                                          AS title,
               thread_body                                           AS body
          FROM threads
         WHERE NOT hidden_status
         ORDER BY thread_date DESC;
       END_SQL
}

By using SQL to assign an alias to the column names my templates now look much cleaner:

<% for my $thread (@$threads) { =%>
  <article class="thread">
    <h3 class="title"><%= %$thread{'title'} %></h3>
    <h4 class="date"><%= %$thread{'date'} %></h4>
    <h5 class="author"><%= %$thread{'author'} %></h5>
    <p class="body"><%= %$thread{'body'} %></p>
  </article>
<% } =%>

Readability becomes more and more important as my memory gets worse and worse... I need to learn POD so if I'm a boi of my word there may be a blog post on that in the future. Knowing me... Years in the future.

#perl #mojolicious #sql

I will say if I had to choose either PHP or JS, I'd choose JS lol. But I do think the World Wide Web without Perl is worse off. I hope the O.G. Perl Mojolicious lives on in harmony.

#perl #javascript #mojolicious #insomnia

So I mentioned in my previous post that, with the powers granted to me by Mojolicious, I have migrated from the venerable Apache server with all my server-side fancy stuff being CGI to Docker containers. Awesome! This morning my buddy n4vn33t shared with me an interesting blog post; I'll let the post speak for itself but my takeaway was essentially, sure that upstream Docker image I'm using ought to be patched and ready to rock but... What if it's not? It would help to add RUN apt-get -y upgrade to my Dockerfile to make sure I've got the latest and greatest stuff. And so I did. And then I ran my container locally to give it a quick test and now my remaining CGI scripts are returning 404s. If I run the app locally with morbo www-swagg.pl it “just works” so I must've borked my Docker container!

To troubleshoot, I comment out the apt-get -y upgrade stuff and rebuild. That should yield the same result as prior to the change right? Well, no it didn't. At this point I'm lost so I begin removing all traces of previous images, re-pulling and re-building things. Still got 404s for my beloved guestbook which I think goes without saying, is completely unacceptable! Then I thought about this line in my Dockerfile:

RUN cpanm RJBS/Getopt-Long-Descriptive-0.105.tar.gz

Why run that instead of cpanm Getopt::Long::Descriptive? You see, this module isn't one that I'm using in my scripts but rather it's a dependency; a module I'm using is using this module. One day (prior to this) my Docker container refused to build and I narrowed the problem down to v0.106 of Getopt::Long::Descriptive building and, as it's not a module I'm much concerned with (I just need it so the module I do need will build) I enter the command as you see above to force installation of the prior version that I know will build just fine. So I start by checking out the changelogs for my modules that I'm using in my scripts. Thankfully it didn't take me long to see this in the changelog for Mojolicious (first module I checked 🙃):

9.11 2021-03-20 – This release contains fixes for security issues, everybody should upgrade! – Disabled format detection by default to fix vulnerabilities in many Mojolicious applications. That means some of your routes that previously matched “/foo” and “/foo.json”, will only match “/foo” after upgrading. From now on you will have to explicitly declare the formats your routes are allowed to handle. # /foo # /foo.html # /foo.json $r->get('/foo')–>to('bar#yada'); becomes $r->get('/foo' => [format => ['html', 'json']])–>to('bar#yada', format => undef); And if you are certain that your application is not vulnerable, you also have the option to re-enable format detection for a route and all its nested routes. Due to the high risk of vulnerabilities, this feature is going to be removed again in a future release however. my $active = $r->any([format => 1]); $active->get('/foo')–>to('Test#first'); $active->put('/bar')–>to('Test#second'); ...

Ahh that's right... I'm doing this:

plugin 'Config';

# CGI scripts
plugin CGI => ['/cgi-bin/guest'  => './cgi-bin/guest_mm.cgi'];
plugin CGI => ['/cgi-bin/whoami' => './cgi-bin/whoami.cgi'  ];

The .cgi file extension wasn't technically necessary but I want that to still work because:

  1. I already have hyperlinks all over the place that use the extension
  2. It's a CGI script and I want it to “look” like that... Petty I know

So here's my v9.11+ compliant way of using the extension:

plugin CGI => ['/cgi-bin/guest.cgi'  => './cgi-bin/guest_mm.cgi'];
plugin CGI => ['/cgi-bin/whoami.cgi' => './cgi-bin/whoami.cgi'  ];

Excellent, we're back in business! Fighting issues like this can sometimes feel like a “waste” of an afternoon because at the end of the day... My site hasn't really changed. I gained no new fun buttons or GIFs but I can sleep easy tonight knowing that my site is just a bit more “hardened” against script kiddies who never cease to make our lives just a little bit more complicated. Let's that tag this puppy, push it to my cloud provider and call it a day:

# After we've run: docker build -t www-swagg .
docker tag www-swagg gcr.io/www-swagg/www-swagg
docker push gcr.io/www-swagg/www-swagg
# Bunch of output follows...

And now we're safe. Until tomorrow when the next round of vulnerabilities gets discovered anyways 🤦‍♂️

Until next time bug,

Dan B

#docker #perl #mojolicious

I've been spending the past few weekends with Mojolicious, a Perl web framework. There's plenty of great web frameworks out there; several of them for Perl if you're a Perl programmer like me (e.g. Dancer2, Catalyst, etc). I plan on making my next few posts about Mojolicious as it really has made web programming fun for me, trying new things and learning new things (sometimes breaking new things). In the process I've been migrating my homepage from Apache/CGI to Mojolicious::Lite.

One thing my website has had is an awesome MIDI (technically an mp3 now) soundtrack, which is great but has merely presented itself as a little player in the upper left corner of the page via this HTML:

<!-- Soundtrack -->
<!-- <embed> doesn't work anymore SAD
<embed src="/misc/Smashmouth_-_All_Star.mid" width="100%" height="60">-->
<audio src="/Music/Smashmouth-All-Star.mp3" autoplay controls
       preload="auto"></audio>

Using <embed> with a MIDI file is NFG nowadays so I've replaced it with an <audio> tag. The controls attritube lets you manually play the song since no modern browser will honor the autoplay attribute. I pieced together via some Stackoverflow posts such as this one that generally a browser won't play media without the user clicking on something. I decided one way to do this is with a fake GDPR compliance banner. People click accept on these all the time without even thinking about it the same way we do with software EULAs. This of course makes them completely useless and society would be better off without them but since they're seemingly here to stay we can at least try to make something useful out of them. What I came up with is some Javascript to play it like so:

<!-- Soundtrack -->
<audio id="soundtrack" src="/Music/Smashmouth-All-Star.mp3" preload="auto">
</audio>
<!-- "GDPR" banner -->
<div id="gdpr">
  <b>Notice:</b> This site uses kickass MIDI technology instead of cookies.
  <img alt="a compact disc playing music" src="/Pictures/Music_CD.gif"
       style="vertical-align: bottom"><br>
  <br>
  <button class="win95button" onclick="playIt();"><u>O</u>K</button>
  <button class="win95button" onclick="closeIt();"><u>C</u>ancel</button>
</div>
<script>
  function closeIt() {
      document.getElementById("gdpr").style.display = "none";
  }

  function playIt() {
      document.getElementById("soundtrack").play();
      closeIt();
  }
</script>

The way this works is the user clicks on “OK” (they always do!), then document.getElementById("soundtrack").play(); reliably triggers the music playback. Mission accomplished but now I have a new grievance: The banner keeps coming back no matter what. The real GDPR banner wouldn't do this even with all its own aggrivations. This is happening because HTTP is a stateless protocol. The irony in my fake GDPR banner is that I'll need to leverage session cookies to maintain state; to tell my homepage, “This user has already heard the joke.” I started by just adding another line to the closeIt() function:

function closeIt() {
    document.getElementById("gdpr").style.display = "none";
    document.cookie = "banner=seen; max-age=600;";
}

This sets my cookie as a plain-text cookie plus I set a max-age so it doesn't linger forever (I'll explain why in a bit). This is fine but because Mojolicious has a more sophisticated way of handling the session I wanted to let Mojolicious set the cookie. That posed a new challenge: HTTP is also a request/response conversation, thus Mojo would want to set its session cookie via a Set-Cookie: response header. However by the time the user sees the banner, the request is completed. I thought of two ways to get around this: Create a new route, let's say /session, then have my accept button point to that. The /session route could then redirect the user back to the value of the Referer: request header (that's the actual spelling) with the Set-Cookie: header in the 301 or 302 response.

Or, since I already have my plain-text cookie in place, why not just look for the presence of this cookie in subsequent requests and if we find it, then include our session cookie in the response? My max-age of 10 min means the plain-text cookie will eventually “roll off” if you will but the fancy, cryptographically signed session cookie will live on with its lifetime of an hour:

# Handle the session
under sub {
    my ($c) = @_;

    if ($c->cookie('banner') eq 'seen') {
        $c->session->{banner} //= 'seen'
    }

    1;
};

I'm using under in my Perl code (the controller) to save myself from having to repeat this logic in every route. Here's the end result according to the response headers:

[daniel@threadlake ~]$ curl -I -k -b 'banner=seen' https://www.swagg.net
HTTP/2 200
content-type: text/html;charset=UTF-8
set-cookie: mojolicious=eyJiYW5uZXIiOiJzZWVuIiwiZXhwaXJlcyI6MTYxNTc2Mjc0OX0---22455b29015766360fe791cb13cd16778e4fb197; expires=Sun, 14 Mar 2021 22:59:09 GMT; path=/; HttpOnly; SameSite=Lax
x-cloud-trace-context: 06fdb28b1bbd08a2b04c3d5d668a5afd;o=1
content-length: 5529
date: Sun, 14 Mar 2021 21:59:09 GMT
server: Google Frontend
via: 1.1 google
alt-svc: clear

We can decode the bit of the session cookie to the left of the three hyphens with, what else, perl:

[daniel@threadlake ~]$ perl -MMIME::Base64 -e 'print decode_base64("eyJiYW5uZXIiOiJzZWVuIiwiZXhwaXJlcyI6MTYxNTc2Mjc0OX0"), "\n";'
{"banner":"seen","expires":1615762749}

Expires in an hour rather than the 600 seconds (aka 10 minutes) of our plain-text cookie:

[daniel@threadlake ~]$ date -d @1615762749; echo "it is now: `date`"
Sun Mar 14 06:59:09 PM EDT 2021
it is now: Sun Mar 14 06:04:13 PM EDT 2021

That “stuff” to the right of the three hyphens is currently still a mystery to me. I imagine it has something to do with the HMAC-SHA1 signature that makes the session cookie tamper-proof. That's about all I can say about session cookies for one weekend. One last thing I want to share is another bonus use I found for my newfangled session. I can look for it in my templates/layouts to save the client from even having to download the mp3 file or GDPR banner resources on subsequent requests as well, sparing them some bandwidth:

<% unless (session('banner') eq 'seen') { %>
<!-- Soundtrack -->
<!-- <embed> doesn't work anymore SAD
<embed src="/misc/Smashmouth_-_All_Star.mid" width="100%" height="60">-->
<audio id="soundtrack" src="/Music/Smashmouth-All-Star.mp3" preload="auto">
</audio>
<!-- "GDPR" banner -->
<div id="gdpr">
  <b>Notice:</b> This site uses kickass MIDI technology instead of cookies.
  <img alt="a compact disc playing music" src="/Pictures/Music_CD.gif"
       style="vertical-align: bottom"><br>
  <br>
  <button class="win95button" onclick="playIt();"><u>O</u>K</button>
  <button class="win95button" onclick="closeIt();"><u>C</u>ancel</button>
</div>
<script>
  function closeIt() {
      document.getElementById("gdpr").style.display = "none";
      document.cookie = "banner=seen; max-age=600;";
  }

  function playIt() {
      document.getElementById("soundtrack").play();
      closeIt();
  }
</script>
<% } %>

Layouts and templates are also fun Mojo things I'll want to say more about in later posts. I look forward to sharing how I've actually deployed this thing via Docker container as I was pleasantly surprised on how painless this can be for someone who cut their teeth on Apache and CGI. Diving into Mojo has exposed me to newfangled concepts that I frankly wasn't particulary excited about before (I still maintain that “serverless” is a damn oxymoron!). I find myself more open to exploring them now and hopefully will give me cool things to muse on here in the future.

Source code for my cool new web 2.0 page is on my Codeberg and is, as always, “under construction”.

Happy GDPR-compliant cookie acceptance,

Dan

EDIT: Fixed some markdown formatting errors

#perl #mojolicious