The forgotten Sent Messages for Sensei LMS

I had multiple plugins in the WordPress.org directory, but a few years ago, I closed them all except for one.

Sensei LMS was an LMS that I seriously considered for a project, and while investigating it, I even submitted a couple of minor fixes.

This was three years ago. Gutenberg wasn't a thing, and Sensei LMS was just becoming important again for Automattic.

Back then, I noticed that:

By default, there is no way to have access to previously sent messages from the course, lessons, or quizzes. The only option to see the messages is by going to the Inbox page. This might be cumbersome, especially if you sent a few messages and you are expecting an answer.

And for this reason, I wrote and released a plugin:

Sent Messages for Sensei LMS addresses this shortcoming by giving you quick access to previous messages.

These quotes are from the old announcement post titled Introducing Sent Messages for Sensei LMS.

Sensei LMS is at version 4 now, and it comes with a new theme called Learning Mode. It has a feature similar to what existed in LearnDash for years under the name of Focus Mode. Still, it's exciting news.

It's exciting enough to motivate me to update the Sent Messages for Sensei LMS to work with the block editor.

Automate explicit date assignments for Eleventy posts using Git log

Since version 1.0.1, 11ty can infer the date from the git log automatically.

The Eleventy website mentions relying on the date associated with a piece of content (e.g., date created) when CI (Continuous Integration) is involved as a common pitfall.

Be careful relying on the default date associated with a piece of content. By default Eleventy uses file creation dates, which works fine if you run Eleventy locally but may reset in some conditions if you run Eleventy on a Continuous Integration server. Work around this by using explicit date assignments, either in your front matter or your content’s file name.

Let's take the Netlify deployment process as an example.

When you create a project, you associate it with a Git repository. You do this because part of the deployment process, Netlify clones the repository. This is how it gets access to your files.

...
6:28:02 PM: Starting to prepare the repo for build
6:28:03 PM: Preparing Git Reference refs/heads/master
...

The "problem" is that the files' creation dates will be the time of the cloning. This is because files did not exist before on the CI "machine"; they are created as brand new files.

If you rely on the creation date, the articles you wrote two years ago will appear as published just right now.

The recommended workaround is to use explicit date assignments, aka put that date somewhere!

It makes sense. Still, it's a little annoying.

Say you are using WordPress (or any other CMS, really) for light-weight blogging, you usually don't set the date. The published date will be the time of clicking the "publish" button.

Of course, tradeoffs.


This got me thinking, is there something equivalent to the "publish" button when using a static site generator?

I think so. We can consider the time of committing the file the time of publishing.

We have access to this information as long as we keep the posts under version control. And yes, we can explicitly assign the dates automatically using this information!

Here's an idea of how to go about this.

Get the "published date" of a post (Markdown file).

git log --format=%as ./src/content/posts/commit-without-file-changes-in-git.md | tail -1

The %as for that format flag is the "author date, short format (YYYY-MM-DD)". There are multiple format types, for example, %at is the UNIX timestamp format, %aD uses the RFC2822 style.

These are documented; check the help manual using git log --help.

Instead of the typical log with the committer name, message, date, you get back this:

2021-12-27
2021-12-20
2021-12-13
2021-12-06
2021-12-04

tail -1 returns the contents of the last row, which is the date of the first-ever commit, aka the "published date".

Do this for multiple posts.

#!/bin/bash
FILES=$(find ./src/content/posts -maxdepth 1 -type f -name "*.md")

for FILE in $FILES
do
git log --format=%as "$FILE" | tail -1
done

The regular expression of the renaming part largely depends on what convention you use for naming your posts.

#!/bin/bash
FILES=$(find ./src/content/posts -maxdepth 1 -type f -name "*.md")

for FILE in $FILES
do
DATE_PUBLISHED=$(git log --format=%as "$FILE" | tail -1)
RENAMED_FILE=$(echo "$FILE" | sed -E "s/([A-Za-z0-9-]+)(\.md)/$DATE_PUBLISHED-\1\2/g" )
mv "$FILE" "$RENAMED_FILE"
done

(The script does not check if the returned files are empty or if the files already have the date prepended. If you sanity checks, make sure you added them.)

