ExtensionDesign

Summary: How to create extensions compatible with ExtensionHub
Version: 2024-04-10
Prerequisites: ExtensionHub
Status: Experimental
Maintainer: Petko
License: GPLv3+
Users: (view? / edit)

How would you create a PmWiki extension, and why would anyone want that?

Description

Extensions are a new kind of PmWiki recipes that are easier to install, configure, and maintain (per PmWiki Philosophy#5), without the need to edit PHP config files.

Extensions are managed by ExtensionHub, a control panel where wiki administrators can enable and configure extensions.

Extensions are slightly different from classic PmWiki recipes, as they:

  • should be structured in a certain way in order to be recognized and automated by the Hub
  • are placed in the pmwiki/extensions directory instead of the pmwiki/cookbook directory.

The main benefit for extensions is the ease of installation and configuration for wiki administrators. This is true for simple recipes, but more so for recipes shipping with additional resources like styles and scripts. Being a wiki administrator myself, I plan to write any new recipes, and eventually convert most of my existing recipes, into the new extension format.

There are also benefits for recipe authors. The Hub includes a number of helper functions that make it easy to access their own local configuration, or inject resources like CSS and JavaScript. Having all extension files grouped in one directory (rather than intermingled in pmwiki/cookbook and pmwiki/pub), is easier to maintain, update, and manage in version control. Being a recipe author myself, ... you get the idea. :-)

Helper functions for extension authors:

  • Easier installation for your recipes, with no required editing of config files, and simpler documentation.
  • Adding, removing, enabling, disabling configurations is managed by the Hub.
  • Extensions can define actions when they need to be included, and priority compared to the processing of local.php, stdconfig.php, and relatively to other extensions.
  • Extensions can have custom configuration forms so that wiki admins set their preferences. The forms can be annotated as needed, and can have documentation or demonstrations.
  • The Hub is managing the storage of the configuration values for the extension.
  • Extensions receive their own configuration options for the currently browsed page.
  • Extensions can optionally include scripts, styles, and wiki pages (documentation, templates, forms...).
    • The extension's base directory is similar to that of a PmWiki skin, with a PHP script, optionally CSS/JS files, pictures, fonts, all in one place. This is easier to manage and update compared to older complex recipes that typically have files and directories intermingled in cookbook/ and pub/.
  • There are helper functions to easily inject scripts and styles (shipped with the extension, or external), and wikilib.d pages.
  • There will be a simple way to define i18n strings for different languages, if the recipe needs this (to do).

There are also some downsides or considerations about using the new format over classic recipes.

  • Wiki administrators need to first install ExtensionHub before your extension can be used.
  • ExtensionHub requires the latest, or a very recent PmWiki version, and at least PHP 7.0.
  • Converting existing recipes to the extension format needs some additional work.
  • While I've been working on the Hub for months, the current API is not written in stone and if there is a good reason to change something, early extensions might need to be updated.

While we add new extensions, any classic recipes will continue to work as before, these are not deprecated or removed.

The following sections describe the structure of an extension, as expected by the Hub, and the usage of the helper functions.

Examples

You can review existing extensions and see how they are are organized, see listing at Extensions. Sample extensions are:

  • BootstrapIcons   BootstrapIcons extension for PmWiki
  • CodeHighlight   Syntax highlighting for programming languages
  • Filterable   Search box for instant filtering of long lists and tables

Let me know if you have any questions or difficulties.

Structure

Extensions are placed in their own directories under the pmwiki/extension directory. All file names will be treated as case-sensitive, including on Windows.

An extension directory structure may look like this:

  ExtensionName/  Name of your extension, capitalized, case sensitive
     ExtensionName.php Same name as above
     LICENSE.txt  recommended, software license terms
     README       optional
     styles.css   optional resource
     script.js    optional resource
     image.png    optional resource
     lib/         optional resource directory with js, css, images, fonts
     wikilib.d/   optional, additional wiki pages
        Site.ExtensionNameForm optional, configuration page
     wikiplain.d/ optional, additional wiki pages
        Site.ExtensionNameForm optional, configuration page

Only the ExtensionName.php script is required, the other resources are optional. The directory and the script need to have the same name, case sensitive.

