Introduction

diffpreview is a plugin for DokuWiki, a PHP wiki software, that adds a “Changes” button to display the changes between your edit and the previous version. Just like MediaWiki’s “Show Changes”.
At the time of writing, the latest stable release of DokuWiki is 2018-04-22 “Greebo”.

Overview

We’re writing a plugin that is considered an Action Plugin. That means that it modifies the behaviour of the software by subscribing to DokuWiki events. To put it simply, they’re the most common type of plugin, if you’re looking to modify what happens when you click on stuff.

What we’re doing is more or less making a Preview button that, instead of displaying a preview, displays a diff like the one in page history.

I’ll be talking about php files that can be found on DokuWiki’s XRef, and the source code of the plugin available on GitHub.

Strategy

So how do we do that ? We modify the UI, create an action names “changes” and handle it. For that, you have to subscribe to these events:

  • HTML_EDITFORM_OUTPUT - BEFORE: that’s just before the edit form (which is the text area and the summary) is compiled to HTML. This lets you add stuff to it
  • ACTION_ACT_PREPROCESS - BEFORE: that’s just before DokuWiki tries to execute the action supplied by ?do= in the URL (or other ways). We use it to tell DokuWiki that we know how to handle the changes action.
  • TPL_ACT_UNKNOWN - BEFORE: that’s the event called when the template is trying to render the page, and doesn’t have a template content associated to the action. So you tell DokuWiki that you handle that part.

The full list of events is available in the docs, but it’s not super explicit. Good enough if you kinda know what you’re looking for.

The button

So the form is created by html_edit() in html.php. The buttons are created by this bit of code which uses functions from form.php:

$form->addElement(form_makeButton('submit', 'save', $lang['btn_save'], array('id'=>'edbtn__save', 'accesskey'=>'s', 'tabindex'=>'4')));
$form->addElement(form_makeButton('submit', 'preview', $lang['btn_preview'], array('id'=>'edbtn__preview', 'accesskey'=>'p', 'tabindex'=>'5')));

We add our button just after the preview button. Those buttons only exists if the page is writable, so that’s a convenient check.
The buttons have a tabindex attribute. It’s used to set the order in which the things on the page get focused while navigating with Tab and Space. See tabindex on MDN. Incrementing the tabindex is a bad practice only because it’s not very maintainable. We want our “Changes” button to focus just after “Preview” so we give it the same tabindex.

Sending the action

The button’s purpose is to add the normal entrypoint to the changes action mode. But how does the server get that information ? PHP weirdness.

Our button in HTML has type “submit” and a name of “do[changes]”. That means that when we click on it, it will submit a HTML POST request with the contents of the other form fields (the text, summary and what not), as well as a key-value pair of the button name and its value. Ours doesn’t have a value so it’s empty. See Button on MDN.
The POST field "do[changes]": undef gets interpreted by PHP into "do": [ "changes": undef ] (using JS object notation) and stores it in the superglobal $_POST. This is documented as “Variables from External Sources - HTML Forms”.

This value is grabbed by the global $ACT variable in doku.php through the PHP superglobal $_REQUEST which is just the variable that we would get if we took $_GET, then overwrote it with $_POST then $_COOKIE at object creation time. This is the default behaviour, which can be changed by the request_order configuration option for php.ini, which uses the variable_order option if it’s unset.

DokuWiki has quite a few global variables which usually represent the global state. Notably, the current edit text is stored in $TEXT.

Dealing with our action

After some initialization code, doku.php calls act_dispatch() which is, as the description says it so well, “[where] all action processing starts […]”. That in turn gets an instance of the (new) ActionRouter which cleans $ACT into a string and starts the processing logic. Said logic is based on things that we can’t access yet such as extending dokuwiki\Action\Plugin and giving it more or less the same functions that we currently define for our plugin. If we could access that, I’m pretty sure we wouldn’t have to call preventDefault() on the $event object, as the default is to create that Plugin and call the necessary functions.

In any case, since our functionality is rather similar to what is implemented by dokuwiki\Action\Preview, we just copy its draft creation code so that we use the same draft.

Outputting the HTML

This part is simple, just find out that html_diff can take text as an argument and diff the current page version against that text. Just remember to add the scroll__here anchor so that the page scrolls to the diff when it reloads.

We could fix the width of the summary text field. But then it would get a bit too short. So although this isn’t ideal, it works.

Oh new features

After a while, I noticed that I could actually access dokuwiki\Action\Edit. Which means that I don’t have to copy its functionality when I’m accessing the “changes” mode directly. Because in that case, I need to load the page contents into the $TEXT, otherwise html_diff thinks that we’re diffing the last 2 revisions, which is not what we want at all: we want to put the user into edit mode with the editor and its toolbars.

So just create a Action\Edit object, and call its functions.

What’s next ?

Regression testing ! That’s painful.

Luckily enough, it works all the way back to “Ponder Stibbons”. In retrospect, it probably does as previous version were checked against those releases, and we made sure to isolate the new API calls.

Conclusion

I spent a week-end figuring out what happens internally, and I guess learned quite a bit about DokuWiki in the process. I don’t really know how useful that is, but at least it’s experience.

The next release might include an easier way to write plugins with dokuwiki\Plugin that extends dokuwiki\AbstractAction from which all the other Actions inherit. So I’ll probably have to update it for the next release.