To deconstruct the regex pattern, check out the explanation on RegEx101. You will see information about the match and replacement. It essentially prepends the published date to the filename in the most straightforward way.

This bash script can be run during the deployment process on Netlify. If you previously ran npx @11ty/eleventy to build the website, you can just run the bash script before it:

./add-explicit-date-assignments-to-posts && npx @11ty/eleventy

Eleventy automatically removes the dates from the permalinks when it generates the HTML files. This means that it does not matter that after renaming, you ended up with files like this:

...
./src/content/posts/2021-12-04-commit-without-file-changes-in-git.md
...

12ft Ladder to remove paywalls

The 12ft Ladder is well-executed. It's based on an old idea, but the way it's put together is very user-friendly. I especially like the idea behind the iOS shortcut.

The only "problem" with it, it does not work anymore. When trying to view The New York Times, it returns the "12ft has been disabled for this site" message.

So what's that old idea? If outlets want to rank high and be indexed by search engines - and they do - then they have to let crawlers index their content. So supposedly, crawlers get served the full articles. The trick is to pretend to be the Googlebot, Bingbot or Whateverbot.

Of course, it's more complicated than it sounds. Portals, journals, magazines are smart enough to detect the most obvious ways of people faking requests. And if a paywall removal service finds a backdoor and becomes popular, it ends up as 12ft Ladder, blocked.

Popularity is not always beneficial.

Well-known URL for changing password in WordPress

There's a proposal for a well-known URL for changing passwords.

The main idea is to redirect the /.well-known/change-password URL to the actual URL for changing the password. If you have a WordPress installation, your change password URL, by default, /wp-login.php?action=lostpassword.

Of course, this applies only if your website offers some account registration.

If we would all implement this as a standard, users, and apps, specifically password managers, would know where the change password page is located without an effort. And with knowledge, we could build better integrations, workflows, and so on.

It got me thinking, is this something WordPress could offer by default? In fact, there's already a ticket created in the Track requiring this feature.

But until then, how can somebody implement this? There are three ways to do it.

The server handles the redirect

For some sites on an Apache server, if .htaccess is enabled, the redirect could be as straightforward as this:

Redirect 301 "/.well-known/change-password" "/wp-login.php?action=lostpassword"

Surely, this would not work for all installations. If you are using multi-sites, you would have to go with regex and RedirectMatch to get what you want.

I don't think WordPress core will update the .htaccess rules that are generated when pretty permalinks are enabled. But many security, caching plugins add additional rules, so I think we could see this approach from a plugin.

Users with other server types, like Nginx, would have to do what they do now, follow the documentation and instructions.

Probably managed WordPress hostings will take care if this for you sooner or later; for example, WordPress.com set up the redirect.

Create an actual file

If you are running WordPress, this would be the most atypical way to solve it, that is, to create a file named change-password inside the .well-known folder with the following content:

<!DOCTYPE html><meta http-equiv="refresh" content="0;url=/wp-login.php?action=lostpassword">

I can imagine this as a fallback solution when nothing else works since it's not a good practice to create files outside the uploads folder in the WordPress space.

And it's also unclear for me how this would work for multi-sites.

Technically the redirection should work because of the http-equiv="refresh". At least as long as the proper content-type header is set and the browser interprets the file as HTML.

Handling the response with PHP

The third option is to intercept the request and do a redirect with PHP.

Because there are many uses for well-known URLs, there are already plugins that support specific well-known URLs in the WordPress.org plugin directory.

For example, the Brave Payments Verification plugin uses a rewrite rule to handle a well-known URL. This is quite common), I would say the most common way to do it.

Another way is to use an early hook and check the global $_SERVER. This is what WordPress does for handling some current redirects.

Handing it on the PHP side is also not a bulletproof solution. Many servers do not pass the request to the PHP because they URLs with paths that start with . (dot) differently.


I hope whoever writes a WordPress plugin for this well-known change password URL will somehow implement all options for the redirect. That is because those who are not able to do this on their own need the most options. A site with membership or a shop has the capability or resources to resolve it.

Wrap elements in a group with a border using TCPDF