If you plan to publish the extension on the PmWiki.org Cookbook website, it is mandatory to name the directory and php filename exactly as the Cookbook page name, which will ensure it will work with RecipeCheck.

Script file

  • Your PHP script file should be named exactly the same as your directory, and end with ".php".
  • If your extension directory is "MyCoolExtension", it should contain a file "MyCoolExtension.php".
  • In the ExtensionHub page PmWiki will create a link to Cookbook:MyCoolExtension unless there is a configuration form page.

An extension script file may look like this:

<?php if (!defined('PmWiki')) exit(); # very strongly recommended
/**
  ExtensionName for PmWiki
  Written by (c) Petko Yotov 2023-2024   www.pmwiki.org/petko
  License: MIT, see file LICENSE
*/
$RecipeInfo['ExtensionName']['Version'] = '2024-02-24';

# default configuration
SDVA($MyExtDefaultConf, [
  'var1' => 'value1',
  'var2' => 5,
]);

# Your code here, for example:
function MyExtensionInit($pagename) {
  global $MyExtDefaultConf;

  # Configuration from the wiki, for the current page, 
  # merged with the default values
  $conf = extGetConfig($MyExtDefaultConf);

  # $conf['var1'] is now either 'value1', 
  # or whatever the admin set in the Hub

  # do your magic...

}
MyExtensionInit($pagename);

# EOF, no closing ?> marker

You can configure wiki actions when your script should be included, and the priority, see Form page (configuration) below.

A wiki administrator can add separate configurations for different page patterns, for example per wiki group. The helper function extGetConfig() will return an array of values that apply to the current page and group.

There are additional elements in the array returned by extGetConfig():

  • $conf['=dir'] The base directory of your extension, where your ExtensionName.php file is located.
    • If the extension is compressed, it will start with "phar://" (PHP archive stream).
    • You can include a separate PHP file from your extension directory with include_once("{$conf['=dir']}/anotherscript.php"); which should work both with uncompressed and compressed extensions.
    • To scan and read files from that directory, you can use most PHP filesystem functions, except glob() - use scandir() with MatchNames() or preg_grep() instead.
    • Your script should not try to write or modify files in that directory : it should be expected to be read-only.
  • $conf['=url'] The browser-reachable base URL to your extension base directory.
    • If the extension is compressed, hub.php will serve your files from it on the fly.
    • For CSS and JS files, call extAddHeaderResource() instead, see next section.

Resources

Your extension can optionally ship with stylesheets, JavaScript scripts, images, icons, fonts, and other resources.

These can be in the ExtensionName directory, or in a subdirectory. The file names, and the subdirectory names, can be anything, but it is recommended to stick to letters [a-zA-Z], digits [0-9], minus, dot, and underscore [-._] (no spaces, parentheses, brackets, special, or international characters).

Users can install an extension by dropping its zip archive in the pmwiki/extension directory, and optionally uncompress it. So in the wild, your extension may be compressed, or uncompressed. Additionally, if downloaded from version control like GitHub, the zip file name, and the directory name, may have a release suffix. Additionally, the public path to "extensions" directory may change on different wikis.

We cannot know in advance the public URLs of the resources, that's why a helper function makes it easy to inject these resources in the $HTMLHeaderFmt or $HTMLFooterFmt arrays.

To inject resources, use the helper function extAddHeaderResource like this:
extAddHeaderResource($space_separated_resources, $attributes);

You can call this function once or multiple times (with different resources).

Example:
# add resources to $HTMLHeaderFmt
extAddHeaderResource('mystyles.css lib/library.js myscript.js');

This will add to $HTMLHeaderFmt the necessary <link> and <script> tags that will load your resources, with the correct public extension URLs.

To inject resources in $HTMLFooterFmt, use:

extAddFooterResource('footer_script.js');

If your resources need custom attributes, use the following:

# <script src="...myscript.js" id="extname" data-custom="1">
$attrs["myscript.js"]['id'] = 'extname';
$attrs["myscript.js"]['data-custom'] = '1';

extAddHeaderResource('mystyles.css myscript.js', $attrs); 
# similar with extAddFooterResource() second argument

By default, the resources are relative to the base directory of your extension. To add resources from elsewhere (like a CDN), use the full URL of the resource:

extAddHeaderResource('https://code.jquery.com/jquery-3.7.1.min.js   myscript.js');

