Requests for comment/Skin templating
The current skinning system is difficult to use for many use-cases, and could therefore use improvement.
There are numerous competing goals for a skinning system in MediaWiki, which are described below. Any system we ship should ideally cater to all of these goals. These goals are broken out by which stakeholders they apply to.
- As a wiki user, I want to:
- Choose between a list of enabled skins to help personalise my browsing and editing experience.
- Customise a skin to modify its appearance or otherwise add or remove functionality, either for only myself or for everyone on the wiki, for varied reasons.
- As a system administrator, I want to:
- Decide which skins are enabled for wiki users to choose, so that they can receive the best set of features available to them or otherwise ensure that the wiki stays on-brand.
- Make simple modifications to skins, to better make them match the unique needs of the wiki.
- Have fast loading times for pages, so that users are not driven away by long load times.
- As a skin developer, I want to:
- Develop skins very similar to existing ones with minimal fuss, to customise various aspects of the skin.
- Develop brand-new skins unlike anything seen before, to provide a unique experience for users of my skin.
- As a PHP extension developer, I want to:
- Inject new elements into defined places of a skin to expose the functionality of my extension.
- Re-arrange or outright replace parts of a skin to expose the functionality of my extension.
- Have a consistent DOM where I can hook my js into to expose its functionality.
Mustache is already being used as a templating engine elsewhere within core and extensions. It will serve this purpose and there is therefore no need to introduce yet another engine when an existing one will work. Avoiding ad-hoc engines reduces friction in developing new skins, and having an engine means we don't need to mix PHP (code) with presentation logic. We currently have such mixing, and it makes some of the above goals nearly impossible to achieve.
What's Wrong With the Current System?
The current skinning system does not adequately meet the above Goals.
- System administrators cannot make simple modifications to skins without resorting to either coding extensions (requiring PHP knowledge they may not have, and there may not be existing hook points for the modifications they wish to make), or by directly modifying the core skin files (which is fragile in the face of upgrades). At best, they can copy the entire skin and make their couple of simple tweaks, and push that out as a new skin which they must then maintain across upgrades.
- Skin developers cannot easily make new skins that function as expected. Many extensions rely on a DOM structure similar to vector (and will break if such a DOM structure is not found), and any attempt to make a new skin requires duplicating everything, even if only small changes are desired from a base skin such as vector.
Wiki users are adequately served by the existing system, and will continue to be adequately served by the new system, as the customisation points in Special:Preferences and sitewide and user css/js pages will remain as-is for this proposal.
Requests for comment/Redo skin framework seems like a previous attempt, however it is still in draft status and is nebulous as to how it will actually be implemented. This RFC contains a concrete implementation strategy, and achieves the same goals as the older draft RFC.
To meet the Goals above, I propose a system where a skin is broken into numerous templates, each responsible for one small piece of the overall rendered page. A hierarchy of skins will allow for skins to inherit templates from parent skins if they wish to use them as-is, or otherwise customise those templates to provide their own functionality. In Core, this inheritance will look somewhat like the following:
- Fallback: Base templates that provide a consistent DOM that all skins can use. It contains no CSS.
When a mustache template is being resolved, it traverses up the hierarchy, using the first defined template it finds. In other words, child templates completely replace parent templates. This search order is defined as follows:
- System administrator customisations of a template (cannot be modified by Extensions)
- The template for the selected skin (possibly modified by Extensions)
- The template of that skin's parent (possibly modified by Extensions)
- ... and so on, up the hierarchy, until we reach the root
When a LESS template is being resolved, the hierarchy is traversed from top-down, so that each child element cascades and replaces things defined in the parent. Unlike mustache templates, every part of these is rendered:
- The template of the topmost skin in the hierarchy
- Children of that skin which are parents of our selected skin, in order
- Our selected skin
- Extension customisations
- system administrator customisations
LESS templates are resolved before transforming it to CSS, meaning that the existing extension points like MediaWiki:Common.css are irrelevant to the above discussion. The goal is that system administrators can customise variables such as @primaryColour to easily theme the site without needing to make their own skin, or change the color in 500 different places once the LESS is expanded to CSS and variables therefore no longer apply.
The above lookups will likely be quite expensive in terms of file I/O, and possibly database calls (depending on where system admin customisations live). As such, this design will expand all templates during install or upgrade, and cache the results. System admin customisations will need to trigger a re-cache for the modified templates via some mechanism (if via Special page, do it as part of that. Otherwise, provide a maintenance script)
Mustache itself only resolves "partials" at runtime, but this would play havoc with the caching system. We would need to extend the mustache parser to expand partials immediately and cache the full result.
In a previous discussion, the security of caching these templates was brought to light. If we're storing arbitrary .php files as part of the cache, that becomes an injection vector. To mitigate these concerns, I can see one of two ways forward:
- Cache the expanded Mustache template, after all partials are replaced recursively with their contents and all system admin customisations are applied. At runtime, we still have the performance hit of parsing the Mustache template to generate the skin itself, but on-filesystem it's just the template so there is less chance that it can be exploited for arbitrary RCE. XSS and other client-side exploits are still possible with this method should an attacker be able to overwrite the cached template with a malicious version.
- Cache the full PHP file, but store a hash of the valid cached file's contents in the database. Before executing the file as PHP code, compare the database hash with the file's hash and discard the cached file if they differ. We could also apply a similar hash validation scheme with Option 1 to validate that a cached Mustache template was not tampered with.
In both of the above proposals, it is assumed that the cache directory is not web-accessible. This can be achieved by shipping .htaccess which denies all requests to the directory. The cache directory should be treated with an equivalent level of trust to the upload directory (that is, not trusted at all).
Extensions can use a hook that receives the text of the template as a string reference, and can manipulate that string to modify template contents. This hook is fired during the upgrade process, and the resulting template is cached.
System admins can customise templates as well, however the means of this customisation is not yet defined. There are two options, which each have security and convenience tradeoffs:
- A special page on the wiki which lets privileged users modify templates. There are two issues with this, as this gives on-wiki users full control of rendered HTML (something that has typically not happened before), and it is possible for a wiki admin to break a template in a way that they render the UI unusable, and therefore are unable to revert the break. Given the security impact of such a special page, the permission to access it would not be granted to any group by default. System administrators should evaluate who (if anyone) should have access to the page and assign the permission via $wgGroupPermissions only after performing such an evaluation. This means that extensibility is effectively disabled by default with this option.
- A defined path on the filesystem for template overrides, which lets people with filesystem access to override templates by placing files there. This keeps control over HTML outside of the wiki, and is more easily reverted in case things break, but it is harder to provide good tooling or good UX when someone is just messing directly with files in a directory. Furthermore, this poses challenges for load-balanced environments: a customisation would need to be applied to every web server in the farm so each local PHP install can find it; not insurmountable using tools such as ansible, but still an extra caveat to keep in mind.
If the above design is put in place, we meet our Goals in the following ways:
- Wiki users can still use Special:Preferences to choose their skin, and can customise skins via sitewide and user css/js.
- system admins can still choose the list of allowed skins and default skins via the same mechanisms as now (wfLoadSkin, $wg variables to control skin selection)
- system admins can use the Extensibility points to customise mustache templates (to manipulate HTML) or LESS templates (to manipulate styles) in a way that is currently not achievable without creating a brand-new skin.
- Skin developers can inherit from any skin if they want to make simple or minor tweaks, they can inherit from Fallback if they want to provide something wholly custom while still having a consistent DOM with other skins, or they could make their new skin as a root if a consistent DOM is not a concern for them.
- PHP extension developers can use the Extensibility points to make changes to templates that are consistent across every skin.
- JS authors can rely on a (more) consistent DOM across skins than what we currently have, making things like Gadgets more robust.
What About the Current System?
The current skinning system and infrastructure will continue to work, but will be deprecated. After a period of at least 4 releases where it is deprecated, it will subsequently be removed.
For example, if this system makes it into 1.34, the old system will be soft deprecated in 1.34 and 1.35, hard deprecated in 1.36 and 1.37, and removed entirely in 1.38. Critical issues raised by 3rd party users where the new system is not capable of achieving something the old system is capable of doing may merit prolonging the deprecation period.
Questions and Comments
Why not Twig?
- Since both Drupal & Symfony use Twig, maybe that is a better option than Mustache? --DBarratt (WMF) (talk) 18:12, 26 February 2019 (UTC)
- It may be best not to try to reopen T379 here. Anomie (talk) 18:57, 26 February 2019 (UTC)
- Per the #Why Mustache? section above, mustache is already in core. Anomie provided a link to the relevant RFC that described it. Mustache would not be my first choice in a greenfield implementation, but I want to stick with what was already discussed and decided upon earlier. --Skizzerz 19:13, 26 February 2019 (UTC)
- I really can't care less which template engine it is. We should start with simply enforcing that HTML is generated using templates ! —TheDJ (Not WMF) (talk • contribs) 12:28, 27 February 2019 (UTC)