From time to time, I stumbled upon TCPDF and knew that many higher-level libraries, CMSs use it as a dependency. But until now, when I had to create a complex, dynamic document, I never worked with it.

It has some quirks. For example, the getPageWidth is with lowercase, but the GetX is not, even though both are public methods.

Another one, if you want to have a different vertical alignment than the default on a cell, you have to overwrite 14 arguments before you can specify that you want it aligned to the bottom.

// See the "B" at the end which stands for bottom
$pdf->MultiCell(200, 80, 'Lorem ipsum ...', 0, 'J', false, 1, $pdf->GetX(), $pdf->GetY(), true, 0, false, true, 0, 'B');

One more, when you call SetMargins to set the margins for the page, there's no option for the bottom. You can only set the left, top, right.

But putting all these aside, in the end, it grew on me, and I'm grateful somebody took the time to create, continuously develop, and maintain it since 2002.


In the beginning, one of the things that tripped me up was not being able to nest cells inside other cells or somehow group them into another object.

A cell in TCPDF world is a rectangular area containing some text. You can apply some basic styles to the cell, like background color and border width. The rectangular area is invisible by default, so you will see just the text with no background color or border.

Usually, you call the Cell, MultiCell, or similar methods to display a text.

In one area, I wanted to achieve a configuration of elements like this:

After going over all the 65 examples, I was baffled. I could not see how I can create a cell with a border that contains other cells.

This is not something that works:

$pdf->setCellPaddings(4);
$pdf->MultiCell(
0, // the width; 0 means as long as the page
0, // the height; 0 means as long as the content
$pdf->MultiCell(0, 0, 'Lorem ipsum ...'),
1 // 1 enables the border using the default style
);

But after I started thinking about TCPDF more like the Canvas API, I realized there's no need to group or nest elements.

It's all about the looks.

It's not a coincidence that you can at any point render, draw something anywhere on the page.

TCPDF renders elements usually from top to bottom, but you can jump from one point to another and continue from there.

There's something similar even in their API:

const ctx = canvas.getContext('2d');

ctx.rect(10, 20, 150, 100);
ctx.stroke();
$pdf = new TCPDF();

$pdf->SetLineStyle(...);
$pdf->Rect(10, 20, 150, 100);

It's more like a drawing tool than some templating language.

My conclusion was, if I want to group things, I have to draw four lines around the elements or use a rectangle or a polygon. Visually the result is the same.

In my case, this was not a one-time-needed thing, I had to "wrap elements in a group" multiple times, so I created the wrapInFauxGroupWithBorder method.

class ExtendedTCPDF extends TCPDF
{
public function wrapInFauxGroupWithBorder(callable $innerContent)
{
// ...

$innerContent();

// ...
}
}

This allows me to use it like this:

$pdf = new ExtendedTCPDF();

$pdf->wrapInFauxGroupWithBorder(function () use ($pdf) {
$pdf->MultiCell(...);

$pdf->WriteHTML(...);

$pdf->MultiCell(...);
});

I can put - what I consider - the inner content in the callback, and the method will take care of the necessary calculation, setting padding, and drawing the border.


If you ever need this too, you can go about it in the following way.

As the first step, just draw the border around whatever the inner content is.

Here's the code for this:

class ExtendedTCPDF extends TCPDF  
{
public function wrapInFauxGroupWithBorder(callable $innerContent)
{
$groupStartX = $this->GetX();
$groupStartY = $this->GetY();

$pageMargins = $this->getMargins();

$pageWidth = $this->getCurrentPageWidth();
$pageInnerWidth = $pageWidth - $pageMargins['left'] - $pageMargins['right'];

$innerContent();

$innerContentEndY = $this->GetY();
$innerContentHeight = $innerContentEndY - $groupStartY;

$this->Rect(
$groupStartX,
$groupStartY,
$pageInnerWidth,
$innerContentHeight
);
}

public function getCurrentPageWidth(): int
{
return $this->getPageWidth();
}
}

The main concern here is determining the rectangle's width and height that visually wraps the content.

The Rect needs the starting positions and the dimensions. From where you start drawing is given, it's from where the "group" starts.