Wiki pages (wikilib.d, wikiplain.d)

Your extension can optionally ship with wiki pages that will be made available on the wiki after installation. There are helper functions that allow for simpler activation of your pages.

You can have a regular ExtensionName/wikilib.d directory, where you copy actual wiki page files from your wiki.d. (Maybe strip them from the page history, see ExpireDiff.)

Alternatively, you can have an ExtensionName/wikiplain.d directory, where your wiki pages are simple text files with only the text content, without metadata, permissions, or history. This may be convenient if you prefer to edit the text files in your code editor. If you publish the extension to GitHub or other version control system, a plain text format creates better "diffs" that are easier to review than the native PmWiki page format.

Files in both wikilib.d and wikiplain.d stay in your extension directory, they are not copied to the core wikilib.d directory. However, should a wiki user edit one of the pages you ship, the modified page will be copied to "wiki.d" and subsequently that one will be used when needed. This behavior is similar to how core pages work, for example Main.WikiSandbox or Site.EditForm.

The helper functions that add your pages to the wiki are:

# add a regular wikilib.d with native PmWiki page files
extAddWikiLibDir('wikilib.d');

# add a directory with plain-text wiki pages
extAddWikiPlainDir('wikiplain.d');

If you use these helper functions, the wiki pages will be made available to the wiki. The only exception is a configuration form page (see below), which is only used in the Hub; in this case, and with the default directory names, the page will be included automatically without you calling a helper function.

Form page (configuration)

In the Hub, wiki administrators can configure and enable/disable different "configurations" for different page patterns.

You can ship a form page that will be included by the Hub when a wiki admin enables and configures your extension. The data that is saved by the administrator will be made available to your extension via the function extGetConfig().

The form page should be named Site.ExtensionNameForm, where ExtensionName is the base name of your extension directory and PHP script.

  • The file can be in a subdirectory wikiplain.d, in that case this should be plain text file, with only the wiki source text of the page (no metadata, attributes, or history).
  • The file can be in a subdirectory wikilib.d, in that case this should be a native PmWiki page file, that you have edited on your wiki then copied from wiki.d into this subdirectory.
  • The Hub will load the page from wikiplain.d if it exists, otherwise from wikilib.d if it exists.

This is not a full form, no (:input form:), (:input submit:) or (:input end:) elements, just input fields like text, number, textarea, select, radio and other, see Forms. You can add annotations before and after the fields to make it easy for the administrator.

The form page can have a >>recipeinfo frame<< header with a summary, version, your name, and possibly a Cookbook page URL. Then follow the form elements, then optionally a footer content that may be documentation or demonstration.

Here is a sample configuration form from the Filterable extension:

>>frame<<
$[Summary]: Search field to filter tables and lists\\
$[Version]: {$ExtVersion}\\
$[Maintainer]: [[(https://www.pmwiki.org/wiki/Profiles/)Petko]]\\
$[Cookbook]: [[(Cookbook:)Filterable]]
>><<

[[#form]]
(:comment Optional custom configuration fields:)

Minimum number of items/rows to enable search box (optional):\\
(:input number minsize min=1 step=1:) \\
''Default: '''5''' ''

Query selectors for filterable lists and tables (optional): \\
(:input text selector size=40:) \\
''Default: '''ul.filterable, ol.filterable, table.filterable''' ''

[[#formend]]
(:comment Optional footer content, documentation, demo:)

Summary: Search field to filter tables and lists
Version:
Maintainer: Petko
Cookbook: Filterable

Minimum number of items/rows to enable search box (optional):

Default: 5

Query selectors for filterable lists and tables (optional):

Default: ul.filterable, ol.filterable, table.filterable

When editing an extension configuration, if there is a configuration page, the Hub will include:

  • the header before the [[#form]] anchor at the top of the page,
  • the part between [[#form]]...[[#formend]] inside its own configuration form,
  • the part after [[#formend]] after the configuration form.

Two custom page variables can be used in the form, {$ExtName} and {$ExtVersion}. These are only available when the extension is being configured.

When you request the extension configuration, an array will be returned with the same keys you have in your form, and the values that have been saved. In the form with fields:

(:input number minsize min=1 step=1:)
(:input text selector size=40:)

You get the saved configuration like this:

  $MyDefaultValues ['minsize'] = 5;
  $MyDefaultValues ['selector'] = '';
  $conf = extGetConfig($MyDefaultValues);
  # you can now access $conf['minsize'] and $conf['selector']

There is a limited support for configuration arrays, use something like this:

(:input checkbox lang[] PHP "PHP":)
(:input checkbox lang[] HTML "HTML":)

(:input checkbox theme[defaut] 1 "Default theme":)
(:input checkbox theme[mobile] 1 "Mobile theme":)

When you get the local values:

  $MyDefaultValues ['lang'] = [];
  $MyDefaultValues ['theme'] = [];
  $conf = extGetConfig($MyDefaultValues);
  # $conf['lang'] may be [ 'PHP', 'HTML' ] if both were checked
  # $conf['theme'] may be [ 'default'=>1, 'mobile'=>1 ] if both were checked

The extension SimplePlaylist has a configuration form with an array, check how it is processed.

Your initial/default configuration can include more complex data structures that may not be exposed in a Hub configuration form, but that very advanced users can define in local configuration.

Notes

  • To avoid interference with the core and the Hub, custom field names :
    • cannot be "n", "action", "pmtoken", "i" or "x",
    • cannot start with lowercase "x" followed by an uppercase letter [A-Z] like "xSomething", and
    • should be composed of only ASCII letters a-zA-Z, numbers 0-9 and the underscore character "_".
  • Field names starting with "enc_" or "passwd" will have their values base64-encoded for storage, then decoded when the recipe requests them, or in the recipe configuration form.
  • If a field is left empty, it will not be saved, and will not appear in the $conf array. You can merge the $conf array with your default values as shown above.
  • Numeric values will be cast to Float.

Setting script actions and priority

If your extension needs to be included only for certain wiki actions, you can define them in your configuration form (between the [[#form]]...[[#formend]] anchors) in hidden input fields:

(:input hidden xAction "copy,move,rename":)
Only include the extension when someone calls wiki?action=copy (or move, or rename).
(:input hidden xAction "*,-edit":)
Do not include the extension when the action is "edit" (case sensitive).

The default action is "*" (any action). You don't need to have a xAction field if this works for you.

Similarly, and also optionally, you can define the "priority" or order of your extension relative to config.php, stdconfig.php and other extensions.

(:input hidden xPriority 120:)
include the extension before most other extensions.

Extensions that are enabled on the wiki will be ordered by their priority, then included one after another in that order. Notes:

  • The default priority for extensions is 150. You don't need to include a xPriority element if this works for you.
  • If the priority is between 0 and 100 inclusive, the extension will be included very early in the processing.
    • This is only needed for extensions that do advanced configurations with system and security, and that apply to the whole wiki, for example adding a PageStore class, or an AuthUser backend, or setting global variables like $DefaultName or $UploadPrefixFmt.
    • Note that the global PmWiki variable $pagename may not be available or reliable at that moment of the processing, and you should not resolve it.
    • Such extensions do not have per-page-pattern configurations.
  • If the priority is greater than 100 and less than or equal to 200, the extension will be included after local/config.php, local/Group.Page.php, and local/Group.php, at the beginning of scripts/stdconfig.php.
    • Most recipes should be safely included here. Your recipe can add or disable Markup rules and core variables.
    • $pagename is available, correctly initialized.
  • If the priority is greater than 200, the extension will be included after scripts/stdconfig.php.
    • This should only be needed for rare cases, and you should know what you are doing. For example, here markup rules cannot be redefined or disabled (but new ones could be added), and Skin variables cannot be modified.

Notes

To do / some day / maybe

  • I18n translations should be added in the future.

Change log / Release notes

  • 2024-04-10 Replaced calls to extAddResource() with extAddHeaderResource() and extAddFooterResource()
  • 2024-03-02 Page created

See also

Cookbook /
ExtensionHub  Configuration panel for extensions (Experimental)
Extensions  Recipes compatible with ExtensionHub, see ExtensionDesign.

Contributors

  • Written and maintained by Petko
  • Simon helped me clear the concept with useful insights.

Comments

See discussion at ExtensionDesign-Talk

User notes? : If you use, used or reviewed this recipe, you can add your name. These statistics appear in the Cookbook listings and will help newcomers browsing through the wiki.