The width should be the width of the content area. That is the width of the page minus the margins.

The height should be the height of the inner content. You can get that by subtracting the Y-axis position from the moment it stops rendering from the place it started.

The Rect method accepts additional arguments. Those are for the border style and the fill color, but I'm okay with inheriting the defaults.

The getCurrentPageWidth is up to you; it's not that important. I prefer to make explicit that I'm getting the width of the current page, as with the getPageWidth, you can get the width of other pages.

For the next step, apply the padding to add some space between the border and content.

class ExtendedTCPDF extends TCPDF
{
+ public const GROUP_INNER_PADDING = 4;

public function wrapInFauxGroupWithBorder(callable $innerContent)
{
$groupStartX = $this->GetX();
$groupStartY = $this->GetY();

$pageMargins = $this->getMargins();

$pageWidth = $this->getCurrentPageWidth();
$pageInnerWidth = $pageWidth - $pageMargins['left'] - $pageMargins['right'];

+ $this->StartTransform();
+ $this->Translate(self::GROUP_INNER_PADDING, self::GROUP_INNER_PADDING);

$innerContent();

$innerContentEndY = $this->GetY();
$innerContentHeight = $innerContentEndY - $groupStartY;

+ $this->StopTransform();

$this->Rect(
$groupStartX,
$groupStartY,
$pageInnerWidth,
- $innerContentHeight
+ $innerContentHeight + self::GROUP_INNER_PADDING * 2
);

+ $this->SetY($innerContentEndY + self::GROUP_INNER_PADDING * 2);
}
}

Keep the starting position of the rectangle but shift the position of the inner content elements.

Instead of modifying the X and Y values, use the Translate method. This keeps the calculations way simpler. Just trust me on this.

To keep the change isolated, don't forget to wrap it in the StartTransform and StopTransform. Otherwise, all subsequent elements are going to be offset.

The height of the rectangle has to increase by the value of the top and bottom padding. If it's equal, just multiply it by two.

When you use the Translate that does not change the Y position (nor the X), you have to explicitly set the next element's starting position. If not, elements will overlap.

Here's how it all looks now:

The last piece of the puzzle deals with the elements without an explicit width. Currently, these are exceeding our "group" width.

If you don't set the width explicitly for a cell, that will be the content area's width. TCPDF assigns the width correctly here, but remember how you shifted the positions with the Translate method, and now you have to account for that.

One way to solve this is to temporarily change the page's width and then set it back once the inner contents are rendered.

class ExtendedTCPDF extends TCPDF  
{
public function wrapInFauxGroupWithBorder(callable $innerContent)
{
$groupStartX = $this->GetX();
$groupStartY = $this->GetY();

$pageMargins = $this->getMargins();

- $pageWidth = $this->getCurrentPageWidth();
- $pageInnerWidth = $pageWidth - $pageMargins['left'] - $pageMargins['right'];
+ $outsideGroupPageWidth = $this->getCurrentPageWidth();
+ $outsideGroupPageInnerWidth = $outsideGroupPageWidth - $pageMargins['left'] - $pageMargins['right'];

$this->StartTransform();
$this->Translate(self::GROUP_INNER_PADDING, self::GROUP_INNER_PADDING);

+ $this->setCurrentPageWidth($outsideGroupPageWidth - self::GROUP_INNER_PADDING * 2);

$innerContent();

$innerContentEndY = $this->GetY();
$innerContentHeight = $innerContentEndY - $groupStartY;

$this->StopTransform();

$this->Rect(
$groupStartX,
$groupStartY,
$outsideGroupPageInnerWidth,
$innerContentHeight + self::GROUP_INNER_PADDING * 2
);

+ $this->setCurrentPageWidth($outsideGroupPageWidth);

$this->SetY($innerContentEndY + self::GROUP_INNER_PADDING * 2);
}

+ public function setCurrentPageWidth(int $width)
+ {
+ $this->w = $width;
+ }
}

TCPDF does not have a method for setting the page width using a method, hence the introduction of setCurrentPageWidth.

And with all this, you encapsulated all the calculations in a reusable method that you can call anytime when you need "group elements" together: