--- /dev/null
+---
+title: Creating a simple package
+sidebar: sidebar
+permalink: getting-started_quick-start.html
+folder: getting-started
+---
+
+## Setup and Requirements
+
+This guide will help you to create a simple package that provides a simple test
+page. It is nothing too fancy, but you can use it as the foundation for your
+next project.
+
+There are some requirements you should met before starting:
+
+- Text editor with syntax highlighting for PHP, [Notepad++](https://notepad-plus-plus.org/) is a solid pick
+ - `*.php` and `*.tpl` should be encoded with ANSI/ASCII
+ - `*.xml` are always encoded with UTF-8, but omit the BOM (byte-order-mark)
+ - Use tabs instead of spaces to indent lines
+ - It is recommended to set the tab width to `8` spaces, this is used in the entire software and will ease reading the source files
+- An active installation of WoltLab Suite 3
+- An application to create `*.tar` archives, e.g. [7-Zip](http://www.7-zip.org/) on Windows
+
+## The package.xml File
+
+We want to create a simple page that will display the sentence "Hello World" embedded
+into the application frame. Create an empty directory in the workspace of your choice
+to start with.
+
+Create a new file called `package.xml` and insert the code below:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/package.xsd" name="com.example.test">
+ <packageinformation>
+ <!-- com.example.test -->
+ <packagename>Simple Package</packagename>
+ <packagedescription>A simple package to demonstrate the package system of WoltLab Suite Core</packagedescription>
+ <version>1.0.0</version>
+ <date>2019-04-28</date>
+ </packageinformation>
+ <authorinformation>
+ <author>Your Name</author>
+ <authorurl>http://www.example.com</authorurl>
+ </authorinformation>
+ <excludedpackages>
+ <excludedpackage version="6.0.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+ </excludedpackages>
+ <instructions type="install">
+ <instruction type="file" />
+ <instruction type="template" />
+ <instruction type="page" />
+ </instructions>
+</package>
+```
+
+There is an [entire chapter][package_package-xml] on the package system that explains what the code above
+does and how you can adjust it to fit your needs. For now we'll keep it as it is.
+
+## The PHP Class
+
+The next step is to create the PHP class which will serve our page:
+
+1. Create the directory `files` in the same directory where `package.xml` is located
+2. Open `files` and create the directory `lib`
+3. Open `lib` and create the directory `page`
+4. Within the directory `page`, please create the file `TestPage.class.php`
+
+Copy and paste the following code into the `TestPage.class.php`:
+
+```php
+<?php
+namespace wcf\page;
+use wcf\system\WCF;
+
+/**
+ * A simple test page for demonstration purposes.
+ *
+ * @author YOUR NAME
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+class TestPage extends AbstractPage {
+ /**
+ * @var string
+ */
+ protected $greet = '';
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_GET['greet'])) $this->greet = $_GET['greet'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($this->greet)) {
+ $this->greet = 'World';
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'greet' => $this->greet
+ ]);
+ }
+}
+
+```
+
+The class inherits from [wcf\page\AbstractPage](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/page/AbstractPage.class.php), the default implementation of pages without form controls. It
+defines quite a few methods that will be automatically invoked in a specific order, for example `readParameters()` before `readData()` and finally `assignVariables()` to pass arbitrary values to the template.
+
+The property `$greet` is defined as `World`, but can optionally be populated through a GET variable (`index.php?test/&greet=You` would output `Hello You!`). This extra code illustrates the separation of data
+processing that takes place within all sort of pages, where all user-supplied data is read from within a single method. It helps organizing the code, but most of all it enforces a clean class logic that does not
+start reading user input at random places, including the risk to only escape the input of variable `$_GET['foo']` 4 out of 5 times.
+
+Reading and processing the data is only half the story, now we need a template to display the actual content for our page. You don't need to specify it yourself, it will be automatically guessed based on your
+namespace and class name, you can [read more about it later](#appendixTemplateGuessing).
+
+Last but not least, you must not include the closing PHP tag `?>` at the end, it can cause PHP to break on whitespaces and is not required at all.
+
+## The Template
+
+Navigate back to the root directory of your package until you see both the `files` directory and the `package.xml`. Now create a directory called `templates`, open it and create the file `test.tpl`.
+
+```smarty
+{include file='header'}
+
+<div class="section">
+ Hello {$greet}!
+</div>
+
+{include file='footer'}
+```
+
+Templates are a mixture of HTML and Smarty-like template scripting to overcome the static nature of raw HTML. The above code will display the phrase `Hello World!` in the application frame, just as any other
+page would render. The included templates `header` and `footer` are responsible for the majority of the overall page functionality, but offer a whole lot of customization abilities to influence their behavior and appearance.
+
+## The Page Definition
+
+The package now contains the PHP class and the matching template, but it is still missing the page definition. Please create the file `page.xml` in your project's root directory, thus on the same level as the `package.xml`.
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/page.xsd">
+ <import>
+ <page identifier="com.example.test.Test">
+ <controller>wcf\page\TestPage</controller>
+ <name language="en">Test Page</name>
+ <pageType>system</pageType>
+ </page>
+ </import>
+</data>
+```
+
+You can provide a lot more data for a page, including logical nesting and dedicated handler classes for display in menus.
+
+## Building the Package
+
+If you have followed the above guidelines carefully, your package directory should now look like this:
+
+```
+├── files
+│ └── lib
+│ ├── page
+│ │ ├── TestPage.class.php
+├── package.xml
+├── page.xml
+├── templates
+│ └── test.tpl
+```
+
+Both files and templates are archive-based package components, that deploy their payload using tar archives rather than adding the raw files to the package file. Please create the archive `files.tar` and add the contents of the `files/*` directory, but not the directory `files/` itself. Repeat the same process for the `templates` directory, but this time with the file name `templates.tar`. Place both files in the root of your project.
+
+Last but not least, create the package archive `com.example.test.tar` and add all the files listed below.
+
+- `files.tar`
+- `package.xml`
+- `page.xml`
+- `templates.tar`
+
+The archive's filename can be anything you want, all though it is the general convention to use the package name itself for easier recognition.
+
+## Installation
+
+Open the Administration Control Panel and navigate to `Configuration > Packages > Install Package`, click on `Upload Package` and select the file `com.example.test.tar` from your disk. Follow the on-screen instructions until it has been successfully installed.
+
+Open a new browser tab and navigate to your newly created page. If WoltLab Suite is installed at `https://example.com/wsc/`, then the URL should read `https://example.com/wsc/index.php?test/`.
+
+Congratulations, you have just created your first package!
+
+## Developer Tools
+
+{% include callout.html content="This feature is available with WoltLab Suite 3.1 or newer only." type="warning" %}
+
+The developer tools provide an interface to synchronize the data of an installed package with a bare repository on the local disk. You can re-import most PIPs at any time and have the changes applied without crafting a manual update. This process simulates a regular package update with a single PIP only, and resets the cache after the import has been completed.
+
+### Registering a Project
+
+Projects require the absolute path to the package directory, that is, the directory where it can find the `package.xml`. It is not required to install an package to register it as a project, but you have to install it in order to work with it. It does not install the package by itself!
+
+There is a special button on the project list that allows for a mass-import of projects based on a search path. Each direct child directory of the provided path will be tested and projects created this way will use the identifier extracted from the `package.xml`.
+
+### Synchronizing
+
+The install instructions in the `package.xml` are ignored when offering the PIP imports, the detection works entirely based on the default filename for each PIP. On top of that, only PIPs that implement the interface `wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin` are valid for import, as it indicates that importing the PIP multiple times will have no side-effects and that the result is deterministic regardless of the number of times it has been imported.
+
+Some built-in PIPs, such as `sql` or `script`, do not qualify for this step and remain unavailable at all times. However, you can still craft and perform an actual package update to have these PIPs executed.
+
+## Appendix
+
+### Template Guessing {#appendixTemplateGuessing}
+
+The class name including the namespace is used to automatically determine the path to the template and its name. The example above used the page class name `wcf\page\TestPage` that is then split into four distinct parts:
+
+1. `wcf`, the internal abbreviation of WoltLab Suite Core (previously known as WoltLab Community Framework)
+2. `\page\` (ignored)
+3. `Test`, the actual name that is used for both the template and the URL
+4. `Page` (page type, ignored)
+
+The fragments `1.` and `3.` from above are used to construct the path to the template: `<installDirOfWSC>/templates/test.tpl` (the first letter of `Test` is being converted to lower-case).
+
+{% include links.html %}
--- /dev/null
+---
+title: WoltLab Suite 5.3 Documentation
+sidebar: sidebar
+permalink: index.html
+toc: false
+---
+
+## Introduction
+
+This documentation explains the basic API functionality and the creation of own packages. It is expected that you are somewhat experienced with [PHP](https://en.wikipedia.org/wiki/PHP), [Object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) and [MySQL](https://en.wikipedia.org/wiki/MySQL).
+
+Head over to the [quick start tutorial][getting-started_quick-start] to learn more.
+
+## About WoltLab Suite 5.3
+
+The [WoltLab Suite Core](https://github.com/WoltLab/WCF) as well as most of the other packages are available on [github.com/WoltLab/](https://github.com/WoltLab) and are licensed under the terms of the [GNU Lesser General Public License 2.1](https://github.com/WoltLab/WCF/blob/master/LICENSE).
+
+You can edit this documentation by visiting the edit link on each page, it is also available on [GitHub](https://github.com/WoltLab/woltlab.github.io).
+
+{% include links.html %}
--- /dev/null
+---
+title: Code Snippets - JavaScript API
+sidebar: sidebar
+permalink: javascript_code-snippets.html
+folder: javascript
+---
+
+This is a list of code snippets that do not fit into any of the other articles
+and merely describe how to achieve something very specific, rather than explaining
+the inner workings of a function.
+
+## ImageViewer
+
+The ImageViewer is available on all frontend pages by default, you can easily
+add images to the viewer by wrapping the thumbnails with a link with the CSS
+class `jsImageViewer` that points to the full version.
+
+```html
+<a href="http://example.com/full.jpg" class="jsImageViewer">
+ <img src="http://example.com/thumbnail.jpg">
+</a>
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: General JavaScript Usage
+sidebar: sidebar
+permalink: javascript_general-usage.html
+folder: javascript
+---
+
+## The History of the Legacy API
+
+The WoltLab Suite 3.0 [introduced a new API][javascript_new-api_writing-a-module] based on AMD-Modules
+with ES5-JavaScript that was designed with high performance and visible dependencies
+in mind. This was a fundamental change in comparison to [the legacy API][javascript_legacy-api]
+that was build many years before while jQuery was still a thing and we had to deal
+with ancient browsers such as Internet Explorer 9 that felt short in both CSS and
+JavaScript capabilities.
+
+Fast forward a few years, the old API is still around and most important, it is
+actively being used by some components that have not been rewritten yet. This
+has been done to preserve the backwards-compatibility and to avoid the
+significant amount of work that it requires to rewrite a component. The components
+invoked on page initialization have all been rewritten to use the modern API, but
+some deferred objects that are invoked later during the page runtime may still
+use the old API.
+
+However, the legacy API is deprecated and you should not rely on it for new
+components at all. It slowly but steadily gets replaced up until a point where its
+last bits are finally removed from the code base.
+
+## Embedding JavaScript inside Templates
+
+The `<script>`-tags are extracted and moved during template processing, eventually
+placing them at the very end of the body element while preserving their order of
+appearance.
+
+This behavior is controlled through the `data-relocate="true"` attribute on the `<script>`
+which is mandatory for almost all scripts, mostly because their dependencies (such
+as jQuery) are moved to the bottom anyway.
+
+```html
+<script data-relocate="true">
+ $(function() {
+ // Code that uses jQuery (Legacy API)
+ });
+</script>
+
+<!-- or -->
+
+<script data-relocate="true">
+ require(["Some", "Dependencies"], function(Some, Dependencies) {
+ // Modern API
+ });
+</script>
+```
+
+## Including External JavaScript Files
+
+The AMD-Modules used in the new API are automatically recognized and lazy-loaded
+on demand, so unless you have a rather large and pre-compiled code-base, there
+is nothing else to worry about.
+
+### Debug-Variants and Cache-Buster
+
+Your JavaScript files may change over time and you would want the users' browsers
+to always load and use the latest version of your files. This can be achieved by
+appending the special `LAST_UPDATE_TIME` constant to your file path. It contains
+the unix timestamp of the last time any package was installed, updated or removed
+and thus avoid outdated caches by relying on a unique value, without invalidating
+the cache more often that it needs to be.
+
+```html
+<script data-relocate="true" src="{@$__wcf->getPath('app')}js/App.js?t={@LAST_UPDATE_TIME}"></script>
+```
+
+For small scripts you can simply serve the full, non-minified version to the user
+at all times, the differences in size and execution speed are insignificant and
+are very unlikely to offer any benefits. They might even yield a worse performance,
+because you'll have to include them statically in the template, even if the code
+is never called.
+
+However, if you're including a minified build in your app or plugin, you should
+include a switch to load the uncompressed version in the debug mode, while serving
+the minified and optimized file to the average visitor. You should use the
+`ENABLE_DEBUG_MODE` constant to decide which version should be loaded.
+
+```html
+<script data-relocate="true" src="{@$__wcf->getPath('app')}js/App{if !ENABLE_DEBUG_MODE}.min{/if}.js?t={@LAST_UPDATE_TIME}"></script>
+```
+
+### The Accelerated Guest View ("Tiny Builds")
+
+{% include callout.html content="You can learn more on the [Accelerated Guest View][migration_wsc-30_javascript] in the migration docs." type="info" %}
+
+The "Accelerated Guest View" was introduced in WoltLab Suite 3.1 and aims to
+decrease page size and to improve responsiveness by enabling a read-only mode
+for visitors. If you are providing a separate compiled build for this mode, you'll
+need to include yet another switch to serve the right version to the visitor.
+
+```html
+<script data-relocate="true" src="{@$__wcf->getPath('app')}js/App{if !ENABLE_DEBUG_MODE}{if VISITOR_USE_TINY_BUILD}.tiny{/if}.min{/if}.js?t={@LAST_UPDATE_TIME}"></script>
+```
+
+### The `{js}` Template Plugin
+
+The `{js}` template plugin exists solely to provide a much easier and less error-prone
+method to include external JavaScript files.
+
+```html
+{js application='app' file='App' hasTiny=true}
+```
+
+The `hasTiny` attribute is optional, you can set it to `false` or just omit it
+entirely if you do not provide a tiny build for your file.
+
+{% include links.html %}
--- /dev/null
+---
+title: JavaScript Helper Functions
+sidebar: sidebar
+permalink: javascript_helper-functions.html
+folder: javascript
+---
+
+## Introduction
+
+Since version 3.0, WoltLab Suite ships with a set of global helper functions that are
+exposed on the `window`-object and thus are available regardless of the context.
+They are meant to reduce code repetition and to increase readability by moving
+potentially relevant parts to the front of an instruction.
+
+## Elements
+
+### `elCreate(tagName: string): Element`
+
+Creates a new element with the provided tag name.
+
+```js
+var element = elCreate("div");
+// equals
+var element = document.createElement("div");
+```
+
+### `elRemove(element: Element)`
+
+Removes an element from its parent without returning it. This function will throw
+an error if the `element` doesn't have a parent node.
+
+```js
+elRemove(element);
+// equals
+element.parentNode.removeChild(element);
+```
+
+### `elShow(element: Element)`
+
+Attempts to show an element by removing the `display` CSS-property, usually used
+in conjunction with the `elHide()` function.
+
+```js
+elShow(element);
+// equals
+element.style.removeProperty("display");
+```
+
+### `elHide(element: Element)`
+
+Attempts to hide an element by setting the `display` CSS-property to `none`, this
+is intended to be used with `elShow()` that relies on this behavior.
+
+```js
+elHide(element);
+// equals
+element.style.setProperty("display", "none", "");
+```
+
+### `elToggle(element: Element)`
+
+Attempts to toggle the visibility of an element by examining the value of the
+`display` CSS-property and calls either `elShow()` or `elHide()`.
+
+## Attributes
+
+### `elAttr(element: Element, attribute: string, value?: string): string`
+
+Sets or reads an attribute value, value are implicitly casted into strings and
+reading non-existing attributes will always yield an empty string. If you want
+to test for attribute existence, you'll have to fall-back to the native
+[`Element.hasAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)
+method.
+
+You should read and set native attributes directly, such as `img.src` rather
+than `img.getAttribute("src");`.
+
+```js
+var value = elAttr(element, "some-attribute");
+// equals
+var value = element.getAttribute("some-attribute");
+
+elAttr(element, "some-attribute", "some value");
+// equals
+element.setAttribute("some-attribute", "some value");
+```
+
+### `elAttrBool(element: Element, attribute: string): boolean`
+
+Reads an attribute and converts it value into a boolean value, the strings `"1"`
+and `"true"` will evaluate to `true`. All other values, including a missing attribute,
+will return `false`.
+
+```js
+if (elAttrBool(element, "some-attribute")) {
+ // attribute is true-ish
+}
+```
+
+### `elData(element: Element, attribute: string, value?: string): string`
+
+Short-hand function to read or set HTML5 `data-*`-attributes, it essentially
+prepends the `data-` prefix before forwarding the call to `elAttr()`.
+
+```js
+var value = elData(element, "some-attribute");
+// equals
+var value = elAttr(element, "data-some-attribute");
+
+elData(element, "some-attribute", "some value");
+// equals
+elAttr(element, "data-some-attribute", "some value");
+```
+
+### `elDataBool(element: Element, attribute: string): boolean`
+
+Short-hand function to convert a HTML5 `data-*`-attribute into a boolean value. It
+prepends the `data-` prefix before forwarding the call to `elAttrBool()`.
+
+```js
+if (elDataBool(element, "some-attribute")) {
+ // attribute is true-ish
+}
+// equals
+if (elAttrBool(element, "data-some-attribute")) {
+ // attribute is true-ish
+}
+```
+
+## Selecting Elements
+
+{% include callout.html content="Unlike libraries like jQuery, these functions will return `null` if an element is not found. You are responsible to validate if the element exist and to branch accordingly, invoking methods on the return value without checking for `null` will yield an error." type="warning" %}
+
+### `elById(id: string): Element | null`
+
+Selects an element by its `id`-attribute value.
+
+```js
+var element = elById("my-awesome-element");
+// equals
+var element = document.getElementById("my-awesome-element");
+```
+
+### `elBySel(selector: string, context?: Element): Element | null`
+
+{% include callout.html content="The underlying `querySelector()`-method works on the entire DOM hierarchy and can yield results outside of your context element! Please read and understand the MDN article on [`Element.querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector#The_entire_hierarchy_counts) to learn more about this." type="danger" %}
+
+Select a single element based on a CSS selector, optionally limiting the results
+to be a direct or indirect children of the context element.
+
+```js
+var element = elBySel(".some-element");
+// equals
+var element = document.querySelector(".some-element");
+
+// limiting the scope to a context element:
+var element = elBySel(".some-element", context);
+// equals
+var element = context.querySelector(".some-element");
+```
+
+### `elBySelAll(selector: string, context?: Element, callback: (element: Element) => void): NodeList`
+
+{% include callout.html content="The underlying `querySelector()`-method works on the entire DOM hierarchy and can yield results outside of your context element! Please read and understand the MDN article on [`Element.querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector#The_entire_hierarchy_counts) to learn more about this." type="danger" %}
+
+Finds and returns a `NodeList` containing all elements that match the provided
+CSS selector. Although `NodeList` is an array-like structure, it is not possible
+to iterate over it using array functions, including `.forEach()` which is not
+available in Internet Explorer 11.
+
+```js
+var elements = elBySelAll(".some-element");
+// equals
+var elements = document.querySelectorAll(".some-element");
+
+// limiting the scope to a context element:
+var elements = elBySelAll(".some-element", context);
+// equals
+var elements = context.querySelectorAll(".some-element");
+```
+
+#### Callback to Iterate Over Elements
+
+`elBySelAll()` supports an optional third parameter that expects a callback function
+that is invoked for every element in the list.
+
+```js
+// set the 2nd parameter to `undefined` or `null` to query the whole document
+elBySelAll(".some-element", undefined, function(element) {
+ // is called for each element
+});
+
+// limiting the scope to a context element:
+elBySelAll(".some-element", context, function(element) {
+ // is called for each element
+});
+```
+
+### `elClosest(element: Element, selector: string): Element | null`
+
+Returns the first `Element` that matches the provided CSS selector, this will
+return the provided element itself if it matches the selector.
+
+```js
+var element = elClosest(context, ".some-element");
+// equals
+var element = context.closest(".some-element");
+```
+
+#### Text Nodes
+
+If the provided context is a `Text`-node, the function will move the context to
+the parent element before applying the CSS selector. If the `Text` has no parent,
+`null` is returned without evaluating the selector.
+
+### `elByClass(className: string, context?: Element): NodeList`
+
+Returns a live `NodeList` containing all elements that match the provided CSS
+class now _and_ in the future! The collection is automatically updated whenever
+an element with that class is added or removed from the DOM, it will also include
+elements that get dynamically assigned or removed this CSS class.
+
+You absolutely need to understand that this collection is dynamic, that means that
+elements can and will be added and removed from the collection _even while_ you
+iterate over it. There are only very few cases where you would need such a collection,
+almost always `elBySelAll()` is what you're looking for.
+
+```js
+// no leading dot!
+var elements = elByClass("some-element");
+// equals
+var elements = document.getElementsByClassName("some-element");
+
+// limiting the scope to a context element:
+var elements = elByClass("some-element", context);
+// equals
+var elements = context.getElementsByClassName(".some-element");
+```
+
+### `elByTag(tagName: string, context?: Element): NodeList`
+
+Returns a live `NodeList` containing all elements with the provided tag name now
+_and_ in the future! Please read the remarks on `elByClass()` above to understand
+the implications of this.
+
+```js
+var elements = elByTag("div");
+// equals
+var elements = document.getElementsByTagName("div");
+
+// limiting the scope to a context element:
+var elements = elByTag("div", context);
+// equals
+var elements = context.getElementsByTagName("div");
+```
+
+## Utility Functions
+
+### `elInnerError(element: Element, errorMessage?: string, isHtml?: boolean): Element | null``
+
+Unified function to display and remove inline error messages for input elements,
+please read the [section in the migration docs](migration_wsc-30_javascript.html#helper-function-for-inline-error-messages)
+to learn more about this function.
+
+## String Extensions
+
+### `hashCode(): string`
+
+Computes a numeric hash value of a string similar to Java's `String.hashCode()` method.
+
+```js
+console.log("Hello World".hashCode());
+// outputs: -862545276
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Legacy JavaScript API
+sidebar: sidebar
+permalink: javascript_legacy-api.html
+folder: javascript
+---
+
+## Introduction
+
+The legacy JavaScript API is the original code that was part of the 2.x series
+of WoltLab Suite, formerly known as WoltLab Community Framework. It has been
+superseded for the most part by the [ES5/AMD-modules API][javascript_new-api_writing-a-module]
+introduced with WoltLab Suite 3.0.
+
+Some parts still exist to this day for backwards-compatibility and because some
+less important components have not been rewritten yet. The old API is still
+supported, but marked as deprecated and will continue to be replaced parts by
+part in future releases, up until their entire removal, including jQuery support.
+
+This guide does not provide any explanation on the usage of those legacy components,
+but instead serves as a cheat sheet to convert code to use the new API.
+
+## Classes
+
+### Singletons
+
+Singleton instances are designed to provide a unique "instance" of an object
+regardless of when its first instance was created. Due to the lack of a `class`
+construct in ES5, they are represented by mere objects that act as an instance.
+
+```js
+// App.js
+window.App = {};
+App.Foo = {
+ bar: function() {}
+};
+
+// --- NEW API ---
+
+// App/Foo.js
+define([], function() {
+ "use strict";
+
+ return {
+ bar: function() {}
+ };
+});
+```
+
+### Regular Classes
+
+```js
+// App.js
+window.App = {};
+App.Foo = Class.extend({
+ bar: function() {}
+});
+
+// --- NEW API ---
+
+// App/Foo.js
+define([], function() {
+ "use strict";
+
+ function Foo() {};
+ Foo.prototype = {
+ bar: function() {}
+ };
+
+ return Foo;
+});
+```
+
+#### Inheritance
+
+```js
+// App.js
+window.App = {};
+App.Foo = Class.extend({
+ bar: function() {}
+});
+App.Baz = App.Foo.extend({
+ makeSnafucated: function() {}
+});
+
+// --- NEW API ---
+
+// App/Foo.js
+define([], function() {
+ "use strict";
+
+ function Foo() {};
+ Foo.prototype = {
+ bar: function() {}
+ };
+
+ return Foo;
+});
+
+// App/Baz.js
+define(["Core", "./Foo"], function(Core, Foo) {
+ "use strict";
+
+ function Baz() {};
+ Core.inherit(Baz, Foo, {
+ makeSnafucated: function() {}
+ });
+
+ return Baz;
+});
+```
+
+## Ajax Requests
+
+```js
+// App.js
+App.Foo = Class.extend({
+ _proxy: null,
+
+ init: function() {
+ this._proxy = new WCF.Action.Proxy({
+ success: $.proxy(this._success, this)
+ });
+ },
+
+ bar: function() {
+ this._proxy.setOption("data", {
+ actionName: "baz",
+ className: "app\\foo\\FooAction",
+ objectIDs: [1, 2, 3],
+ parameters: {
+ foo: "bar",
+ baz: true
+ }
+ });
+ this._proxy.sendRequest();
+ },
+
+ _success: function(data) {
+ // ajax request result
+ }
+});
+
+// --- NEW API ---
+
+// App/Foo.js
+define(["Ajax"], function(Ajax) {
+ "use strict";
+
+ function Foo() {}
+ Foo.prototype = {
+ bar: function() {
+ Ajax.api(this, {
+ objectIDs: [1, 2, 3],
+ parameters: {
+ foo: "bar",
+ baz: true
+ }
+ });
+ },
+
+ // magic method!
+ _ajaxSuccess: function(data) {
+ // ajax request result
+ },
+
+ // magic method!
+ _ajaxSetup: function() {
+ return {
+ actionName: "baz",
+ className: "app\\foo\\FooAction"
+ }
+ }
+ }
+
+ return Foo;
+});
+```
+
+## Phrases
+
+```html
+<script data-relocate="true">
+$(function() {
+ WCF.Language.addObject({
+ 'app.foo.bar': '{lang}app.foo.bar{/lang}'
+ });
+
+ console.log(WCF.Language.get("app.foo.bar"));
+});
+</script>
+
+<!-- NEW API -->
+
+<script data-relocate="true">
+require(["Language"], function(Language) {
+ Language.addObject({
+ 'app.foo.bar': '{jslang}app.foo.bar{/jslang}'
+ });
+
+ console.log(Language.get("app.foo.bar"));
+});
+</script>
+```
+
+## Event-Listener
+
+```html
+<script data-relocate="true">
+$(function() {
+ WCF.System.Event.addListener("app.foo.bar", "makeSnafucated", function(data) {
+ console.log("Event was invoked.");
+ });
+
+ WCF.System.Event.fireEvent("app.foo.bar", "makeSnafucated", { some: "data" });
+});
+</script>
+
+<!-- NEW API -->
+
+<script data-relocate="true">
+require(["EventHandler"], function(EventHandler) {
+ EventHandler.add("app.foo.bar", "makeSnafucated", function(data) {
+ console.log("Event was invoked");
+ });
+
+ EventHandler.fire("app.foo.bar", "makeSnafucated", { some: "data" });
+});
+</script>
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Ajax Requests - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_ajax.html
+folder: javascript
+---
+
+## Ajax inside Modules
+
+The Ajax component was designed to be used from inside modules where an object
+reference is used to delegate request callbacks. This is acomplished through
+a set of magic methods that are automatically called when the request is created
+or its state has changed.
+
+### `_ajaxSetup()`
+
+The lazy initialization is performed upon the first invocation from the callee,
+using the magic `_ajaxSetup()` method to retrieve the basic configuration for
+this and any future requests.
+
+The data returned by `_ajaxSetup()` is cached and the data will be used to
+pre-populate the request data before sending it. The callee can overwrite any of
+these properties. It is intended to reduce the overhead when issuing request
+when these requests share the same properties, such as accessing the same endpoint.
+
+```js
+// App/Foo.js
+define(["Ajax"], function(Ajax) {
+ "use strict";
+
+ function Foo() {};
+ Foo.prototype = {
+ one: function() {
+ // this will issue an ajax request with the parameter `value` set to `1`
+ Ajax.api(this);
+ },
+
+ two: function() {
+ // this request is almost identical to the one issued with `.one()`, but
+ // the value is now set to `2` for this invocation only.
+ Ajax.api(this, {
+ parameters: {
+ value: 2
+ }
+ });
+ },
+
+ _ajaxSetup: function() {
+ return {
+ data: {
+ actionName: "makeSnafucated",
+ className: "app\\data\\foo\\FooAction",
+ parameters: {
+ value: 1
+ }
+ }
+ }
+ }
+ };
+
+ return Foo;
+});
+```
+
+### Request Settings
+
+The object returned by the aforementioned `_ajaxSetup()` callback can contain these
+values:
+
+#### `data`
+
+_Defaults to `{}`._
+
+A plain JavaScript object that contains the request data that represents the form
+data of the request. The `parameters` key is recognized by the PHP Ajax API and
+becomes accessible through `$this->parameters`.
+
+#### `contentType`
+
+_Defaults to `application/x-www-form-urlencoded; charset=UTF-8`._
+
+The request content type, sets the `Content-Type` HTTP header if it is not empty.
+
+#### `responseType`
+
+_Defaults to `application/json`._
+
+The server must respond with the `Content-Type` HTTP header set to this value,
+otherwise the request will be treated as failed. Requests for `application/json`
+will have the return body attempted to be evaluated as JSON.
+
+Other content types will only be validated based on the HTTP header, but no
+additional transformation is performed. For example, setting the `responseType`
+to `application/xml` will check the HTTP header, but will not transform the
+`data` parameter, you'll still receive a string in `_ajaxSuccess`!
+
+#### `type`
+
+_Defaults to `POST`._
+
+The HTTP Verb used for this request.
+
+#### `url`
+
+_Defaults to an empty string._
+
+Manual override for the request endpoint, it will be automatically set to the
+Core API endpoint if left empty. If the Core API endpoint is used, the options
+`includeRequestedWith` and `withCredentials` will be force-set to true.
+
+#### `withCredentials`
+
+{% include callout.html content="Enabling this parameter for any domain other than the current will trigger a CORS preflight request." type="warning" %}
+
+_Defaults to `false`._
+
+Include cookies with this requested, is always true when `url` is (implicitly)
+set to the Core API endpoint.
+
+#### `autoAbort`
+
+_Defaults to `false`._
+
+When set to `true`, any pending responses to earlier requests will be silently
+discarded when issuing a new request. This only makes sense if the new request
+is meant to completely replace the result of the previous one, regardless of its
+reponse body.
+
+Typical use-cases include input field with suggestions, where possible values
+are requested from the server, but the input changed faster than the server was
+able to reply. In this particular case the client is not interested in the result
+for an earlier value, auto-aborting these requests avoids implementing this logic
+in the requesting code.
+
+#### `ignoreError`
+
+_Defaults to `false`._
+
+Any failing request will invoke the `failure`-callback to check if an error
+message should be displayed. Enabling this option will suppress the general
+error overlay that reports a failed request.
+
+You can achieve the same result by returning `false` in the `failure`-callback.
+
+#### `silent`
+
+_Defaults to `false`._
+
+Enabling this option will suppress the loading indicator overlay for this request,
+other non-"silent" requests will still trigger the loading indicator.
+
+#### `includeRequestedWith`
+
+{% include callout.html content="Enabling this parameter for any domain other than the current will trigger a CORS preflight request." type="warning" %}
+
+_Defaults to `true`._
+
+Sets the custom HTTP header `X-Requested-With: XMLHttpRequest` for the request,
+it is automatically set to `true` when `url` is pointing at the WSC API endpoint.
+
+#### `failure`
+
+_Defaults to `null`._
+
+Optional callback function that will be invoked for requests that have failed
+for one of these reasons:
+ 1. The request timed out.
+ 2. The HTTP status is not `2xx` or `304`.
+ 3. A `responseType` was set, but the response HTTP header `Content-Type` did not match the expected value.
+ 4. The `responseType` was set to `application/json`, but the response body was not valid JSON.
+
+The callback function receives the parameter `xhr` (the `XMLHttpRequest` object)
+and `options` (deep clone of the request parameters). If the callback returns
+`false`, the general error overlay for failed requests will be suppressed.
+
+There will be no error overlay if `ignoreError` is set to `true` or if the
+request failed while attempting to evaluate the response body as JSON.
+
+#### `finalize`
+
+_Defaults to `null`._
+
+Optional callback function that will be invoked once the request has completed,
+regardless if it succeeded or failed. The only parameter it receives is
+`options` (the request parameters object), but it does not receive the request's
+`XMLHttpRequest`.
+
+#### `success`
+
+_Defaults to `null`._
+
+This semi-optional callback function will always be set to `_ajaxSuccess()` when
+invoking `Ajax.api()`. It receives four parameters:
+ 1. `data` - The request's response body as a string, or a JavaScript object if
+ `contentType` was set to `application/json`.
+ 2. `responseText` - The unmodified response body, it equals the value for `data`
+ for non-JSON requests.
+ 3. `xhr` - The underlying `XMLHttpRequest` object.
+ 4. `requestData` - The request parameters that were supplied when the request
+ was issued.
+
+### `_ajaxSuccess()`
+
+This callback method is automatically called for successful AJAX requests, it
+receives four parameters, with the first one containing either the response body
+as a string, or a JavaScript object for JSON requests.
+
+### `_ajaxFailure()`
+
+Optional callback function that is invoked for failed requests, it will be
+automatically called if the callee implements it, otherwise the global error
+handler will be executed.
+
+## Single Requests Without a Module
+
+The `Ajax.api()` method expects an object that is used to extract the request
+configuration as well as providing the callback functions when the request state
+changes.
+
+You can issue a simple Ajax request without object binding through `Ajax.apiOnce()`
+that will destroy the instance after the request was finalized. This method is
+significantly more expensive for repeated requests and does not offer deriving
+modules from altering the behavior. It is strongly recommended to always use
+`Ajax.api()` for requests to the WSC API endpoint.
+
+```html
+<script data-relocate="true">
+ require(["Ajax"], function(Ajax) {
+ Ajax.apiOnce({
+ data: {
+ actionName: "makeSnafucated",
+ className: "app\\data\\foo\\FooAction",
+ parameters: {
+ value: 3
+ }
+ },
+ success: function(data) {
+ elBySel(".some-element").textContent = data.bar;
+ }
+ })
+ });
+</script>
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Browser and Screen Sizes - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_browser.html
+folder: javascript
+---
+
+## `Ui/Screen`
+
+CSS offers powerful media queries that alter the layout depending on the screen
+sizes, including but not limited to changes between landscape and portrait mode
+on mobile devices.
+
+The `Ui/Screen` module exposes a consistent interface to execute JavaScript code
+based on the same media queries that are available in the CSS code already. It
+features support for unmatching and executing code when a rule matches for the
+first time during the page lifecycle.
+
+### Supported Aliases
+
+You can pass in custom media queries, but it is strongly recommended to use the
+built-in media queries that match the same dimensions as your CSS.
+
+| Alias | Media Query |
+|---|---|
+| `screen-xs` | `(max-width: 544px)` |
+| `screen-sm` | `(min-width: 545px) and (max-width: 768px)` |
+| `screen-sm-down` | `(max-width: 768px)` |
+| `screen-sm-up` | `(min-width: 545px)` |
+| `screen-sm-md` | `(min-width: 545px) and (max-width: 1024px)` |
+| `screen-md` | `(min-width: 769px) and (max-width: 1024px)` |
+| `screen-md-down` | `(max-width: 1024px)` |
+| `screen-md-up` | `(min-width: 769px)` |
+| `screen-lg` | `(min-width: 1025px)`
+
+### `on(query: string, callbacks: Object): string`
+
+Registers a set of callback functions for the provided media query, the possible
+keys are `match`, `unmatch` and `setup`. The method returns a randomly generated
+UUIDv4 that is used to identify these callbacks and allows them to be removed
+via `.remove()`.
+
+### `remove(query: string, uuid: string)`
+
+Removes all callbacks for a media query that match the UUIDv4 that was previously
+obtained from the call to `.on()`.
+
+### `is(query: string): boolean`
+
+Tests if the provided media query currently matches and returns true on match.
+
+### `scrollDisable()`
+
+Temporarily prevents the page from being scrolled, until `.scrollEnable()` is
+called.
+
+### `scrollEnable()`
+
+Enables page scrolling again, unless another pending action has also prevented
+the page scrolling.
+
+## `Environment`
+
+{% include callout.html content="The `Environment` module uses a mixture of feature detection and user agent sniffing to determine the browser and platform. In general, its results have proven to be very accurate, but it should be taken with a grain of salt regardless. Especially the browser checks are designed to be your last resort, please use feature detection instead whenever it is possible!" type="warning" %}
+
+Sometimes it may be necessary to alter the behavior of your code depending on
+the browser platform (e. g. mobile devices) or based on a specific browser in
+order to work-around some quirks.
+
+### `browser(): string`
+
+Attempts to detect browsers based on their technology and supported CSS vendor
+prefixes, and although somewhat reliable for major browsers, it is highly
+recommended to use feature detection instead.
+
+Possible values:
+ - `chrome` (includes Opera 15+ and Vivaldi)
+ - `firefox`
+ - `safari`
+ - `microsoft` (Internet Explorer and Edge)
+ - `other` (default)
+
+### `platform(): string`
+
+Attempts to detect the browser platform using user agent sniffing.
+
+Possible values:
+ - `ios`
+ - `android`
+ - `windows` (IE Mobile)
+ - `mobile` (generic mobile device)
+ - `desktop` (default)
+
+{% include links.html %}
--- /dev/null
+---
+title: Core Modules and Functions - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_core.html
+folder: javascript
+---
+
+A brief overview of common methods that may be useful when writing any module.
+
+## `Core`
+
+### `clone(object: Object): Object`
+
+Creates a deep-clone of the provided object by value, removing any references on
+the original element, including arrays. However, this does not clone references
+to non-plain objects, these instances will be copied by reference.
+
+```js
+require(["Core"], function(Core) {
+ var obj1 = { a: 1 };
+ var obj2 = Core.clone(obj1);
+
+ console.log(obj1 === obj2); // output: false
+ console.log(obj2.hasOwnProperty("a") && obj2.a === 1); // output: true
+});
+```
+
+### `extend(base: Object, ...merge: Object[]): Object`
+
+Accepts an infinite amount of plain objects as parameters, values will be copied
+from the 2nd...nth object into the first object. The first parameter will be
+cloned and the resulting object is returned.
+
+```js
+require(["Core"], function(Core) {
+ var obj1 = { a: 2 };
+ var obj2 = { a: 1, b: 2 };
+ var obj = Core.extend({
+ b: 1
+ }, obj1, obj2);
+
+ console.log(obj.b === 2); // output: true
+ console.log(obj.hasOwnProperty("a") && obj.a === 2); // output: false
+});
+```
+
+### `inherit(base: Object, target: Object, merge?: Object)`
+
+Derives the second object's prototype from the first object, afterwards the
+derived class will pass the `instanceof` check against the original class.
+
+```js
+// App.js
+window.App = {};
+App.Foo = Class.extend({
+ bar: function() {}
+});
+App.Baz = App.Foo.extend({
+ makeSnafucated: function() {}
+});
+
+// --- NEW API ---
+
+// App/Foo.js
+define([], function() {
+ "use strict";
+
+ function Foo() {};
+ Foo.prototype = {
+ bar: function() {}
+ };
+
+ return Foo;
+});
+
+// App/Baz.js
+define(["Core", "./Foo"], function(Core, Foo) {
+ "use strict";
+
+ function Baz() {};
+ Core.inherit(Baz, Foo, {
+ makeSnafucated: function() {}
+ });
+
+ return Baz;
+});
+```
+
+### `isPlainObject(object: Object): boolean`
+
+Verifies if an object is a plain JavaScript object and not an object instance.
+
+```js
+require(["Core"], function(Core) {
+ function Foo() {}
+ Foo.prototype = {
+ hello: "world";
+ };
+
+ var obj1 = { hello: "world" };
+ var obj2 = new Foo();
+
+ console.log(Core.isPlainObject(obj1)); // output: true
+ console.log(obj1.hello === obj2.hello); // output: true
+ console.log(Core.isPlainObject(obj2)); // output: false
+});
+```
+
+### `triggerEvent(element: Element, eventName: string)`
+
+Creates and dispatches a synthetic JavaScript event on an element.
+
+```js
+require(["Core"], function(Core) {
+ var element = elBySel(".some-element");
+ Core.triggerEvent(element, "click");
+});
+```
+
+## `Language`
+
+### `add(key: string, value: string)`
+
+Registers a new phrase.
+
+```html
+<script data-relocate="true">
+ require(["Language"], function(Language) {
+ Language.add('app.foo.bar', '{jslang}app.foo.bar{/jslang}');
+ });
+</script>
+```
+
+### `addObject(object: Object)`
+
+Registers a list of phrases using a plain object.
+
+```html
+<script data-relocate="true">
+ require(["Language"], function(Language) {
+ Language.addObject({
+ 'app.foo.bar': '{jslang}app.foo.bar{/jslang}'
+ });
+ });
+</script>
+```
+
+### `get(key: string, parameters?: Object): string`
+
+Retrieves a phrase by its key, optionally supporting basic template scripting
+with dynamic variables passed using the `parameters` object.
+
+```js
+require(["Language"], function(Language) {
+ var title = Language.get("app.foo.title");
+ var content = Language.get("app.foo.content", {
+ some: "value"
+ });
+});
+```
+
+## `StringUtil`
+
+### `escapeHTML(str: string): string`
+
+Escapes special HTML characters by converting them into an HTML entity.
+
+| Character | Replacement |
+|---|---|
+| `&` | `&` |
+| `"` | `"` |
+| `<` | `<` |
+| `>` | `>` |
+
+### `escapeRegExp(str: string): string`
+
+Escapes a list of characters that have a special meaning in regular expressions
+and could alter the behavior when embedded into regular expressions.
+
+### `lcfirst(str: string): string`
+
+Makes a string's first character lowercase.
+
+### `ucfirst(str: string): string`
+
+Makes a string's first character uppercase.
+
+### `unescapeHTML(str: string): string`
+
+Converts some HTML entities into their original character. This is the reverse
+function of `escapeHTML()`.
+
+{% include links.html %}
--- /dev/null
+---
+title: Data Structures - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_data-structures.html
+folder: javascript
+---
+
+## Introduction
+
+JavaScript offers only limited types of collections to hold and iterate over
+data. Despite the ongoing efforts in ES6 and newer, these new data structures
+and access methods, such as `for … of`, are not available in the still supported
+Internet Explorer 11.
+
+## `Dictionary`
+
+Represents a simple key-value map, but unlike the use of plain objects, will
+always to guarantee to iterate over directly set values only.
+
+_In supported browsers this will use a native `Map` internally, otherwise a plain object._
+
+### `set(key: string, value: any)`
+
+Adds or updates an item using the provided key. Numeric keys will be converted
+into strings.
+
+### `delete(key: string)`
+
+Removes an item from the collection.
+
+### `has(key: string): boolean`
+
+Returns true if the key is contained in the collection.
+
+### `get(key: string): any`
+
+Returns the value for the provided key, or `undefined` if the key was not found.
+Use `.has()` to check for key existence.
+
+### `forEach(callback: (value: any, key: string) => void)`
+
+Iterates over all items in the collection in an arbitrary order and invokes the
+supplied callback with the value and the key.
+
+### `size: number`
+
+This read-only property counts the number of items in the collection.
+
+## `List`
+
+Represents a list of unique values.
+
+_In supported browsers this will use a native `Set` internally, otherwise an array._
+
+### `add(value: any)`
+
+Adds a value to the list. If the value is already part of the list, this method
+will silently abort.
+
+### `clear()`
+
+Resets the collection.
+
+### `delete(value: any): boolean`
+
+Attempts to remove a value from the list, it returns true if the value has been
+part of the list.
+
+### `forEach(callback: (value: any) => void)`
+
+Iterates over all values in the list in an arbitrary order and invokes the
+supplied callback for each value.
+
+### `has(value: any): boolean`
+
+Returns true if the provided value is part of this list.
+
+### `size: number`
+
+This read-only property counts the number of items in the list.
+
+## `ObjectMap`
+
+{% include callout.html content="This class uses a `WeakMap` internally, the keys are only weakly referenced and do not prevent garbage collection." type="info" %}
+
+Represents a collection where any kind of objects, such as class instances or
+DOM elements, can be used as key. These keys are weakly referenced and will not
+prevent garbage collection from happening, but this also means that it is not
+possible to enumerate or iterate over the stored keys and values.
+
+This class is especially useful when you want to store additional data for
+objects that may get disposed on runtime, such as DOM elements. Using any regular
+data collections will cause the object to be referenced indefinitely, preventing
+the garbage collection from removing orphaned objects.
+
+### `set(key: Object, value: Object)`
+
+Adds the key with the provided value to the map, if the key was already part
+of the collection, its value is overwritten.
+
+### `delete(key: Object)`
+
+Attempts to remove a key from the collection. The method will abort silently if
+the key is not part of the collection.
+
+### `has(key: Object): boolean`
+
+Returns true if there is a value for the provided key in this collection.
+
+### `get(key: Object): Object | undefined`
+
+Retrieves the value of the provided key, or `undefined` if the key was not found.
+
+{% include links.html %}
--- /dev/null
+---
+title: Dialogs - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_dialogs.html
+folder: javascript
+---
+
+## Introduction
+
+Dialogs are full screen overlays that cover the currently visible window area
+using a semi-opague backdrop and a prominently placed dialog window in the
+foreground. They shift the attention away from the original content towards the
+dialog and usually contain additional details and/or dedicated form inputs.
+
+## `_dialogSetup()`
+
+The lazy initialization is performed upon the first invocation from the callee,
+using the magic `_dialogSetup()` method to retrieve the basic configuration for
+the dialog construction and any event callbacks.
+
+```js
+// App/Foo.js
+define(["Ui/Dialog"], function(UiDialog) {
+ "use strict";
+
+ function Foo() {};
+ Foo.prototype = {
+ bar: function() {
+ // this will open the dialog constructed by _dialogSetup
+ UiDialog.open(this);
+ },
+
+ _dialogSetup: function() {
+ return {
+ id: "myDialog",
+ source: "<p>Hello World!</p>",
+ options: {
+ onClose: function() {
+ // the fancy dialog was closed!
+ }
+ }
+ }
+ }
+ };
+
+ return Foo;
+});
+```
+
+### `id: string`
+
+The `id` is used to identify a dialog on runtime, but is also part of the first-
+time setup when the dialog has not been opened before. If `source` is `undefined`,
+the module attempts to construct the dialog using an element with the same id.
+
+### `source: any`
+
+There are six different types of value that `source` does allow and each of them
+changes how the initial dialog is constructed:
+
+1. `undefined`
+ The dialog exists already and the value of `id` should be used to identify the
+ element.
+2. `null`
+ The HTML is provided using the second argument of `.open()`.
+3. `() => void`
+ If the `source` is a function, it is executed and is expected to start the
+ dialog initialization itself.
+4. `Object`
+ Plain objects are interpreted as parameters for an Ajax request, in particular
+ `source.data` will be used to issue the request. It is possible to specify the
+ key `source.after` as a callback `(content: Element, responseData: Object) => void`
+ that is executed after the dialog was opened.
+5. `string`
+ The string is expected to be plain HTML that should be used to construct the
+ dialog.
+6. `DocumentFragment`
+ A new container `<div>` with the provided `id` is created and the contents of
+ the `DocumentFragment` is appended to it. This container is then used for the
+ dialog.
+
+### `options: Object`
+
+All configuration options and callbacks are handled through this object.
+
+#### `options.backdropCloseOnClick: boolean`
+
+_Defaults to `true`._
+
+Clicks on the dialog backdrop will close the top-most dialog. This option will
+be force-disabled if the option `closeable` is set to `false`.
+
+#### `options.closable: boolean`
+
+_Defaults to `true`._
+
+Enables the close button in the dialog title, when disabled the dialog can be
+closed through the `.close()` API call only.
+
+#### `options.closeButtonLabel: string`
+
+_Defaults to `Language.get("wcf.global.button.close")`._
+
+The phrase that is displayed in the tooltip for the close button.
+
+#### `options.closeConfirmMessage: string`
+
+_Defaults to `""`._
+
+Shows a [confirmation dialog][javascript_new-api_ui] using the configured message
+before closing the dialog. The dialog will not be closed if the dialog is
+rejected by the user.
+
+#### `options.title: string`
+
+_Defaults to `""`._
+
+The phrase that is displayed in the dialog title.
+
+#### `options.onBeforeClose: (id: string) => void`
+
+_Defaults to `null`._
+
+The callback is executed when the user clicks on the close button or, if enabled,
+on the backdrop. The callback is responsible to close the dialog by itself, the
+default close behavior is automatically prevented.
+
+#### `options.onClose: (id: string) => void`
+
+_Defaults to `null`._
+
+The callback is notified once the dialog is about to be closed, but is still
+visible at this point. It is not possible to abort the close operation at this
+point.
+
+#### `options.onShow: (content: Element) => void`
+
+_Defaults to `null`._
+
+Receives the dialog content element as its only argument, allowing the callback
+to modify the DOM or to register event listeners before the dialog is presented
+to the user. The dialog is already visible at call time, but the dialog has not
+been finalized yet.
+
+## `setTitle(id: string | Object, title: string)`
+
+Sets the title of a dialog.
+
+## `setCallback(id: string | Object, key: string, value: (data: any) => void | null)`
+
+Sets a callback function after the dialog initialization, the special value
+`null` will remove a previously set callback. Valid values for `key` are
+`onBeforeClose`, `onClose` and `onShow`.
+
+## `rebuild(id: string | Object)`
+
+Rebuilds a dialog by performing various calculations on the maximum dialog
+height in regards to the overflow handling and adjustments for embedded forms.
+This method is automatically invoked whenever a dialog is shown, after invoking
+the `options.onShow` callback.
+
+## `close(id: string | Object)`
+
+Closes an open dialog, this will neither trigger a confirmation dialog, nor does
+it invoke the `options.onBeforeClose` callback. The `options.onClose` callback
+will always be invoked, but it cannot abort the close operation.
+
+## `getDialog(id: string | Object): Object`
+
+{% include callout.html content="This method returns an internal data object by reference, any modifications made do have an effect on the dialogs behavior and in particular no validation is performed on the modification. It is strongly recommended to use the `.set*()` methods only." type="warning" %}
+
+Returns the internal dialog data that is attached to a dialog. The most important
+key is `.content` which holds a reference to the dialog's inner content element.
+
+## `isOpen(id: string | Object): boolean`
+
+Returns true if the dialog exists and is open.
+
+{% include links.html %}
--- /dev/null
+---
+title: Working with the DOM - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_dom.html
+folder: javascript
+---
+
+## Helper Functions
+
+There is large set of [helper functions][javascript_helper-functions] that assist
+you when working with the DOM tree and its elements. These functions are globally
+available and do not require explicit module imports.
+
+## `Dom/Util`
+
+### `createFragmentFromHtml(html: string): DocumentFragment`
+
+Parses a HTML string and creates a `DocumentFragment` object that holds the
+resulting nodes.
+
+### `identify(element: Element): string`
+
+Retrieves the unique identifier (`id`) of an element. If it does not currently
+have an id assigned, a generic identifier is used instead.
+
+### `outerHeight(element: Element, styles?: CSSStyleDeclaration): number`
+
+Computes the outer height of an element using the element's `offsetHeight` and
+the sum of the rounded down values for `margin-top` and `margin-bottom`.
+
+### `outerWidth(element: Element, styles?: CSSStyleDeclaration): number`
+
+Computes the outer width of an element using the element's `offsetWidth` and
+the sum of the rounded down values for `margin-left` and `margin-right`.
+
+### `outerDimensions(element: Element): { height: number, width: number }`
+
+Computes the outer dimensions of an element including its margins.
+
+### `offset(element: Element): { top: number, left: number }`
+
+Computes the element's offset relative to the top left corner of the document.
+
+### `setInnerHtml(element: Element, innerHtml: string)`
+
+Sets the inner HTML of an element via `element.innerHTML = innerHtml`. Browsers
+do not evaluate any embedded `<script>` tags, therefore this method extracts each
+of them, creates new `<script>` tags and inserts them in their original order of
+appearance.
+
+### `contains(element: Element, child: Element): boolean`
+
+Evaluates if `element` is a direct or indirect parent element of `child`.
+
+### `unwrapChildNodes(element: Element)`
+
+Moves all child nodes out of `element` while maintaining their order, then removes
+`element` from the document.
+
+## `Dom/ChangeListener`
+
+This class is used to observe specific changes to the DOM, for example after an
+Ajax request has completed. For performance reasons this is a manually-invoked
+listener that does not rely on a `MutationObserver`.
+
+```js
+require(["Dom/ChangeListener"], function(DomChangeListener) {
+ DomChangeListener.add("App/Foo", function() {
+ // the DOM may have been altered significantly
+ });
+
+ // propagate changes to the DOM
+ DomChangeListener.trigger();
+});
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Event Handling - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_events.html
+folder: javascript
+---
+
+## `EventKey`
+
+This class offers a set of static methods that can be used to determine if some
+common keys are being pressed. Internally it compares either the `.key` property
+if it is supported or the value of `.which`.
+
+```js
+require(["EventKey"], function(EventKey) {
+ elBySel(".some-input").addEventListener("keydown", function(event) {
+ if (EventKey.Enter(event)) {
+ // the `Enter` key was pressed
+ }
+ });
+});
+```
+
+### `ArrowDown(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `↓` key.
+
+### `ArrowLeft(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `←` key.
+
+### `ArrowRight(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `→` key.
+
+### `ArrowUp(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `↑` key.
+
+### `Comma(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `,` key.
+
+### `Enter(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `↲` key.
+
+### `Escape(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `Esc` key.
+
+### `Tab(event: KeyboardEvent): boolean`
+
+Returns true if the user has pressed the `↹` key.
+
+## `EventHandler`
+
+A synchronous event system based on string identifiers rather than DOM elements,
+similar to the PHP event system in WoltLab Suite. Any components can listen to
+events or trigger events itself at any time.
+
+### Identifiying Events with the Developer Tools
+
+The Developer Tools in WoltLab Suite 3.1 offer an easy option to identify existing
+events that are fired while code is being executed. You can enable this watch
+mode through your browser's console using `Devtools.toggleEventLogging()`:
+
+```
+> Devtools.toggleEventLogging();
+< Event logging enabled
+< [Devtools.EventLogging] Firing event: bar @ com.example.app.foo
+< [Devtools.EventLogging] Firing event: baz @ com.example.app.foo
+```
+
+### `add(identifier: string, action: string, callback: (data: Object) => void): string`
+
+Adding an event listeners returns a randomly generated UUIDv4 that is used to
+identify the listener. This UUID is required to remove a specific listener through
+the `remove()` method.
+
+### `fire(identifier: string, action: string, data?: Object)`
+
+Triggers an event using an optional `data` object that is passed to each listener
+by reference.
+
+### `remove(identifier: string, action: string, uuid: string)`
+
+Removes a previously registered event listener using the UUID returned by `add()`.
+
+### `removeAll(identifier: string, action: string)`
+
+Removes all event listeners registered for the provided `identifier` and `action`.
+
+### `removeAllBySuffix(identifier: string, suffix: string)`
+
+Removes all event listeners for an `identifier` whose action ends with the value
+of `suffix`.
+
+{% include links.html %}
--- /dev/null
+---
+title: User Interface - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_ui.html
+folder: javascript
+---
+
+## `Ui/Alignment`
+
+Calculates the alignment of one element relative to another element, with support
+for boundary constraints, alignment restrictions and additional pointer elements.
+
+### `set(element: Element, referenceElement: Element, options: Object)`
+
+Calculates and sets the alignment of the element `element`.
+
+#### `verticalOffset: number`
+
+_Defaults to `0`._
+
+Creates a gap between the element and the reference element, in pixels.
+
+#### `pointer: boolean`
+
+_Defaults to `false`._
+
+Sets the position of the pointer element, requires an existing child of the
+element with the CSS class `.elementPointer`.
+
+#### `pointerOffset: number`
+
+_Defaults to `4`._
+
+The margin from the left/right edge of the element and is used to avoid the
+arrow from being placed right at the edge.
+
+Does not apply when aligning the element to the reference elemnent's center.
+
+#### `pointerClassNames: string[]`
+
+_Defaults to `[]`._
+
+If your element uses CSS-only pointers, such as using the `::before` or `::after`
+pseudo selectors, you can specifiy two separate CSS class names that control the
+alignment:
+
+- `pointerClassNames[0]` is applied to the element when the pointer is displayed
+ at the bottom.
+- `pointerClassNames[1]` is used to align the pointer to the right side of the
+ element.
+
+#### `refDimensionsElement: Element`
+
+_Defaults to `null`._
+
+An alternative element that will be used to determine the position and dimensions
+of the reference element. This can be useful if you reference element is contained
+in a wrapper element with alternating dimensions.
+
+#### `horizontal: string`
+
+{% include callout.html content="This value is automatically flipped for RTL (right-to-left) languages, `left` is changed into `right` and vice versa." type="info" %}
+
+_Defaults to `"left"`._
+
+Sets the prefered alignment, accepts either `left` or `right`. The value `left`
+instructs the module to align the element with the left boundary of the reference
+element.
+
+The `horizontal` alignment is used as the default and a flip only occurs, if there
+is not enough space in the desired direction. If the element exceeds the boundaries
+in both directions, the value of `horizontal` is used.
+
+#### `vertical: string`
+
+_Defaults to `"bottom"`._
+
+Sets the prefered alignment, accepts either `bottom` or `top`. The value `bottom`
+instructs the module to align the element below the reference element.
+
+The `vertical` alignment is used as the default and a flip only occurs, if there
+is not enough space in the desired direction. If the element exceeds the boundaries
+in both directions, the value of `vertical` is used.
+
+#### `allowFlip: string`
+
+{% include callout.html content="The value for `horizontal` is automatically flipped for RTL (right-to-left) languages, `left` is changed into `right` and vice versa. This setting only controls the behavior when violating space constraints, therefore the aforementioned transformation is always applied." type="info" %}
+
+_Defaults to `"both"`._
+
+Restricts the automatic alignment flipping if the element exceeds the window
+boundaries in the instructed direction.
+
+- `both` - No restrictions.
+- `horizontal` - Element can be aligned with the left _or_ the right boundary of
+ the reference element, but the vertical position is fixed.
+- `vertical` - Element can be aligned below _or_ above the reference element,
+ but the vertical position is fixed.
+- `none` - No flipping can occur, the element will be aligned regardless of
+ any space constraints.
+
+## `Ui/CloseOverlay`
+
+Register elements that should be closed when the user clicks anywhere else, such
+as drop-down menus or tooltips.
+
+```js
+require(["Ui/CloseOverlay"], function(UiCloseOverlay) {
+ UiCloseOverlay.add("App/Foo", function() {
+ // invoked, close something
+ });
+});
+```
+
+### `add(identifier: string, callback: () => void)`
+
+Adds a callback that will be invoked when the user clicks anywhere else.
+
+## `Ui/Confirmation`
+
+Prompt the user to make a decision before carrying out an action, such as a safety
+warning before permanently deleting content.
+
+```js
+require(["Ui/Confirmation"], function(UiConfirmation) {
+ UiConfirmation.show({
+ confirm: function() {
+ // the user has confirmed the dialog
+ },
+ message: "Do you really want to continue?"
+ });
+});
+```
+
+### `show(options: Object)`
+
+Displays a dialog overlay with actions buttons to confirm or reject the dialog.
+
+#### `cancel: (parameters: Object) => void`
+
+_Defaults to `null`._
+
+Callback that is invoked when the dialog was rejected.
+
+#### `confirm: (parameters: Object) => void`
+
+_Defaults to `null`._
+
+Callback that is invoked when the user has confirmed the dialog.
+
+#### `message: string`
+
+_Defaults to '""'._
+
+Text that is displayed in the content area of the dialog, optionally this can
+be HTML, but this requires `messageIsHtml` to be enabled.
+
+#### `messageIsHtml`
+
+_Defaults to `false`._
+
+The `message` option is interpreted as text-only, setting this option to `true`
+will cause the `message` to be evaluated as HTML.
+
+#### `parameters: Object`
+
+Optional list of parameter options that will be passed to the `cancel()` and
+`confirm()` callbacks.
+
+#### `template: string`
+
+An optional HTML template that will be inserted into the dialog content area,
+but after the `message` section.
+
+## `Ui/Notification`
+
+Displays a simple notification at the very top of the window, such as a success
+message for Ajax based actions.
+
+```js
+require(["Ui/Notification"], function(UiNotification) {
+ UiNotification.show(
+ "Your changes have been saved.",
+ function() {
+ // this callback will be invoked after 2 seconds
+ },
+ "success"
+ );
+});
+```
+
+### `show(message: string, callback?: () => void, cssClassName?: string)`
+
+Shows the notification and executes the callback after 2 seconds.
+
+{% include links.html %}
--- /dev/null
+---
+title: Writing a Module - JavaScript API
+sidebar: sidebar
+permalink: javascript_new-api_writing-a-module.html
+folder: javascript
+---
+
+## Introduction
+
+The new JavaScript-API was introduced with WoltLab Suite 3.0 and was a major
+change in all regards. The previously used API heavily relied on larger JavaScript
+files that contained a lot of different components with hidden dependencies
+and suffered from extensive jQuery usage for historic reasons.
+
+Eventually a new API was designed that solves the issues with the legacy API
+by following a few basic principles:
+ 1. Vanilla ES5-JavaScript.
+ It allows us to achieve the best performance across all platforms, there is
+ simply no reason to use jQuery today and the performance penalty on mobile
+ devices is a real issue.
+ 2. Strict usage of modules.
+ Each component is placed in an own file and all dependencies are explicitly
+ declared and injected at the top.Eventually we settled with AMD-style modules
+ using require.js which offers both lazy loading and "ahead of time"-compilatio
+ with `r.js`.
+ 3. No jQuery-based components on page init.
+ Nothing is more annoying than loading a page and then wait for JavaScript to
+ modify the page before it becomes usable, forcing the user to sit and wait.
+ Heavily optimized vanilla JavaScript components offered the speed we wanted.
+ 4. Limited backwards-compatibility.
+ The new API should make it easy to update existing components by providing
+ similar interfaces, while still allowing legacy code to run side-by-side for
+ best compatibility and to avoid rewritting everything from the start.
+
+## Defining a Module
+
+The default location for modules is `js/` in the Core's app dir,
+but every app and plugin can register their own lookup path by providing the path
+using a [template-listener][package_pip_template-listener] on `requirePaths@headIncludeJavaScript`.
+
+For this example we'll assume the file is placed at `js/WoltLabSuite/Core/Ui/Foo.js`,
+the module name is therefore `WoltLabSuite/Core/Ui/Foo`, it is automatically
+derived from the file path and name.
+
+For further instructions on how to define and require modules head over to the [RequireJS API](http://requirejs.org/docs/api.html).
+
+```js
+define(["Ajax", "WoltLabSuite/Core/Ui/Bar"], function(Ajax, UiBar) {
+ "use strict";
+
+ function Foo() { this.init(); }
+ Foo.prototype = {
+ init: function() {
+ elBySel(".myButton").addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+ },
+
+ _click: function(event) {
+ event.preventDefault();
+
+ if (UiBar.isSnafucated()) {
+ Ajax.api(this);
+ }
+ },
+
+ _ajaxSuccess: function(data) {
+ console.log("Received response", data);
+ },
+
+ _ajaxSetup: function() {
+ return {
+ data: {
+ actionName: "makeSnafucated",
+ className: "wcf\\data\\foo\\FooAction"
+ }
+ };
+ }
+ }
+
+ return Foo;
+});
+```
+
+## Loading a Module
+
+Modules can then be loaded through their derived name:
+
+```html
+<script data-relocate="true">
+ require(["WoltLabSuite/Core/Ui/Foo"], function(UiFoo) {
+ new UiFoo();
+ });
+</script>
+```
+
+### Module Aliases
+
+Some common modules have short-hand aliases that can be used to include them
+without writing out their full name. You can still use their original path, but
+it is strongly recommended to use the aliases for consistency.
+
+| Alias | Full Path |
+|---|---|
+| [Ajax][javascript_new-api_ajax] | WoltLabSuite/Core/Ajax |
+| AjaxJsonp | WoltLabSuite/Core/Ajax/Jsonp |
+| AjaxRequest | WoltLabSuite/Core/Ajax/Request |
+| CallbackList | WoltLabSuite/Core/CallbackList |
+| ColorUtil | WoltLabSuite/Core/ColorUtil |
+| [Core][javascript_new-api_core] | WoltLabSuite/Core/Core |
+| DateUtil | WoltLabSuite/Core/Date/Util |
+| Devtools | WoltLabSuite/Core/Devtools |
+| [Dictionary][javascript_new-api_data-structures] | WoltLabSuite/Core/Dictionary |
+| [Dom/ChangeListener][javascript_new-api_dom] | WoltLabSuite/Core/Dom/Change/Listener |
+| Dom/Traverse | WoltLabSuite/Core/Dom/Traverse |
+| [Dom/Util][javascript_new-api_dom] | WoltLabSuite/Core/Dom/Util |
+| [Environment][javascript_new-api_browser] | WoltLabSuite/Core/Environment |
+| [EventHandler][javascript_new-api_events] | WoltLabSuite/Core/Event/Handler |
+| [EventKey][javascript_new-api_events] | WoltLabSuite/Core/Event/Key |
+| [Language][javascript_new-api_core] | WoltLabSuite/Core/Language |
+| [List][javascript_new-api_data-structures] | WoltLabSuite/Core/List |
+| [ObjectMap][javascript_new-api_data-structures] | WoltLabSuite/Core/ObjectMap |
+| Permission | WoltLabSuite/Core/Permission |
+| [StringUtil][javascript_new-api_core] | WoltLabSuite/Core/StringUtil |
+| [Ui/Alignment][javascript_new-api_ui] | WoltLabSuite/Core/Ui/Alignment |
+| [Ui/CloseOverlay][javascript_new-api_ui] | WoltLabSuite/Core/Ui/CloseOverlay |
+| [Ui/Confirmation][javascript_new-api_ui] | WoltLabSuite/Core/Ui/Confirmation |
+| [Ui/Dialog][javascript_new-api_dialogs] | WoltLabSuite/Core/Ui/Dialog |
+| [Ui/Notification][javascript_new-api_ui] | WoltLabSuite/Core/Ui/Notification |
+| Ui/ReusableDropdown | WoltLabSuite/Core/Ui/Dropdown/Reusable |
+| [Ui/Screen][javascript_new-api_browser] | WoltLabSuite/Core/Ui/Screen |
+| Ui/Scroll | WoltLabSuite/Core/Ui/Scroll |
+| Ui/SimpleDropdown | WoltLabSuite/Core/Ui/Dropdown/Simple |
+| Ui/TabMenu | WoltLabSuite/Core/Ui/TabMenu |
+| Upload | WoltLabSuite/Core/Upload |
+| User | WoltLabSuite/Core/User |
+
+{% include links.html %}
--- /dev/null
+---
+title: WCF 2.1.x - CSS
+sidebar: sidebar
+permalink: migration_wcf-21_css.html
+folder: migration/wcf-21
+---
+
+The LESS compiler has been in use since WoltLab Community Framework 2.0, but was replaced with a SCSS compiler in WoltLab Suite 3.0. This change was motivated by SCSS becoming the de facto standard for CSS pre-processing and some really annoying shortcomings in the old LESS compiler.
+
+The entire CSS has been rewritten from scratch, please read the [docs on CSS][view_css] to learn what has changed.
+
+{% include links.html %}
--- /dev/null
+---
+title: WCF 2.1.x - Package Components
+sidebar: sidebar
+permalink: migration_wcf-21_package.html
+folder: migration/wcf-21
+---
+
+## package.xml
+
+### Short Instructions
+
+Instructions can now omit the filename, causing them to use the default filename if defined by the package installation plugin (in short: `PIP`). Unless overridden it will default to the PIP's class name with the first letter being lower-cased, e.g. `EventListenerPackageInstallationPlugin` implies the filename `eventListener.xml`. The file is always assumed to be in the archive's root, files located in subdirectories need to be explicitly stated, just as it worked before.
+
+Every PIP can define a custom filename if the default value cannot be properly derived. For example the `ACPMenu`-pip would default to `aCPMenu.xml`, requiring the class to explicitly override the default filename with `acpMenu.xml` for readability.
+
+### Example
+
+```xml
+<instructions type="install">
+ <!-- assumes `eventListener.xml` -->
+ <instruction type="eventListener" />
+ <!-- assumes `install.sql` -->
+ <instruction type="sql" />
+ <!-- assumes `language/*.xml` -->
+ <instruction type="language" />
+
+ <!-- exceptions -->
+
+ <!-- assumes `files.tar` -->
+ <instruction type="file" />
+ <!-- no default value, requires relative path -->
+ <instruction type="script">acp/install_com.woltlab.wcf_3.0.php</instruction>
+</instructions>
+```
+
+### Exceptions
+
+{% include callout.html content="These exceptions represent the built-in PIPs only, 3rd party plugins and apps may define their own exceptions." type="info" %}
+
+| PIP | Default Value |
+|-------|-------|
+| `acpTemplate` | `acptemplates.tar` |
+| `file` | `files.tar` |
+| `language` | `language/*.xml` |
+| `script` | (No default value) |
+| `sql` | `install.sql` |
+| `template` | `templates.tar` |
+
+## acpMenu.xml
+
+### Renamed Categories
+
+The following categories have been renamed, menu items need to be adjusted to reflect the new names:
+
+| Old Value | New Value |
+|-------|-------|
+| `wcf.acp.menu.link.system` | `wcf.acp.menu.link.configuration` |
+| `wcf.acp.menu.link.display` | `wcf.acp.menu.link.customization` |
+| `wcf.acp.menu.link.community` | `wcf.acp.menu.link.application` |
+
+### Submenu Items
+
+Menu items can now offer additional actions to be accessed from within the menu using an icon-based navigation. This step avoids filling the menu with dozens of `Add …` links, shifting the focus on to actual items. Adding more than one action is not recommended and you should at maximum specify two actions per item.
+
+### Example
+
+```xml
+<!-- category -->
+<acpmenuitem name="wcf.acp.menu.link.group">
+ <parent>wcf.acp.menu.link.user</parent>
+ <showorder>2</showorder>
+</acpmenuitem>
+
+<!-- menu item -->
+<acpmenuitem name="wcf.acp.menu.link.group.list">
+ <controller>wcf\acp\page\UserGroupListPage</controller>
+ <parent>wcf.acp.menu.link.group</parent>
+ <permissions>admin.user.canEditGroup,admin.user.canDeleteGroup</permissions>
+</acpmenuitem>
+<!-- menu item action -->
+<acpmenuitem name="wcf.acp.menu.link.group.add">
+ <controller>wcf\acp\form\UserGroupAddForm</controller>
+ <!-- actions are defined by menu items of menu items -->
+ <parent>wcf.acp.menu.link.group.list</parent>
+ <permissions>admin.user.canAddGroup</permissions>
+ <!-- required FontAwesome icon name used for display -->
+ <icon>fa-plus</icon>
+</acpmenuitem>
+```
+
+### Common Icon Names
+
+You should use the same icon names for the (logically) same task, unifying the meaning of items and making the actions predictable.
+
+| Meaning | Icon Name | Result |
+|-------|-------|-------|
+| Add or create | `fa-plus` | <i class="fa fa-plus"></i> |
+| Search | `fa-search` | <i class="fa fa-search"></i> |
+| Upload | `fa-upload` | <i class="fa fa-upload"></i> |
+
+## box.xml
+
+The [box][package_pip_box] PIP has been added.
+
+## cronjob.xml
+
+{% include callout.html content="Legacy cronjobs are assigned a non-deterministic generic name, the only way to assign them a name is removing them and then adding them again." type="warning" %}
+
+Cronjobs can now be assigned a name using the name attribute as in `<cronjob name="com.woltlab.wcf.refreshPackageUpdates">`, it will be used to identify cronjobs during an update or delete.
+
+## eventListener.xml
+
+{% include callout.html content="Legacy event listeners are assigned a non-deterministic generic name, the only way to assign them a name is removing them and then adding them again." type="warning" %}
+
+Event listeners can now be assigned a name using the name attribute as in `<eventlistener name="sessionPageAccessLog">`, it will be used to identify event listeners during an update or delete.
+
+## menu.xml
+
+The [menu][package_pip_menu] PIP has been added.
+
+## menuItem.xml
+
+The [menuItem][package_pip_menu-item] PIP has been added.
+
+## objectType.xml
+
+The definition `com.woltlab.wcf.user.dashboardContainer` has been removed, it was previously used to register pages that qualify for dashboard boxes. Since WoltLab Suite 3.0, all pages registered through the `page.xml` are valid containers and therefore there is no need for this definition anymore.
+
+The definitions `com.woltlab.wcf.page` and `com.woltlab.wcf.user.online.location` have been superseded by the `page.xml`, they're no longer supported.
+
+## option.xml
+
+The `module.display` category has been renamed into `module.customization`.
+
+## page.xml
+
+The [page][package_pip_page] PIP has been added.
+
+## pageMenu.xml
+
+The `pageMenu.xml` has been superseded by the `page.xml` and is no longer available.
+
+{% include links.html %}
--- /dev/null
+---
+title: WCF 2.1.x - PHP
+sidebar: sidebar
+permalink: migration_wcf-21_php.html
+folder: migration/wcf-21
+---
+
+## Message Processing
+
+WoltLab Suite 3.0 finally made the transition from raw bbcode to bbcode-flavored HTML, with many new features related to message processing being added. This change impacts both message validation and storing, requiring slightly different APIs to get the job done.
+
+### Input Processing for Storage
+
+The returned HTML is an intermediate representation with a maximum of meta data embedded into it, designed to be stored in the database. Some bbcodes are replaced during this process, for example `[b]…[/b]` becomes `<strong>…</strong>`, while others are converted into a metacode tag for later processing.
+
+```php
+<?php
+$processor = new \wcf\system\html\input\HtmlInputProcessor();
+$processor->process($message, $messageObjectType, $messageObjectID);
+$html = $processor->getHtml();
+```
+
+The `$messageObjectID` can be zero if the element did not exist before, but it should be non-zero when saving an edited message.
+
+### Embedded Objects
+
+Embedded objects need to be registered after saving the message, but once again you can use the processor instance to do the job.
+
+```php
+<?php
+$processor = new \wcf\system\html\input\HtmlInputProcessor();
+$processor->process($message, $messageObjectType, $messageObjectID);
+$html = $processor->getHtml();
+
+// at this point the message is saved to database and the created object
+// `$example` is a `DatabaseObject` with the id column `$exampleID`
+
+$processor->setObjectID($example->exampleID);
+if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($processor)) {
+ // there is at least one embedded object, this is also the point at which you
+ // would set `hasEmbeddedObjects` to true (if implemented by your type)
+ (new \wcf\data\example\ExampleEditor($example))->update(['hasEmbeddedObjects' => 1]);
+}
+```
+
+### Rendering the Message
+
+The output processor will parse the intermediate HTML and finalize the output for display. This step is highly dynamic and allows for bbcode evaluation and contextual output based on the viewer's permissions.
+
+```php
+<?php
+$processor = new \wcf\system\html\output\HtmlOutputProcessor();
+$processor->process($html, $messageObjectType, $messageObjectID);
+$renderedHtml = $processor->getHtml();
+```
+
+#### Simplified Output
+
+At some point there can be the need of a simplified output HTML that includes only basic HTML formatting and reduces more sophisticated bbcodes into a simpler representation.
+
+```php
+<?php
+$processor = new \wcf\system\html\output\HtmlOutputProcessor();
+$processor->setOutputType('text/simplified-html');
+$processor->process(…);
+```
+
+#### Plaintext Output
+
+The `text/plain` output type will strip down the simplified HTML into pure text, suitable for text-only output such as the plaintext representation of an email.
+
+```php
+<?php
+$processor = new \wcf\system\html\output\HtmlOutputProcessor();
+$processor->setOutputType('text/plain');
+$processor->process(…);
+```
+
+### Rebuilding Data
+
+#### Converting from BBCode
+
+{% include callout.html content="Enabling message conversion for HTML messages is undefined and yields unexpected results." type="warning" %}
+
+Legacy message that still use raw bbcodes must be converted to be properly parsed by the html processors. This process is enabled by setting the fourth parameter of `process()` to `true`.
+
+```php
+<?php
+$processor = new \wcf\system\html\input\HtmlInputProcessor();
+$processor->process($html, $messageObjectType, $messageObjectID, true);
+$renderedHtml = $processor->getHtml();
+```
+
+#### Extracting Embedded Objects
+
+The `process()` method of the input processor is quite expensive, as it runs through the full message validation including the invocation of HTMLPurifier. This is perfectly fine when dealing with single messages, but when you're handling messages in bulk to extract their embedded objects, you're better of with `processEmbeddedContent()`. This method deconstructs the message, but skips all validation and expects the input to be perfectly valid, that is the output of a previous run of `process()` saved to storage.
+
+```php
+<?php
+$processor = new \wcf\system\html\input\HtmlInputProcessor();
+$processor->processEmbeddedContent($html, $messageObjectType, $messageObjectID);
+
+// invoke `MessageEmbeddedObjectManager::registerObjects` here
+```
+
+## Breadcrumbs / Page Location
+
+{% include callout.html content="Breadcrumbs used to be added left to right, but parent locations are added from the bottom to the top, starting with the first ancestor and going upwards. In most cases you simply need to reverse the order." type="warning" %}
+
+Breadcrumbs used to be a lose collection of arbitrary links, but are now represented by actual page objects and the control has shifted over to the `PageLocationManager`.
+
+```php
+<?php
+// before
+\wcf\system\WCF::getBreadcrumbs()->add(new \wcf\system\breadcrumb\Breadcrumb('title', 'link'));
+
+// after
+\wcf\system\page\PageLocationManager::getInstance()->addParentLocation($pageIdentifier, $pageObjectID, $object);
+```
+
+## Pages and Forms
+
+The property `$activeMenuItem` has been deprecated for the front end and is no longer evaluated at runtime. Recognition of the active item is entirely based around the invoked controller class name and its definition in the page table. You need to properly [register your pages](package_pip_page.html) for this feature to work.
+
+## Search
+
+### ISearchableObjectType
+
+Added the `setLocation()` method that is used to set the current page location based on the search result.
+
+### SearchIndexManager
+
+The methods `SearchIndexManager::add()` and `SearchIndexManager::update()` have been deprecated and forward their call to the new method `SearchIndexManager::set()`.
+
+{% include links.html %}
--- /dev/null
+---
+title: WCF 2.1.x - Templates
+sidebar: sidebar
+permalink: migration_wcf-21_templates.html
+folder: migration/wcf-21
+---
+
+## Page Layout
+
+The template structure has been overhauled and it is no longer required nor recommended to include internal templates, such as `documentHeader`, `headInclude` or `userNotice`. Instead use a simple `{include file='header'}` that now takes care of of the entire application frame.
+
+* Templates must not include a trailing `</body></html>` after including the `footer` template.
+* The `documentHeader`, `headInclude` and `userNotice` template should no longer be included manually, the same goes with the `<body>` element, please use `{include file='header'}` instead.
+* The `sidebarOrientation` variable for the `header` template has been removed and no longer works.
+* `header.boxHeadline` has been unified and now reads `header.contentHeader`
+
+Please see the full example at the end of this page for more information.
+
+## Sidebars
+
+Sidebars are now dynamically populated by the box system, this requires a small change to unify the markup. Additionally the usage of `<fieldset>` has been deprecated due to browser inconsistencies and bugs and should be replaced with `section.box`.
+
+Previous markup used in WoltLab Community Framework 2.1 and earlier:
+
+```html
+<fieldset>
+ <legend><!-- Title --></legend>
+
+ <div>
+ <!-- Content -->
+ </div>
+</fieldset>
+```
+
+The new markup since WoltLab Suite 3.0:
+
+```html
+<section class="box">
+ <h2 class="boxTitle"><!-- Title --></h2>
+
+ <div class="boxContent">
+ <!-- Content -->
+ </div>
+</section>
+```
+
+## Forms
+
+The input tag for session ids `SID_INPUT_TAG` has been deprecated and no longer yields any content, it can be safely removed. In previous versions forms have been wrapped in `<div class="container containerPadding marginTop">…</div>` which no longer has any effect and should be removed.
+
+If you're using the preview feature for WYSIWYG-powered input fields, you need to alter the preview button include instruction:
+
+```smarty
+{include file='messageFormPreviewButton' previewMessageObjectType='com.example.foo.bar' previewMessageObjectID=0}
+```
+
+*The message object id should be non-zero when editing.*
+
+## Icons
+
+The old `.icon-<iconName>` classes have been removed, you are required to use the official `.fa-<iconName>` class names from FontAwesome. This does not affect the generic classes `.icon` (indicates an icon) and `.icon<size>` (e.g. `.icon16` that sets the dimensions), these are still required and have not been deprecated.
+
+Before:
+
+```html
+<span class="icon icon16 icon-list">
+```
+
+Now:
+
+```html
+<span class="icon icon16 fa-list">
+```
+
+### Changed Icon Names
+
+Quite a few icon names have been renamed, the official wiki lists the [new icon names](https://github.com/FortAwesome/Font-Awesome/wiki/Upgrading-from-3.2.1-to-4) in FontAwesome 4.
+
+## Changed Classes
+
+* `.dataList` has been replaced and should now read `<ol class="inlineList commaSeparated">` (same applies to `<ul>`)
+* `.framedIconList` has been changed into `.userAvatarList`
+
+## Removed Elements and Classes
+
+* `<nav class="jsClipboardEditor">` and `<div class="jsClipboardContainer">` have been replaced with a floating button.
+* The anchors `a.toTopLink` have been replaced with a floating button.
+* Avatars should no longer receive the class `framed`
+* The `dl.condensed` class, as seen in the editor tab menu, is no longer required.
+* Anything related to `sidebarCollapsed` has been removed as sidebars are no longer collapsible.
+
+## Simple Example
+
+The code below includes only the absolute minimum required to display a page, the content title is already included in the output.
+
+```smarty
+{include file='header'}
+
+<div class="section">
+ Hello World!
+</div>
+
+{include file='footer'}
+```
+
+## Full Example
+
+```smarty
+{*
+ The page title is automatically set using the page definition, avoid setting it if you can!
+ If you really need to modify the title, you can still reference the original title with:
+ {$__wcf->getActivePage()->getTitle()}
+*}
+{capture assign='pageTitle'}Custom Page Title{/capture}
+
+{*
+ NOTICE: The content header goes here, see the section after this to learn more.
+*}
+
+{* you must not use `headContent` for JavaScript *}
+{capture assign='headContent'}
+ <link rel="alternate" type="application/rss+xml" title="{lang}wcf.global.button.rss{/lang}" href="…">
+{/capture}
+
+{* optional, content will be added to the top of the left sidebar *}
+{capture assign='sidebarLeft'}
+ …
+
+ {event name='boxes'}
+{/capture}
+
+{* optional, content will be added to the top of the right sidebar *}
+{capture assign='sidebarRight'}
+ …
+
+ {event name='boxes'}
+{/capture}
+
+{capture assign='headerNavigation'}
+ <li><a href="#" title="Custom Button" class="jsTooltip"><span class="icon icon16 fa-check"></span> <span class="invisible">Custom Button</span></a></li>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+ <div class="paginationTop">
+ {content}
+ {pages …}
+ {/content}
+ </div>
+{/hascontent}
+
+{* the actual content *}
+<div class="section">
+ …
+</div>
+
+<footer class="contentFooter">
+ {* skip this if you're not using any pagination *}
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ <nav class="contentFooterNavigation">
+ <ul>
+ <li><a href="…" class="button"><span class="icon icon16 fa-plus"></span> <span>Custom Button</span></a></li>
+ {event name='contentFooterNavigation'}
+ </ul>
+ </nav>
+</footer>
+
+<script data-relocate="true">
+ /* any JavaScript code you need */
+</script>
+
+{* do not include `</body></html>` here, the footer template is the last bit of code! *}
+{include file='footer'}
+```
+
+### Content Header
+
+There are two different methods to set the content header, one sets only the actual values, but leaves the outer HTML untouched, that is generated by the `header` template. This is the recommended approach and you should avoid using the alternative method whenever possible.
+
+#### Recommended Approach
+
+```smarty
+{* This is automatically set using the page data and should not be set manually! *}
+{capture assign='contentTitle'}Custom Content Title{/capture}
+
+{capture assign='contentDescription'}Optional description that is displayed right after the title.{/capture}
+
+{capture assign='contentHeaderNavigation'}List of navigation buttons displayed right next to the title.{/capture}
+```
+
+#### Alternative
+
+```smarty
+{capture assign='contentHeader'}
+ <header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">Custom Content Title</h1>
+ <p class="contentHeaderDescription">Custom Content Description</p>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='CustomController'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>Custom Button</span></a></li>
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+ </header>
+{/capture}
+```
--- /dev/null
+---
+title: Migrating from WSC 3.0 - CSS
+sidebar: sidebar
+permalink: migration_wsc-30_css.html
+folder: migration/wsc-30
+---
+
+## New Style Variables
+
+{% include callout.html content="The new style variables are only applied to styles that have the compatibility set to WSC 3.1" type="info" %}
+
+### wcfContentContainer
+
+The page content is encapsulated in a new container that wraps around the inner content, but excludes the sidebars, header and page navigation elements.
+
+ * `$wcfContentContainerBackground` - background color
+ * `$wcfContentContainerBorder` - border color
+
+### wcfEditorButton
+
+These variables control the appearance of the editor toolbar and its buttons.
+
+ * `$wcfEditorButtonBackground` - button and toolbar background color
+ * `$wcfEditorButtonBackgroundActive` - active button background color
+ * `$wcfEditorButtonText` - text color for available buttons
+ * `$wcfEditorButtonTextActive` - text color for active buttons
+ * `$wcfEditorButtonTextDisabled` - text color for disabled buttons
+
+## Color Variables in `alert.scss`
+
+The color values for `<small class="innerError">` used to be hardcoded values, but have now been changed to use the values for error messages (`wcfStatusError*`) instead.
+
+{% include links.html %}
--- /dev/null
+---
+title: Migrating from WSC 3.0 - JavaScript
+sidebar: sidebar
+permalink: migration_wsc-30_javascript.html
+folder: migration/wsc-30
+---
+
+## Accelerated Guest View / Tiny Builds
+
+The new tiny builds are highly optimized variants of existing JavaScript files and modules, aiming for significant performance improvements for guests and search engines alike. This is accomplished by heavily restricting page interaction to read-only actions whenever possible, which in return removes the need to provide certain JavaScript modules in general.
+
+For example, disallowing guests to write any formatted messages will in return remove the need to provide the WYSIWYG editor at all. But it doesn't stop there, there are a lot of other modules that provide additional features for the editor, and by excluding the editor, we can also exclude these modules too.
+
+Long story short, the tiny mode guarantees that certain actions will never be carried out by guests or search engines, therefore some modules are not going to be needed by them ever.
+
+### Code Templates for Tiny Builds
+
+The following examples assume that you use the virtual constant `COMPILER_TARGET_DEFAULT` as a switch for the optimized code path. This is also the constant used by the official [build scripts for JavaScript files](https://github.com/WoltLab/WCF/tree/master/extra).
+
+We recommend that you provide a mock implementation for existing code to ensure 3rd party compatibility. It is enough to provide a bare object or class that exposes the original properties using the same primitive data types. This is intended to provide a soft-fail for implementations that are not aware of the tiny mode yet, but is not required for classes that did not exist until now.
+
+#### Legacy JavaScript
+
+```js
+if (COMPILER_TARGET_DEFAULT) {
+ WCF.Example.Foo = {
+ makeSnafucated: function() {
+ return "Hello World";
+ }
+ };
+
+ WCF.Example.Bar = Class.extend({
+ foobar: "baz",
+
+ foo: function($bar) {
+ return $bar + this.foobar;
+ }
+ });
+}
+else {
+ WCF.Example.Foo = {
+ makeSnafucated: function() {}
+ };
+
+ WCF.Example.Bar = Class.extend({
+ foobar: "",
+ foo: function() {}
+ });
+}
+```
+
+#### require.js Modules
+
+```js
+define(["some", "fancy", "dependencies"], function(Some, Fancy, Dependencies) {
+ "use strict";
+
+ if (!COMPILER_TARGET_DEFAULT) {
+ var Fake = function() {};
+ Fake.prototype = {
+ init: function() {},
+ makeSnafucated: function() {}
+ };
+ return Fake;
+ }
+
+ function MyAwesomeClass(niceArgument) { this.init(niceArgument); }
+ MyAwesomeClass.prototype = {
+ init: function(niceArgument) {
+ if (niceArgument) {
+ this.makeSnafucated();
+ }
+ },
+
+ makeSnafucated: function() {
+ console.log("Hello World");
+ }
+ }
+
+ return MyAwesomeClass;
+});
+```
+
+### Including tinified builds through `{js}`
+
+The `{js}` template-plugin has been updated to include support for tiny builds controlled through the optional flag `hasTiny=true`:
+
+```
+{js application='wcf' file='WCF.Example' hasTiny=true}
+```
+
+This line generates a different output depending on the debug mode and the user login-state.
+
+## Real Error Messages for AJAX Responses
+
+The `errorMessage` property in the returned response object for failed AJAX requests contained an exception-specific but still highly generic error message. This issue has been around for quite a long time and countless of implementations are relying on this false behavior, eventually forcing us to leave the value unchanged.
+
+This problem is solved by adding the new property `realErrorMessage` that exposes the message exactly as it was provided and now matches the value that would be displayed to users in traditional forms.
+
+### Example Code
+
+```js
+define(['Ajax'], function(Ajax) {
+ return {
+ // ...
+ _ajaxFailure: function(responseData, responseText, xhr, requestData) {
+ console.log(responseData.realErrorMessage);
+ }
+ // ...
+ };
+});
+```
+
+## Simplified Form Submit in Dialogs
+
+Forms embedded in dialogs often do not contain the HTML `<form>`-element and instead rely on JavaScript click- and key-handlers to emulate a `<form>`-like submit behavior. This has spawned a great amount of nearly identical implementations that all aim to handle the form submit through the `Enter`-key, still leaving some dialogs behind.
+
+WoltLab Suite 3.1 offers automatic form submit that is enabled through a set of specific conditions and data attributes:
+
+ 1. There must be a submit button that matches the selector `.formSubmit > input[type="submit"], .formSubmit > button[data-type="submit"]`.
+ 2. The dialog object provided to `UiDialog.open()` implements the method `_dialogSubmit()`.
+ 3. Input fields require the attribute `data-dialog-submit-on-enter="true"` to be set, the `type` must be one of `number`, `password`, `search`, `tel`, `text` or `url`.
+
+Clicking on the submit button or pressing the `Enter`-key in any watched input field will start the submit process. This is done automatically and does not require a manual interaction in your code, therefore you should not bind any click listeners on the submit button yourself.
+
+Any input field with the `required` attribute set will be validated to contain a non-empty string after processing the value with `String.prototype.trim()`. An empty field will abort the submit process and display a visible error message next to the offending field.
+
+## Helper Function for Inline Error Messages
+
+Displaying inline error messages on-the-fly required quite a few DOM operations that were quite simple but also super repetitive and thus error-prone when incorrectly copied over. The global helper function `elInnerError()` was added to provide a simple and consistent behavior of inline error messages.
+
+You can display an error message by invoking `elInnerError(elementRef, "Your Error Message")`, it will insert a new `<small class="innerError">` and sets the given message. If there is already an inner error present, then the message will be replaced instead.
+
+Hiding messages is done by setting the 2nd parameter to `false` or an empty string:
+
+ * `elInnerError(elementRef, false)`
+ * `elInnerError(elementRef, '')`
+
+The special values `null` and `undefined` are supported too, but their usage is discouraged, because they make it harder to understand the intention by reading the code:
+
+ * `elInnerError(elementRef, null)`
+ * `elInnerError(elementRef)`
+
+### Example Code
+
+```js
+require(['Language'], function(Language)) {
+ var input = elBySel('input[type="text"]');
+ if (input.value.trim() === '') {
+ // displays a new inline error or replaces the message if there is one already
+ elInnerError(input, Language.get('wcf.global.form.error.empty'));
+ }
+ else {
+ // removes the inline error if it exists
+ elInnerError(input, false);
+ }
+
+ // the above condition is equivalent to this:
+ elInnerError(input, (input.value.trim() === '' ? Language.get('wcf.global.form.error.empty') : false));
+}
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Migrating from WSC 3.0 - Package Components
+sidebar: sidebar
+permalink: migration_wsc-30_package.html
+folder: migration/wsc-30
+---
+
+## Cronjob Scheduler uses Server Timezone
+
+The execution time of cronjobs was previously calculated based on the coordinated universal time (UTC). This was changed in WoltLab Suite 3.1 to use the server timezone or, to be precise, the default timezone set in the administration control panel.
+
+## Exclude Pages from becoming a Landing Page
+
+Some pages do not qualify as landing page, because they're designed around specific expectations that aren't matched in all cases. Examples include the user control panel and its sub-pages that cannot be accessed by guests and will therefore break the landing page for those. While it is somewhat to be expected from control panel pages, there are enough pages that fall under the same restrictions, but aren't easily recognized as such by an administrator.
+
+You can exclude these pages by adding `<excludeFromLandingPage>1</excludeFromLandingPage>` (case-sensitive) to the relevant pages in your `page.xml`.
+
+### Example Code
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+ <import>
+ <page identifier="com.example.foo.Bar">
+ <!-- ... -->
+ <excludeFromLandingPage>1</excludeFromLandingPage>
+ <!-- ... -->
+ </page>
+ </import>
+</data>
+```
+
+## New Package Installation Plugin for Media Providers
+
+Please refer to the documentation of the [`mediaProvider.xml`][package_pip_media-provider] to learn more.
+
+## Limited Forward-Compatibility for Plugins
+
+Please refer to the documentation of the [`<compatibility>`](package_package-xml.html#compatibility) tag in the `package.xml`.
+
+{% include links.html %}
--- /dev/null
+---
+title: Migrating from WSC 3.0 - PHP
+sidebar: sidebar
+permalink: migration_wsc-30_php.html
+folder: migration/wsc-30
+---
+
+## Approval-System for Comments
+
+Comments can now be set to require approval by a moderator before being published. This feature is disabled by default if you do not provide a permission in the manager class, enabling it requires a new permission that has to be provided in a special property of your manage implementation.
+
+```php
+<?php
+class ExampleCommentManager extends AbstractCommentManager {
+ protected $permissionAddWithoutModeration = 'foo.bar.example.canAddCommentWithoutModeration';
+}
+```
+
+## Raw HTML in User Activity Events
+
+User activity events were previously encapsulated inside `<div class="htmlContent">…</div>`, with impacts on native elements such as lists. You can now disable the class usage by defining your event as raw HTML:
+
+```php
+<?php
+class ExampleUserActivityEvent {
+ // enables raw HTML for output, defaults to `false`
+ protected $isRawHtml = true;
+}
+```
+
+## Permission to View Likes of an Object
+
+Being able to view the like summary of an object was restricted to users that were able to like the object itself. This creates situations where the object type in general is likable, but the particular object cannot be liked by the current users, while also denying them to view the like summary (but it gets partly exposed through the footer note/summary!).
+
+Implement the interface `\wcf\data\like\IRestrictedLikeObjectTypeProvider` in your object provider to add support for this new permission check.
+
+```php
+<?php
+class LikeableExampleProvider extends ExampleProvider implements IRestrictedLikeObjectTypeProvider, IViewableLikeProvider {
+ public function canViewLikes(ILikeObject $object) {
+ // perform your permission checks here
+ return true;
+ }
+}
+```
+
+## Developer Tools: Sync Feature
+
+The synchronization feature of the newly added developer tools works by invoking a package installation plugin (PIP) outside of a regular installation, while simulating the basic environment that is already exposed by the API.
+
+However, not all PIPs qualify for this kind of execution, especially because it could be invoked multiple times in a row by the user. This is solved by requiring a special marking for PIPs that have no side-effects (= idempotent) when invoked any amount of times with the same arguments.
+
+There's another feature that allows all matching PIPs to be executed in a row using a single button click. In order to solve dependencies on other PIPs, any implementing PIP must also provide the method `getSyncDependencies()` that returns the dependent PIPs in an arbitrary order.
+
+```php
+<?php
+class ExamplePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+ public static function getSyncDependencies() {
+ // provide a list of dependent PIPs in arbitrary order
+ return [];
+ }
+}
+```
+
+## Media Providers
+
+Media providers were added through regular SQL queries in earlier versions, but this is neither convenient, nor did it offer a reliable method to update an existing provider. WoltLab Suite 3.1 adds a new `mediaProvider`-PIP that also offers a `className` parameter to off-load the result evaluation and HTML generation.
+
+### Example Implementation
+
+#### mediaProvider.xml
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/mediaProvider.xsd">
+ <import>
+ <provider name="example">
+ <title>Example Provider</title>
+ <regex><![CDATA[https?://example.com/watch?v=(?P<ID>[a-zA-Z0-9])]]></regex>
+ <className><![CDATA[wcf\system\bbcode\media\provider\ExampleBBCodeMediaProvider]]></className>
+ </provider>
+ </import>
+</data>
+```
+
+#### PHP Callback
+
+The full match is provided for `$url`, while any capture groups from the regular expression are assigned to `$matches`.
+
+```php
+<?php
+class ExampleBBCodeMediaProvider implements IBBCodeMediaProvider {
+ public function parse($url, array $matches = []) {
+ return "final HTML output";
+ }
+}
+```
+
+## Re-Evaluate HTML Messages
+
+{% include callout.html content="You need to manually set the disallowed bbcodes in order to avoid unintentional bbcode evaluation. Please see [this commit](https://github.com/WoltLab/WCF/commit/7e058783da1378dda5393a9bb4df9cfe94e5b394) for a reference implementation inside worker processes." type="warning" %}
+
+The HtmlInputProcessor only supported two ways to handle an existing HTML message:
+
+ 1. Load the string through `process()` and run it through the validation and sanitation process, both of them are rather expensive operations and do not qualify for rebuild data workers.
+ 2. Detect embedded content using `processEmbeddedContent()` which bypasses most tasks that are carried out by `process()` which aren't required, but does not allow a modification of the message.
+
+The newly added method `reprocess($message, $objectType, $objectID)` solves this short-coming by offering a full bbcode and text re-evaluation while bypassing any input filters, assuming that the input HTML was already filtered previously.
+
+### Example Usage
+
+```php
+<?php
+// rebuild data workers tend to contain code similar to this:
+foreach ($this->objectList as $message) {
+ // ...
+ if (!$message->enableHtml) {
+ // ...
+ }
+ else {
+ // OLD:
+ $this->getHtmlInputProcessor()->processEmbeddedContent($message->message, 'com.example.foo.message', $message->messageID);
+
+ // REPLACE WITH:
+ $this->getHtmlInputProcessor()->reprocess($message->message, 'com.example.foo.message', $message->messageID);
+ $data['message'] = $this->getHtmlInputProcessor()->getHtml();
+ }
+ // ...
+}
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Migrating from WSC 3.0 - Templates
+sidebar: sidebar
+permalink: migration_wsc-30_templates.html
+folder: migration/wsc-30
+---
+
+## Comment-System Overhaul
+
+{% include callout.html content="Unfortunately, there has been a breaking change related to the creation of comments. You need to apply the changes below before being able to create new comments." type="danger" %}
+
+### Adding Comments
+
+Existing implementations need to include a new template right before including the generic `commentList` template.
+
+```html
+<ul id="exampleCommentList" class="commentList containerList" data-...>
+ {include file='commentListAddComment' wysiwygSelector='exampleCommentListAddComment'}
+ {include file='commentList'}
+</ul>
+```
+
+## Redesigned ACP User List
+
+Custom interaction buttons were previously added through the template event `rowButtons` and were merely a link-like element with an icon inside. This is still valid and supported for backwards-compatibility, but it is recommend to adapt to the new drop-down-style options using the new template event `dropdownItems`.
+
+```html
+<!-- button for usage with the `rowButtons` event -->
+<span class="icon icon16 fa-list jsTooltip" title="Button Title"></span>
+
+<!-- new drop-down item for the `dropdownItems` event -->
+<li><a href="#" class="jsMyButton">Button Title</a></li>
+```
+
+## Sidebar Toogle-Buttons on Mobile Device
+
+{% include callout.html content="You cannot override the button label for sidebars containing navigation menus." type="info" %}
+
+The page sidebars are automatically collapsed and presented as one or, when both sidebar are present, two condensed buttons. They use generic sidebar-related labels when open or closed, with the exception of embedded menus which will change the button label to read "Show/Hide Navigation".
+
+You can provide a custom label before including the sidebars by assigning the new labels to a few special variables:
+
+```html
+{assign var='__sidebarLeftShow' value='Show Left Sidebar'}
+{assign var='__sidebarLeftHide' value='Hide Left Sidebar'}
+{assign var='__sidebarRightShow' value='Show Right Sidebar'}
+{assign var='__sidebarRightHide' value='Hide Right Sidebar'}
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Migrating from WSC 3.1 - Form Builder
+sidebar: sidebar
+permalink: migration_wsc-31_form-builder.html
+folder: migration/wsc-31
+parent: migration_wsc-31_php
+---
+
+## Example: Two Text Form Fields
+
+As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the [first part of the tutorial series](tutorial_tutorial-series_part-1-base-structure.html) will be updated to the new form builder API.
+This form is the perfect first examples as it is very simple with only two text fields whose only restriction is that they have to be filled out and that their values may not be longer than 255 characters each.
+
+As a reminder, here are the two relevant PHP files and the relevant template file:
+
+{% highlight php %}
+{% include migration/wsc-31/formBuilder/PersonAddForm_old.class.php %}
+{% endhighlight %}
+
+{% highlight php %}
+{% include migration/wsc-31/formBuilder/PersonEditForm_old.class.php %}
+{% endhighlight %}
+
+{% highlight php %}
+{% include migration/wsc-31/formBuilder/personAdd_old.tpl %}
+{% endhighlight %}
+
+Updating the template is easy as the complete form is replace by a single line of code:
+
+{% highlight php %}
+{% include migration/wsc-31/formBuilder/personAdd_new.tpl %}
+{% endhighlight %}
+
+`PersonEditForm` also becomes much simpler:
+only the edited `Person` object must be read:
+
+{% highlight php %}
+{% include migration/wsc-31/formBuilder/PersonEditForm_new.class.php %}
+{% endhighlight %}
+
+Most of the work is done in `PersonAddForm`:
+
+{% highlight php %}
+{% include migration/wsc-31/formBuilder/PersonAddForm_new.class.php %}
+{% endhighlight %}
+
+But, as you can see, the number of lines almost decreased by half.
+All changes are due to extending `AbstractFormBuilderForm`:
+
+- `$formAction` is added and set to `create` as the form is used to create a new person.
+ In the edit form, `$formAction` has not to be set explicitly as it is done automatically if a `$formObject` is set.
+- `$objectActionClass` is set to `PersonAction::class` and is the class name of the used `AbstractForm::$objectAction` object to create and update the `Person` object.
+- `AbstractFormBuilderForm::createForm()` is overridden and the form contents are added:
+ a form container representing the `div.section` element from the old version and the two form fields with the same ids and labels as before.
+ The contents of the old `validate()` method is put into two method calls:
+ `required()` to ensure that the form is filled out and `maximumLength(255)` to ensure that the names are not longer than 255 characters.
--- /dev/null
+---
+title: Migrating from WSC 3.1 - Like System
+sidebar: sidebar
+permalink: migration_wsc-31_like.html
+folder: migration/wsc-31
+parent: migration_wsc-31_php
+---
+
+## Introduction
+
+With version 5.2 of WoltLab Suite Core the like system was completely replaced by the new reactions system. This makes it necessary to make some adjustments to existing code so that your plugin integrates completely into the new system. However, we have kept these adjustments as small as possible so that it is possible to use the reaction system with slight restrictions even without adjustments.
+
+## Limitations if no adjustments are made to the existing code
+
+If no adjustments are made to the existing code, the following functions are not available:
+* Notifications about reactions/likes
+* Recent Activity Events for reactions/likes
+
+## Migration
+### Notifications
+#### Mark notification as compatible
+Since there are no more likes with the new version, it makes no sense to send notifications about it. Instead of notifications about likes, notifications about reactions are now sent. However, this only changes the notification text and not the notification itself. To update the notification, we first add the interface `\wcf\data\reaction\object\IReactionObject` to the `\wcf\data\like\object\ILikeObject` object (e.g. in WoltLab Suite Forum we added the interface to the class `\wbb\data\post\LikeablePost`). After that the object is marked as "compatible with WoltLab Suite Core 5.2" and notifications about reactions are sent again.
+
+#### Language Variables
+Next, to display all reactions for the current notification in the notification text, we include the trait `\wcf\system\user\notification\event\TReactionUserNotificationEvent` in the user notification event class (typically named like `*LikeUserNotificationEvent`). These trait provides a new function that reads out and groups the reactions. The result of this function must now only be passed to the language variable. The name "reactions" is typically used as the variable name for the language variable.
+
+As a final step, we only need to change the language variables themselves. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.
+
+##### English
+
+`{prefix}.like.title`
+```
+Reaction to a {objectName}
+```
+
+`{prefix}.like.title.stacked`
+
+```
+{#$count} users reacted to your {objectName}
+```
+
+`{prefix}.like.message`
+```
+{@$author->getAnchorTag()} reacted to your {objectName} ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
+```
+
+`{prefix}.like.message.stacked`
+
+```
+{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} and {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} and {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} and {#$others} others{/if} reacted to your {objectName} ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
+```
+
+`wcf.user.notification.{objectTypeName}.like.notification.like`
+```
+Notify me when someone reacted to my {objectName}
+```
+
+##### German
+
+`{prefix}.like.title`
+```
+Reaktion auf einen {objectName}
+```
+
+`{prefix}.like.title.stacked`
+
+```
+{#$count} Benutzern haben auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert
+```
+
+`{prefix}.like.message`
+```
+{@$author->getAnchorTag()} hat auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
+```
+
+`{prefix}.like.message.stacked`
+
+```
+{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} und {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} und {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} und {#$others} weitere{/if} haben auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
+```
+
+`wcf.user.notification.{object_type_name}.like.notification.like`
+```
+Jemandem hat auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert
+```
+
+### Recent Activity
+
+To adjust entries in the Recent Activity, only three small steps are necessary. First we pass the concrete reaction to the language variable, so that we can use the reaction object there. To do this, we add the following variable to the text of the `\wcf\system\user\activity\event\IUserActivityEvent` object: `$event->reactionType`. Typically we name the variable `reactionType`. In the second step, we mark the event as compatible. Therefore we set the parameter `supportsReactions` in the [`objectType.xml`](package_pip_object-type) to `1`. So for example the entry looks like this:
+
+```xml
+<type>
+ <name>com.woltlab.example.likeableObject.recentActivityEvent</name>
+ <definitionname>com.woltlab.wcf.user.recentActivityEvent</definitionname>
+ <classname>wcf\system\user\activity\event\LikeableObjectUserActivityEvent</classname>
+ <supportsReactions>1</supportsReactions>
+</type>
+```
+
+Finally we modify our language variable. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.
+
+#### English
+`wcf.user.recentActivity.{object_type_name}.recentActivityEvent`
+```
+Reaction ({objectName})
+```
+
+_Your language variable for the recent activity text_
+```
+Reacted with <span title="{$reactionType->getTitle()}" class="jsTooltip">{@$reactionType->renderIcon()}</span> to the {objectName}.
+```
+
+#### German
+`wcf.user.recentActivity.{objectTypeName}.recentActivityEvent`
+```
+Reaktion ({objectName})
+```
+
+_Your language variable for the recent activity text_
+```
+Hat mit <span title="{$reactionType->getTitle()}" class="jsTooltip">{@$reactionType->renderIcon()}</span> auf {objectName} reagiert.
+```
+
+### Comments
+If comments send notifications, they must also be updated. The language variables are changed in the same way as described in the section [Notifications / Language](migration_wsc-31_like.html#Language-Variables). After that comment must be marked as compatible. Therefore we set the parameter `supportsReactions` in the [`objectType.xml`](package_pip_object-type) to `1`. So for example the entry looks like this:
+
+```xml
+<type>
+ <name>com.woltlab.wcf.objectComment.response.like.notification</name>
+ <definitionname>com.woltlab.wcf.notification.objectType</definitionname>
+ <classname>wcf\system\user\notification\object\type\LikeUserNotificationObjectType</classname>
+ <category>com.woltlab.example</category>
+ <supportsReactions>1</supportsReactions>
+</type>
+```
+
+## Forward Compatibility
+
+So that these changes also work in older versions of WoltLab Suite Core, the used classes and traits were backported with WoltLab Suite Core 3.0.22 and WoltLab Suite Core 3.1.10.
--- /dev/null
+---
+title: Migrating from WSC 3.1 - PHP
+sidebar: sidebar
+permalink: migration_wsc-31_php.html
+folder: migration/wsc-31
+---
+
+## Form Builder
+
+WoltLab Suite Core 5.2 introduces a new, simpler and quicker way of creating forms:
+[form builder](php_api_form_builder.html).
+You can find examples of how to migrate existing forms to form builder [here](migration_wsc-31_form-builder.html).
+
+In the near future, to ensure backwards compatibility within WoltLab packages, we will only use form builder for new forms or for major rewrites of existing forms that would break backwards compatibility anyway.
+
+## Like System
+WoltLab Suite Core 5.2 replaced the like system with the reaction system. You can find the migration guide [here](migration_wsc-31_like.html).
+
+## User Content Providers
+
+User content providers help the WoltLab Suite to find user generated content. They provide a class with which you can find content from a particular user and delete objects.
+
+
+### PHP Class
+
+First, we create the PHP class that provides our interface to provide the data. The class must implement interface `wcf\system\user\content\provider\IUserContentProvider` in any case. Mostly we process data which is based on [`wcf\data\DatabaseObject`](php_database-objects.html). In this case, the WoltLab Suite provides an abstract class `wcf\system\user\content\provider\AbstractDatabaseUserContentProvider` that can be used to automatically generates the standardized classes to generate the list and deletes objects via the DatabaseObjectAction. For example, if we would create a content provider for comments, the class would look like this:
+
+```php
+<?php
+namespace wcf\system\user\content\provider;
+use wcf\data\comment\Comment;
+
+/**
+ * User content provider for comments.
+ *
+ * @author Joshua Ruesweg
+ * @copyright 2001-2018 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\User\Content\Provider
+ * @since 5.2
+ */
+class CommentUserContentProvider extends AbstractDatabaseUserContentProvider {
+ /**
+ * @inheritdoc
+ */
+ public static function getDatabaseObjectClass() {
+ return Comment::class;
+ }
+}
+```
+
+### Object Type
+
+Now the appropriate object type must be created for the class. This object type must be from the definition `com.woltlab.wcf.content.userContentProvider` and include the previous created class as FQN in the parameter `classname`. Also the following parameters can be used in the object type:
+
+#### `nicevalue`
+
+<span class="label label-info">Optional</span>
+
+The nice value is used to determine the order in which the remove content worker are execute the provider. Content provider with lower nice values are executed first.
+
+#### `hidden`
+
+<span class="label label-info">Optional</span>
+
+Specifies whether or not this content provider can be actively selected in the Content Remove Worker. If it cannot be selected, it will not be executed automatically!
+
+#### `requiredobjecttype`
+
+<span class="label label-info">Optional</span>
+
+The specified list of comma-separated object types are automatically removed during content removal when this object type is being removed. Heads up: The order of removal is undefined by default, specify a `nicevalue` if the order is important.
+
+
+
+## PHP Database API
+
+WoltLab Suite 5.2 introduces a new way to update the database scheme:
+[database PHP API](package_database-php-api.html).
\ No newline at end of file
--- /dev/null
+---
+title: Migrating from WSC 5.2 - Third Party Libraries
+sidebar: sidebar
+permalink: migration_wsc-52_libraries.html
+folder: migration/wsc-52
+---
+
+## SCSS Compiler
+
+WoltLab Suite Core 5.3 upgrades the bundled SCSS compiler from `leafo/scssphp` 0.7.x to `scssphp/scssphp` 1.1.x.
+With the updated composer package name the SCSS compiler also received updated namespaces.
+WoltLab Suite Core adds a compatibility layer that maps the old namespace to the new namespace.
+The classes themselves appear to be drop-in compatible.
+Exceptions cannot be mapped using this compatibility layer, any `catch` blocks catching a specific Exception within the `Leafo` namespace will need to be adjusted.
+
+More details can be found in the [Pull Request WoltLab/WCF#3415](https://github.com/WoltLab/WCF/pull/3415).
+
+## Guzzle
+
+WoltLab Suite Core 5.3 ships with a bundled version of [Guzzle 6](http://docs.guzzlephp.org/en/6.5/).
+Going forward using Guzzle is the recommended way to perform HTTP requests.
+The `\wcf\util\HTTPRequest` class should no longer be used and transparently uses Guzzle under the hood.
+
+Use `\wcf\system\io\HttpFactory` to retrieve a correctly configured `GuzzleHttp\ClientInterface`.
+
+Please note that it is recommended to explicitely specify a `sink` when making requests, due to a PHP / Guzzle bug.
+Have a [look at the implementation in WoltLab/WCF](https://github.com/WoltLab/WCF/blob/ce163806c468763f6e3b04e4bf7318c6f8035737/wcfsetup/install/files/lib/util/HTTPRequest.class.php#L194-L195) for an example.
--- /dev/null
+---
+title: Migrating from WSC 5.2 - PHP
+sidebar: sidebar
+permalink: migration_wsc-52_php.html
+folder: migration/wsc-52
+---
+
+## Comments
+
+The [`ICommentManager::isContentAuthor(Comment|CommentResponse): bool`](https://github.com/WoltLab/WCF/blob/aa96d34130d58c150a35ebd8936f09c830ccd685/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php#L151-L158) method was added.
+A default implementation that always returns `false` is available when inheriting from `AbstractCommentManager`.
+
+It is strongly recommended to implement `isContentAuthor` within your custom comment manager.
+An example implementation [can be found in `ArticleCommentManager`](https://github.com/WoltLab/WCF/blob/aa96d34130d58c150a35ebd8936f09c830ccd685/wcfsetup/install/files/lib/system/comment/manager/ArticleCommentManager.class.php#L213-L219).
+
+## Event Listeners
+
+The [`AbstractEventListener`](https://github.com/WoltLab/WCF/blob/75631516d45f9355f6c73d6375bf804d2abd587e/wcfsetup/install/files/lib/system/event/listener/AbstractEventListener.class.php) class was added.
+`AbstractEventListener` contains an implementation of `execute()` that will dispatch the event handling to dedicated methods based on the `$eventName` and, in case of the event object being an `AbstractDatabaseObjectAction`, the action name.
+
+Find the details of the dispatch behavior within the class comment of `AbstractEventListener`.
+
+## Email Activation
+
+Starting with WoltLab Suite 5.3 the user activation status is independent of the email activation status.
+A user can be activated even though their email address has not been confirmed, preventing emails being sent to these users.
+Going forward the new `User::isEmailConfirmed()` method should be used to check whether sending automated emails to this user is acceptable.
+If you need to check the user's activation status you should use the new method `User::pendingActivation()` instead of relying on `activationCode`.
+To check, which type of activation is missing, you can use the new methods `User::requiresEmailActivation()` and `User::requiresAdminActivation()`.
+
+## `*AddForm`
+
+WoltLab Suite 5.3 provides a new framework to allow the administrator to easily edit newly created objects by adding an edit link to the success message.
+To support this edit link two small changes are required within your `*AddForm`.
+
+1. Update the template.
+
+ Replace:
+ ```smarty
+ {include file='formError'}
+
+ {if $success|isset}
+ <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+ {/if}
+ ```
+
+ With:
+ ```smarty
+ {include file='formNotice'}
+ ```
+
+2. Expose `objectEditLink` to the template.
+
+ Example (`$object` being the newly created object):
+ ```php
+ WCF::getTPL()->assign([
+ 'success' => true,
+ 'objectEditLink' => LinkHandler::getInstance()->getControllerLink(ObjectEditForm::class, ['id' => $object->objectID]),
+ ]);
+ ```
+
+## User Generated Links
+
+It is [recommended by search engines](https://support.google.com/webmasters/answer/96569) to mark up links within user generated content using the `rel="ugc"` attribute to indicate that they might be less trustworthy or spammy.
+
+WoltLab Suite 5.3 will automatically sets that attribute on external links during message output processing.
+Set the new `HtmlOutputProcessor::$enableUgc` property to `false` if the type of message is not user-generated content, but restricted to a set of trustworthy users.
+An example of such a type of message would be official news articles.
+
+If you manually generate links based off user input you need to specify the attribute yourself.
+The `$isUgc` attribute was added to [`StringUtil::getAnchorTag(string, string, bool, bool): string`](https://github.com/WoltLab/WCF/blob/af245d7b9bdb411a344f79c0a038350c1f103e70/wcfsetup/install/files/lib/util/StringUtil.class.php#L664-L673), allowing you to easily generate a correct anchor tag.
+
+If you need to specify additional HTML attributes for the anchor tag you can use the new [`StringUtil::getAnchorTagAttributes(string, bool): string`](https://github.com/WoltLab/WCF/blob/af245d7b9bdb411a344f79c0a038350c1f103e70/wcfsetup/install/files/lib/util/StringUtil.class.php#L691-L699) method to generate the anchor attributes that are dependent on the target URL.
+Specifically the attributes returned are the `class="externalURL"` attribute, the `rel="…"` attribute and the `target="…"` attribute.
+
+Within the template the [`{anchorAttributes}`](view_template-plugins.html#53-anchorattributes) template plugin is newly available.
+
+## Resource Management When Scaling Images
+
+It was discovered that the code holds references to scaled image resources for an unnecessarily long time, taking up memory.
+This becomes especially apparent when multiple images are scaled within a loop, reusing the same variable name for consecutive images.
+Unless the destination variable is explicitely cleared before processing the next image up to two images will be stored in memory concurrently.
+This possibly causes the request to exceed the memory limit or ImageMagick's internal resource limits, even if sufficient resources would have been available to scale the current image.
+
+Starting with WoltLab Suite 5.3 it is recommended to clear image handles as early as possible.
+The usual pattern of creating a thumbnail for an existing image would then look like this:
+
+```php
+<?php
+foreach ([ 200, 500 ] as $size) {
+ $adapter = ImageHandler::getInstance()->getAdapter();
+ $adapter->loadFile($src);
+ $thumbnail = $adapter->createThumbnail(
+ $size,
+ $size,
+ true
+ );
+ $adapter->writeImage($thumbnail, $destination);
+ // New: Clear thumbnail as soon as possible to free up the memory.
+ $thumbnail = null;
+}
+```
+
+Refer to [WoltLab/WCF#3505](https://github.com/WoltLab/WCF/pull/3505) for additional details.
+
+## Toggle for Accelerated Mobile Pages (AMP)
+
+Controllers delivering AMP versions of pages have to check for the new option `MODULE_AMP` and the templates of the non-AMP versions have to also check if the option is enabled before outputting the `<link rel="amphtml" />` element.
--- /dev/null
+---
+title: Migrating from WSC 5.2 - Templates and Languages
+sidebar: sidebar
+permalink: migration_wsc-52_templates.html
+folder: migration/wsc-52
+---
+
+## `{jslang}`
+
+Starting with WoltLab Suite 5.3 the `{jslang}` template plugin is available.
+`{jslang}` works like `{lang}`, with the difference that the result is automatically encoded for use within a single quoted JavaScript string.
+
+Before:
+
+```smarty
+<script>
+require(['Language', /* … */], function(Language, /* … */) {
+ Language.addObject({
+ 'app.foo.bar': '{lang}app.foo.bar{/lang}',
+ });
+
+ // …
+});
+</script>
+```
+
+After:
+
+```smarty
+<script>
+require(['Language', /* … */], function(Language, /* … */) {
+ Language.addObject({
+ 'app.foo.bar': '{jslang}app.foo.bar{/jslang}',
+ });
+
+ // …
+});
+</script>
+```
+
+## Template Plugins
+
+The [`{anchor}`](view_template-plugins.html#53-anchor), [`{plural}`](view_template-plugins.html#53-plural), and [`{user}`](view_template-plugins.html#53-user) template plugins have been added.
+
+## Notification Language Items
+
+In addition to using the new template plugins mentioned above, language items for notifications have been further simplified.
+
+As the whole notification is clickable now, all `a` elements have been replaced with `strong` elements in notification messages.
+
+The template code to output reactions has been simplified by introducing helper methods:
+
+```smarty
+{* old *}
+{implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}
+{* new *}
+{@$__wcf->getReactionHandler()->renderInlineList($reactions)}
+
+{* old *}
+<span title="{$like->getReactionType()->getTitle()}" class="jsTooltip">{@$like->getReactionType()->renderIcon()}</span>
+{* new *}
+{@$like->render()}
+```
+
+Similarly, showing labels is now also easier due to the new `render` method:
+
+```smarty
+{* old *}
+<span class="label badge{if $label->getClassNames()} {$label->getClassNames()}{/if}">{$label->getTitle()}</span>
+{* new *}
+{@$label->render()}
+```
+
+The commonly used template code
+
+```smarty
+{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count != 1}{if $count == 2 && !$guestTimesTriggered} and {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3}{if !$guestTimesTriggered} and {else}, {/if} {@$authors[2]->getAnchorTag()}{/if}{/if}{if $guestTimesTriggered} and {if $guestTimesTriggered == 1}a guest{else}guests{/if}{/if}{else}{@$authors[0]->getAnchorTag()}{if $guestTimesTriggered},{else} and{/if} {#$others} other users {if $guestTimesTriggered}and {if $guestTimesTriggered == 1}a guest{else}guests{/if}{/if}{/if}
+```
+
+in stacked notification messages can be replaced with a new language item:
+
+```smarty
+{@'wcf.user.notification.stacked.authorList'|language}
+```
+
+## Popovers
+
+Popovers provide additional information of the linked object when a user hovers over a link.
+We unified the approach for such links:
+
+1. The relevant DBO class implements `wcf\data\IPopoverObject`.
+2. The relevant DBO action class implements `wcf\data\IPopoverAction` and the `getPopover()` method returns an array with popover content.
+3. Globally available, `WoltLabSuite/Core/Controller/Popover` is initialized with the relevant data.
+4. Links are created with the `anchor` template plugin with an additional `class` attribute whose value is the return value of `IPopoverObject::getPopoverLinkClass()`.
+
+Example:
+
+```php
+class Foo extends DatabaseObject implements IPopoverObject {
+ public function getPopoverLinkClass() {
+ return 'fooLink';
+ }
+}
+
+class FooAction extends AbstractDatabaseObjectAction implements IPopoverAction {
+ public function validateGetPopover() {
+ // …
+ }
+
+ public function getPopover() {
+ return [
+ 'template' => '…',
+ ];
+ }
+}
+```
+
+```js
+require(['WoltLabSuite/Core/Controller/Popover'], function(ControllerPopover) {
+ ControllerPopover.init({
+ className: 'fooLink',
+ dboAction: 'wcf\\data\\foo\\FooAction',
+ identifier: 'com.woltlab.wcf.foo'
+ });
+});
+```
+
+```smarty
+{anchor object=$foo class='fooLink'}
+```
--- /dev/null
+---
+title: Migrating from WSC 5.3 - JavaScript
+sidebar: sidebar
+permalink: migration_wsc-53_javascript.html
+folder: migration/wsc-53
+---
+
+## `WCF_CLICK_EVENT`
+
+For event listeners on click events, `WCF_CLICK_EVENT` is deprecated and should no longer be used.
+Instead, use `click` directly:
+
+```javascript
+// before
+element.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+
+// after
+element.addEventListener('click', (ev) => this._click(ev));
+```
--- /dev/null
+---
+title: Migrating from WSC 5.3 - Third Party Libraries
+sidebar: sidebar
+permalink: migration_wsc-53_libraries.html
+folder: migration/wsc-53
+---
+
+## Guzzle
+
+The bundled Guzzle version was updated to Guzzle 7.
+No breaking changes are expected for simple uses.
+A detailed [Guzzle migration guide](https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#60-to-70) can be found in the Guzzle documentation.
+
+The explicit `sink` that was recommended in the [migration guide for WSC 5.2](migration_wsc-52_libraries.html#guzzle) can now be removed, as [the Guzzle issue #2735](https://github.com/guzzle/guzzle/issues/2735) was fixed in Guzzle 7.
+
+## Emogrifier / CSS Inliner
+
+The Emogrifier library was updated from version 2.2 to 5.0.
+This update comes with a breaking change, as the `Emogrifier` class was removed.
+With the updated Emogrifier library, the `CssInliner` class must be used instead.
+
+No compatibility layer was added for the `Emogrifier` class, as the Emogrifier library's purpose was to be used within the email subsystem of WoltLab Suite.
+In case you use Emogrifier directly within your own code, you will need to adjust the usage.
+Refer to the [Emogrifier CHANGELOG](https://github.com/MyIntervals/emogrifier/blob/v5.0.0/CHANGELOG.md) and [WoltLab/WCF #3738](https://github.com/WoltLab/WCF/pull/3738) if you need help making the necessary adjustments.
+
+If you only use Emogrifier indirectly by sending HTML mail via the email subsystem then you might notice unexpected visual changes due to the improved CSS support.
+Double check your CSS declarations and particularly the specificity of your selectors in these cases.
+
+## Constant Time Encoder
+
+WoltLab Suite 5.4 ships the [`paragonie/constant_time_encoding` library](https://github.com/paragonie/constant_time_encoding).
+It is recommended to use this library to perform encoding and decoding of secrets to prevent leaks via cache timing attacks.
+Refer to [the library author’s blog post](https://paragonie.com/blog/2016/06/constant-time-encoding-boring-cryptography-rfc-4648-and-you) for more background detail.
+
+For the common case of encoding the bytes taken from a CSPRNG in hexadecimal form, the required change would look like the following:
+
+Previously:
+
+```php
+<?php
+$encoded = hex2bin(random_bytes(16));
+```
+
+Now:
+
+```php
+<?php
+use ParagonIE\ConstantTime\Hex;
+
+// For security reasons you should add the backslash
+// to ensure you refer to the `random_bytes` function
+// within the global namespace and not a function
+// defined in the current namespace.
+$encoded = Hex::encode(\random_bytes(16));
+```
+
+Please refer to the documentation and source code of the `paragonie/constant_time_encoding` library to learn how to use the library with different encodings (e.g. base64).
--- /dev/null
+---
+title: Migrating from WSC 5.3 - PHP
+sidebar: sidebar
+permalink: migration_wsc-53_php.html
+folder: migration/wsc-53
+---
+
+## Minimum requirements
+
+The minimum requirements have been increased to the following:
+
+- **PHP:** 7.2.24
+- **MySQL:** 5.7.31 or 8.0.19
+- **MariaDB:** 10.1.44
+
+Most notably PHP 7.2 contains usable support for scalar types by the addition of nullable types in PHP 7.1 and parameter type widening in PHP 7.2.
+
+It is recommended to make use of scalar types and other newly introduced features whereever possible.
+Please refer to the PHP documentation for details.
+
+## Flood Control
+
+To prevent users from creating massive amounts of contents in short periods of time, i.e., spam, existing systems already use flood control mechanisms to limit the amount of contents created within a certain period of time.
+With WoltLab Suite 5.4, we have added a general API that manages such rate limiting.
+Leveraging this API is easily done.
+
+1. Register an object type for the definition `com.woltlab.wcf.floodControl`: `com.example.foo.myContent`.
+2. Whenever the active user creates content of this type, call
+ ```php
+ FloodControl::getInstance()->registerContent('com.example.foo.myContent');
+ ```
+ You should only call this method if the user creates the content themselves.
+ If the content is automatically created by the system, for example when copying / duplicating existing content, no activity should be registered.
+3. To check the last time when the active user created content of the relevant type, use
+ ```php
+ FloodControl::getInstance()->getLastTime('com.example.foo.myContent');
+ ```
+ If you want to limit the number of content items created within a certain period of time, for example within one day, use
+ ```php
+ $data = FloodControl::getInstance()->countContent('com.example.foo.myContent', new \DateInterval('P1D'));
+ // number of content items created within the last day
+ $count = $data['count'];
+ // timestamp when the earliest content item was created within the last day
+ $earliestTime = $data['earliestTime'];
+ ```
+ The method also returns `earliestTime` so that you can tell the user in the error message when they are able again to create new content of the relevant type.
+ {% include callout.html content="Flood control entries are only stored for 31 days and older entries are cleaned up daily." type="info" %}
+
+The previously mentioned methods of `FloodControl` use the active user and the current timestamp as reference point.
+`FloodControl` also provides methods to register content or check flood control for other registered users or for guests via their IP address.
+For further details on these methods, please refer to the [documentation in the FloodControl class](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/flood/FloodControl.class.php).
+
+{% include callout.html content="Do not interact directly with the flood control database table but only via the `FloodControl` class!" type="warning" %}
+
+## PHP Database API
+
+The PHP API to add and change database tables during package installations and updates in the `wcf\system\database\table` namespace now also supports renaming existing table columns with the new `IDatabaseTableColumn::renameTo()` method:
+
+```php
+PartialDatabaseTable::create('wcf1_test')
+ ->columns([
+ NotNullInt10DatabaseTableColumn::create('oldName')
+ ->renameTo('newName')
+ ]);
+```
+
+{% include callout.html content="Like with every change to existing database tables, packages can only rename columns that they installed." type="info" %}
+
+## Captcha
+
+The reCAPTCHA v1 implementation was completely removed.
+This includes the `\wcf\system\recaptcha\RecaptchaHandler` class (not to be confused with the one in the `captcha` namespace).
+
+The reCAPTCHA v1 endpoints have already been turned off by Google and always return a HTTP 404.
+Thus the implementation was completely non-functional even before this change.
+
+See [WoltLab/WCF#3781](https://github.com/WoltLab/WCF/pull/3781) for details.
+
+## Search
+
+The generic implementation in the `AbstractSearchEngine::parseSearchQuery()` method was dangerous, because it did not have knowledge about the search engine’s specifics.
+The implementation was completely removed: `AbstractSearchEngine::parseSearchQuery()` now always throws a `\BadMethodCallException`.
+
+If you implemented a custom search engine and relied on this method, you can inline the previous implementation to preserve existing behavior.
+You should take the time to verify the rewritten queries against the manual of the search engine to make sure it cannot generate malformed queries or security issues.
+
+See [WoltLab/WCF#3815](https://github.com/WoltLab/WCF/issues/3815) for details.
--- /dev/null
+---
+title: Migrating from WSC 5.3 - Session Handling and Authentication
+sidebar: sidebar
+permalink: migration_wsc-53_session.html
+folder: migration/wsc-53
+---
+
+WoltLab Suite 5.4 includes a completely refactored session handling.
+As long as you only interact with sessions via `WCF::getSession()`, especially when you perform read-only accesses, you should not notice any breaking changes.
+
+You might appreciate some of the new session methods if you process security sensitive data.
+
+## Summary and Concepts
+
+Most of the changes revolve around the removal of the legacy persistent login functionality and the assumption that every user has a single session only.
+Both aspects are related to each other.
+
+### Legacy Persistent Login
+
+The legacy persistent login was rather an automated login.
+Upon bootstrapping a session, it was checked whether the user had a cookie pair storing the user’s `userID` and (a single BCrypt hash of) the user’s password.
+If such a cookie pair exists and the BCrypt hash within the cookie matches the user’s password hash when hashed again, the session would immediately `changeUser()` to the respective user.
+
+This legacy persistent login was completely removed.
+Instead, any sessions that belong to an authenticated user will automatically be long-lived.
+These long-lived sessions expire no sooner than 14 days after the last activity, ensuring that the user continously stays logged in, provided that they visit the page at least once per fortnight.
+
+### Multiple Sessions
+
+To allow for a proper separation of these long-lived user sessions, WoltLab Suite now allows for multiple sessions per user.
+These sessions are completely unrelated to each other.
+Specifically, they do not share session variables and they expire independently.
+
+As the existing `wcf1_session` table is also used for the online lists and location tracking, it will be maintained on a best effort basis.
+It no longer stores any private session data.
+
+The actual sessions storing security sensitive information are in an unrelated location.
+They must only be accessed via the PHP API exposed by the `SessionHandler`.
+
+### Improved Authentication and Reauthentication
+
+WoltLab Suite 5.4 ships with multi-factor authentication support and a generic re-authentication implementation that can be used to verify the account owner’s presence.
+
+## Additions and Changes
+
+### Password Hashing
+
+WoltLab Suite 5.4 includes a new object-oriented password hashing framework that is modeled after PHP’s `password_*` API.
+Check [`PasswordAlgorithmManager`](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/authentication/password/PasswordAlgorithmManager.class.php) and [`IPasswordAlgorithm`](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/authentication/password/IPasswordAlgorithm.class.php) for details.
+
+The new default password hash is a standard BCrypt hash.
+All newly generated hashes in `wcf1_user.password` will now include a type prefix, instead of just passwords imported from other systems.
+
+### Session Storage
+
+The `wcf1_session` table will no longer be used for session storage.
+Instead, it is maintained for compatibility with existing online lists.
+
+The actual session storage is considered an implementation detail and you *must not* directly interact with the session tables.
+Future versions might support alternative session backends, such as Redis.
+
+{% include callout.html content="Do not interact directly with the session database tables but only via the `SessionHandler` class!" type="warning" %}
+
+### Reauthentication
+
+For security sensitive processing, you might want to ensure that the account owner is actually present instead of a third party accessing a session that was accidentally left logged in.
+
+WoltLab Suite 5.4 ships with a generic reauthentication framework.
+To request reauthentication within your controller you need to:
+
+1. Use the `wcf\system\user\authentication\TReauthenticationCheck` trait.
+2. Call:
+ ```php
+ $this->requestReauthentication(LinkHandler::getInstance()->getControllerLink(static::class, [
+ /* additional parameters */
+ ]));
+ ```
+
+`requestReauthentication()` will check if the user has recently authenticated themselves.
+If they did, the request proceeds as usual.
+Otherwise, they will be asked to reauthenticate themselves.
+After the successful authentication, they will be redirected to the URL that was passed as the first parameter (the current controller within the example).
+
+Details can be found in [WoltLab/WCF#3775](https://github.com/WoltLab/WCF/pull/3775).
+
+### Multi-factor Authentication
+
+To implement multi-factor authentication securely, WoltLab Suite 5.4 implements the concept of a “pending user change”.
+The user will not be logged in (i.e. `WCF::getUser()->userID` returns `null`) until they authenticate themselves with their second factor.
+
+Requesting multi-factor authentication is done on an opt-in basis for compatibility reasons.
+If you perform authentication yourself and do not trust the authentication source to perform multi-factor authentication itself, you will need to adjust your logic to request multi-factor authentication from WoltLab Suite:
+
+Previously:
+
+```php
+WCF::getSession()->changeUser($targetUser);
+```
+
+Now:
+
+```php
+$isPending = WCF::getSession()->changeUserAfterMultifactorAuthentication($targetUser);
+if ($isPending) {
+ // Redirect to the authentication form. The user will not be logged in.
+ // Note: Do not use `getControllerLink` to support both the frontend as well as the ACP.
+ HeaderUtil::redirect(LinkHandler::getInstance()->getLink('MultifactorAuthentication', [
+ 'url' => /* Return To */,
+ ]);
+ exit;
+}
+// Proceed as usual. The user will be logged in.
+```
+
+#### Adding Multi-factor Methods
+
+Adding your own multi-factor method requires the implementation of a single object type:
+
+```xml
+<type>
+ <name>com.example.multifactor.foobar</name>
+ <definitionname>com.woltlab.wcf.multifactor</definitionname>
+ <icon><!-- Font Awesome 4 Icon Name goes here. --></icon>
+ <priority><!-- Determines the sort order, higher priority will be preferred for authentication. --></priority>
+ <classname>wcf\system\user\multifactor\FoobarMultifactorMethod</classname>
+</type>
+```
+
+The given classname must implement the [`IMultifactorMethod`](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php) interface.
+
+As a self-contained example, you can find the initial implementation of the email multi-factor method in [WoltLab/WCF#3729](https://github.com/WoltLab/WCF/pull/3729).
+Please check [the version history](https://github.com/WoltLab/WCF/commits/master/wcfsetup/install/files/lib/system/user/multifactor/EmailMultifactorMethod.class.php) of the PHP class to make sure you do not miss important changes that were added later.
+
+{% include callout.html content="Multi-factor authentication is security sensitive.
+Make sure to carefully read the remarks in IMultifactorMethod for possible issues.
+Also make sure to carefully test your implementation against all sorts of incorrect input and consider attack vectors such as race conditions.
+It is strongly recommended to generously check the current state by leveraging assertions and exceptions." type="warning" %}
+
+## Deprecations and Removals
+
+### SessionHandler
+
+Most of the changes with regard to the new session handling happened in `SessionHandler`.
+Most notably, `SessionHandler` now is marked `final` to ensure proper encapsulation of data.
+
+A number of methods in `SessionHandler` are now deprecated and result in a noop.
+This change mostly affects methods that have been used to bootstrap the session, such as `setHasValidCookie()`.
+
+Additionally, accessing the following keys on the session is deprecated.
+They directly map to an existing method in another class and any uses can easily be updated:
+- `ipAddress`
+- `userAgent`
+- `requestURI`
+- `requestMethod`
+- `lastActivityTime`
+
+Refer to [the implementation](https://github.com/WoltLab/WCF/blob/439de4963c947c3569a0c584f795245f693155b0/wcfsetup/install/files/lib/system/session/SessionHandler.class.php#L168-L178) for details.
+
+### Cookies
+
+The `_userID`, `_password` and `_cookieHash` cookies will no longer be created nor consumed.
+
+### Virtual Sessions
+
+The virtual session logic existed to support multiple devices per single session in `wcf1_session`.
+Virtual sessions are no longer required with the refactored session handling.
+
+Anything related to virtual sessions has been completely removed as they are considered an implementation detail.
+This removal includes PHP classes and database tables.
+
+### Security Token Constants
+
+The security token constants are deprecated.
+Instead, the methods of `SessionHandler` should be used (e.g. `->getSecurityToken()`).
+Within templates, you should migrate to the `{csrfToken}` tag in place of `{@SECURITY_TOKEN_INPUT_TAG}`.
+The `{csrfToken}` tag is a drop-in replacement and was backported to WoltLab Suite 5.2+, allowing you to maintain compatibility across a broad range of versions.
+
+### PasswordUtil and Double BCrypt Hashes
+
+Most of the methods in PasswordUtil are deprecated in favor of the new password hashing framework.
--- /dev/null
+---
+title: Migrating from WSC 5.3 - Templates and Languages
+sidebar: sidebar
+permalink: migration_wsc-53_templates.html
+folder: migration/wsc-523
+---
+
+## `{csrfToken}`
+
+Going forward, any uses of the `SECURITY_TOKEN_*` constants should be avoided.
+To reference the CSRF token (“Security Token”) within templates, the `{csrfToken}` template plugin was added.
+
+Before:
+
+```smarty
+{@SECURITY_TOKEN_INPUT_TAG}
+{link controller="Foo"}t={@SECURITY_TOKEN}{/link}
+```
+
+After:
+
+```smarty
+{csrfToken}
+{link controller="Foo"}t={csrfToken type=url}{/link} {* The use of the CSRF token in URLs is discouraged.
+ Modifications should happen by means of a POST request. *}
+```
+
+The `{csrfToken}` plugin was backported to WoltLab Suite 5.2 and higher, allowing compatibility with a large range of WoltLab Suite branches.
+See [WoltLab/WCF #3612](https://github.com/WoltLab/WCF/pull/3612) for details.
--- /dev/null
+---
+title: Database PHP API
+permalink: package_database-php-api.html
+folder: package
+parent: package_pip
+---
+
+{% include callout.html content="Available since WoltLab Suite 5.2." type="info" %}
+
+While the [sql](package_pip_sql.html) package installation plugin supports adding and removing tables, columns, and indices, it is not able to handle cases where the added table, column, or index already exist.
+We have added a new PHP-based API to manipulate the database scheme which can be used in combination with the [script](package_pip_script.html) package installation plugin that skips parts that already exist:
+
+```php
+$tables = [
+ // TODO
+];
+
+(new DatabaseTableChangeProcessor(
+ /** @var ScriptPackageInstallationPlugin $this */
+ $this->installation->getPackage(),
+ $tables,
+ WCF::getDB()->getEditor())
+)->process();
+```
+
+All of the relevant components can be found in the `wcf\system\database\table` namespace.
+
+
+## Database Tables
+
+There are two classes representing database tables: `DatabaseTable` and `PartialDatabaseTable`.
+If a new table should be created, use `DatabaseTable`.
+In all other cases, `PartialDatabaseTable` should be used as it provides an additional save-guard against accidentally creating a new table by having a typo in the table name:
+If the tables does not already exist, a table represented by `PartialDatabaseTable` will cause an exception (while a `DatabaseTable` table will simply be created).
+
+To create a table, a `DatabaseTable` object with the table's name as to be created and table's columns, foreign keys and indices have to be specified:
+
+```php
+DatabaseTable::create('foo1_bar')
+ ->columns([
+ // columns
+ ])
+ ->foreignKeys([
+ // foreign keys
+ ])
+ ->indices([
+ // indices
+ ])
+```
+
+To update a table, the same code as above can be used, except for `PartialDatabaseTable` being used instead of `DatabaseTable`.
+
+To drop a table, only the `drop()` method has to be called:
+
+```php
+PartialDatabaseTable::create('foo1_bar')
+ ->drop()
+```
+
+
+## Columns
+
+To represent a column of a database table, you have to create an instance of the relevant column class found in the `wcf\system\database\table\column` namespace.
+Such instances are created similarly to database table objects using the `create()` factory method and passing the column name as the parameter.
+
+Every column type supports the following methods:
+
+- `defaultValue($defaultValue)` sets the default value of the column (default: none).
+- `drop()` to drop the column.
+- `notNull($notNull = true)` sets if the value of the column can be `NULL` (default: `false`).
+
+Depending on the specific column class implementing additional interfaces, the following methods are also available:
+
+- `IAutoIncrementDatabaseTableColumn::autoIncrement($autoIncrement = true)` sets if the value of the colum is auto-incremented.
+- `IDecimalsDatabaseTableColumn::decimals($decimals)` sets the number of decimals the column supports.
+- `IEnumDatabaseTableColumn::enumValues(array $values)` sets the predetermined set of valid values of the column.
+- `ILengthDatabaseTableColumn::length($length)` sets the (maximum) length of the column.
+
+Additionally, there are some additionally classes of commonly used columns with specific properties:
+
+- `DefaultFalseBooleanDatabaseTableColumn` (a `tinyint` column with length `1`, default value `0` and whose values cannot be `null`)
+- `DefaultTrueBooleanDatabaseTableColumn` (a `tinyint` column with length `0`, default value `0` and whose values cannot be `null`)
+- `NotNullInt10DatabaseTableColumn` (a `int` column with length `10` and whose values cannot be `null`)
+- `NotNullVarchar191DatabaseTableColumn` (a `varchar` column with length `191` and whose values cannot be `null`)
+- `NotNullVarchar255DatabaseTableColumn` (a `varchar` column with length `255` and whose values cannot be `null`)
+- `ObjectIdDatabaseTableColumn` (a `int` column with length `10`, whose values cannot be `null`, and whose values are auto-incremented)
+
+Examples:
+
+```php
+DefaultFalseBooleanDatabaseTableColumn::create('isDisabled')
+
+NotNullInt10DatabaseTableColumn::create('fooTypeID')
+
+SmallintDatabaseTableColumn::create('bar')
+ ->length(5)
+ ->notNull()
+```
+
+
+## Foreign Keys
+
+Foreign keys are represented by `DatabaseTableForeignKey` objects:
+
+```php
+DatabaseTableForeignKey::create()
+ ->columns(['fooID'])
+ ->referencedTable('wcf1_foo')
+ ->referencedColumns(['fooID'])
+ ->onDelete('CASCADE')
+```
+
+The supported actions for `onDelete()` and `onUpdate()` are `CASCADE`, `NO ACTION`, and `SET NULL`.
+To drop a foreign key, all of the relevant data to create the foreign key has to be present and the `drop()` method has to be called.
+
+`DatabaseTableForeignKey::create()` also supports the foreign key name as a parameter.
+If it is not present, `DatabaseTable::foreignKeys()` will automatically set one based on the foreign key's data.
+
+
+## Indices
+
+Indices are represented by `DatabaseTableIndex` objects:
+
+```php
+DatabaseTableIndex::create()
+ ->type(DatabaseTableIndex::UNIQUE_TYPE)
+ ->columns(['fooID'])
+```
+
+There are four different types: `DatabaseTableIndex::DEFAULT_TYPE` (default), `DatabaseTableIndex::PRIMARY_TYPE`, `DatabaseTableIndex::UNIQUE_TYPE`, and `DatabaseTableIndex::FULLTEXT_TYPE`.
+For primary keys, there is also the `DatabaseTablePrimaryIndex` class which automatically sets the type to `DatabaseTableIndex::PRIMARY_TYPE`.
+To drop a index, all of the relevant data to create the index has to be present and the `drop()` method has to be called.
+
+`DatabaseTableIndex::create()` also supports the index name as a parameter.
+If it is not present, `DatabaseTable::indices()` will automatically set one based on the index data.
--- /dev/null
+---
+title: package.xml
+sidebar: sidebar
+permalink: package_package-xml.html
+folder: package
+---
+
+The `package.xml` is the core component of every package.
+It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.example.package" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/package.xsd">
+ <packageinformation>
+ <packagename>Simple Package</packagename>
+ <packagedescription>A simple package to demonstrate the package system of WoltLab Suite Core</packagedescription>
+ <version>1.0.0</version>
+ <date>2016-12-18</date>
+ </packageinformation>
+
+ <authorinformation>
+ <author>YOUR NAME</author>
+ <authorurl>http://www.example.com</authorurl>
+ </authorinformation>
+
+ <requiredpackages>
+ <requiredpackage minversion="3.0.0">com.woltlab.wcf</requiredpackage>
+ </requiredpackages>
+
+ <excludedpackages>
+ <excludedpackage version="6.0.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+ </excludedpackages>
+
+ <instructions type="install">
+ <instruction type="file" />
+ <instruction type="template">templates.tar</instruction>
+ </instructions>
+</package>
+```
+
+
+## Elements
+
+### `<package>`
+
+The root node of every `package.xml` it contains the reference to the namespace and the location of the XML Schema Definition (XSD).
+
+The attribute `name` is the most important part, it holds the unique package identifier and is mandatory.
+It is based upon your domain name and the package name of your choice.
+
+For example WoltLab Suite Forum (formerly know an WoltLab Burning Board and usually abbreviated as `wbb`) is created by WoltLab which owns the domain `woltlab.com`.
+The resulting package identifier is `com.woltlab.wbb` (`<tld>.<domain>.<packageName>`).
+
+### `<packageinformation>`
+
+Holds the entire meta data of the package.
+
+#### `<packagename>`
+
+This is the actual package name displayed to the end user, this can be anything you want, try to keep it short.
+It supports the attribute `languagecode` which allows you to provide the package name in different languages, please be aware that if it is not present, `en` (English) is assumed:
+
+```xml
+<packageinformation>
+ <packagename>Simple Package</packagename>
+ <packagename languagecode="de">Einfaches Paket</packagename>
+</packageinformation>
+```
+
+#### `<packagedescription>`
+
+Brief summary of the package, use it to explain what it does since the package name might not always be clear enough.
+The attribute `languagecode` is available here too, please reference to [`<packagename>`](#packageName) for details.
+
+#### `<version>`
+
+The package's version number, this is a string consisting of three numbers separated with a dot and optionally followed by a keyword (must be followed with another number).
+
+The possible keywords are:
+
+- Alpha/dev (both is regarded to be the same)
+- Beta
+- RC (release candidate)
+- pl (patch level)
+
+Valid examples:
+
+- 1.0.0
+- 1.12.13 Alpha 19
+- 7.0.0 pl 3
+
+Invalid examples:
+
+- 1.0.0 Beta (keyword Beta must be followed by a number)
+- 2.0 RC 3 (version number must consists of 3 blocks of numbers)
+- 1.2.3 dev 4.5 (4.5 is not an integer, 4 or 5 would be valid but not the fraction)
+
+#### `<date>`
+
+Must be a valid [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) date, e.g. `2013-12-27`.
+
+### `<authorinformation>`
+
+Holds meta data regarding the package's author.
+
+#### `<author>`
+
+Can be anything you want.
+
+#### `<authorurl>`
+
+> (optional)
+
+URL to the author's website.
+
+### `<requiredpackages>`
+
+A list of packages including their version required for this package to work.
+
+#### `<requiredpackage>`
+
+Example:
+
+```xml
+<requiredpackage minversion="2.0.0" file="requirements/com.woltlab.wcf.tar">com.woltlab.wcf</requiredpackage>
+```
+
+The attribute `minversion` must be a valid version number as described in [`<version>`](#version).
+The `file` attribute is optional and specifies the location of the required package's archive relative to the `package.xml`.
+
+### `<optionalpackage>`
+
+A list of optional packages which can be selected by the user at the very end of the installation process.
+
+#### `<optionalpackage>`
+
+Example:
+
+```xml
+<optionalpackage file="optionals/com.woltlab.wcf.moderatedUserGroup.tar">com.woltlab.wcf.moderatedUserGroup</optionalpackage>
+```
+
+The `file` attribute specifies the location of the optional package's archive relative to the `package.xml`.
+
+### `<excludedpackages>`
+
+List of packages which conflict with this package. It is not possible to install it if any of the specified packages is installed. In return you cannot install an excluded package if this package is installed.
+
+#### `<excludedpackage>`
+
+Example:
+
+```xml
+<excludedpackage version="3.1.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+```
+
+The attribute `version` must be a valid version number as described in the [\<version\>](#version) section. In the example above it will be impossible to install this package in WoltLab Suite Core 3.1.0 Alpha 1 or higher.
+
+### `<compatibility>`
+{% include callout.html content="Available since WoltLab Suite 3.1" type="info" %}
+{% include callout.html content="With the release of WoltLab Suite 5.2 the API versions were abolished. Instead of using API versions packages should exclude version `6.0.0 Alpha 1` of `com.woltlab.wcf` going forward." type="warning" %}
+
+WoltLab Suite 3.1 introduced a new versioning system that focused around the API compatibility and is intended to replace the `<excludedpackage>` instruction for the Core for most plugins.
+
+The `<compatibility>`-tag holds a list of compatible API versions, and while only a single version is available at the time of writing, future versions will add more versions with backwards-compatibility in mind.
+
+Example:
+
+```xml
+<compatibility>
+ <api version="2018" />
+</compatibility>
+```
+
+#### Existing API versions
+
+| WoltLab Suite Core | API-Version | Backwards-Compatible to API-Version |
+|---|---|---|
+| 3.1 | 2018 | n/a |
+
+### `<instructions>`
+
+List of instructions to be executed upon install or update. The order is important, the topmost `<instruction>` will be executed first.
+
+#### `<instructions type="install">`
+
+List of instructions for a new installation of this package.
+
+#### `<instructions type="update" fromversion="…">`
+
+The attribute `fromversion` must be a valid version number as described in the [\<version\>](#version) section and specifies a possible update from that very version to the package's version.
+
+{% include callout.html content="The installation process will pick exactly one update instruction, ignoring everything else. Please read the explanation below!" type="warning" %}
+
+Example:
+
+- Installed version: `1.0.0`
+- Package version: `1.0.2`
+
+```xml
+<instructions type="update" fromversion="1.0.0">
+ <!-- … -->
+</instructions>
+<instructions type="update" fromversion="1.0.1">
+ <!-- … -->
+</instructions>
+```
+
+In this example WoltLab Suite Core will pick the first update block since it allows an update from `1.0.0 -> 1.0.2`.
+The other block is not considered, since the currently installed version is `1.0.0`. After applying the update block (`fromversion="1.0.0"`), the version now reads `1.0.2`.
+
+#### `<instruction>`
+
+Example:
+
+```xml
+<instruction type="objectTypeDefinition">objectTypeDefinition.xml</instruction>
+```
+
+The attribute `type` specifies the instruction type which is used to determine the package installation plugin (PIP) invoked to handle its value.
+The value must be a valid file relative to the location of `package.xml`.
+Many PIPs provide default file names which are used if no value is given:
+
+```xml
+<instruction type="objectTypeDefinition" />
+```
+
+There is a [list of all default PIPs](package_pip.html) available.
+
+{% include callout.html content="Both the `type`-attribute and the element value are case-sensitive. Windows does not care if the file is called `objecttypedefinition.xml` but was referenced as `objectTypeDefinition.xml`, but both Linux and Mac systems will be unable to find the file." type="warning" %}
+
+In addition to the `type` attribute, an optional `run` attribute (with `standalone` as the only valid value) is supported which forces the installation to execute this PIP in an isolated request, allowing a single, resource-heavy PIP to execute without encountering restrictions such as PHP’s `memory_limit` or `max_execution_time`:
+
+```xml
+<instruction type="file" run="standalone" />
+```
+
+#### `<void/>`
+
+Sometimes a package update should only adjust the metadata of the package, for example, an optional package was added.
+However, WoltLab Suite Core requires that the list of `<instructions>` is non-empty.
+Instead of using a dummy `<instruction>` that idempotently updates some PIP, the `<void/>` tag can be used for this use-case.
+
+Using the `<void/>` tag is only valid for `<instructions type="update">` and must not be accompanied by other `<instruction>` tags.
+
+Example:
+
+```xml
+<instructions type="update" fromversion="1.0.0">
+ <void/>
+</instructions>
+```
--- /dev/null
+---
+title: Package Installation Plugins
+sidebar: sidebar
+permalink: package_pip.html
+folder: package
+---
+
+Package Installation Plugins (PIPs) are interfaces to deploy and edit content as well as components.
+
+{% include callout.html content="For XML-based PIPs: `<![CDATA[]]>` must be used for language items and page contents. In all other cases it may only be used when necessary." type="info" %}
+
+## Built-In PIPs
+
+| Name | Description |
+|------|-------------|
+| [aclOption][package_pip_acl-option] | Customizable permissions for individual objects |
+| [acpMenu][package_pip_acp-menu] | Admin panel menu categories and items |
+| [acpSearchProvider][package_pip_acp-search-provider] | Data provider for the admin panel search |
+| [acpTemplate][package_pip_acp-template] | Admin panel templates |
+| [bbcode][package_pip_bbcode] | BBCodes for rich message formatting |
+| [box][package_pip_box] | Boxes that can be placed anywhere on a page |
+| [clipboardAction][package_pip_clipboard_action] | Perform bulk operations on marked objects |
+| [coreObject][package_pip_core-object] | Access Singletons from within the template |
+| [cronjob][package_pip_cronjob] | Periodically execute code with customizable intervals |
+| [eventListener][package_pip_event-listener] | Register listeners for the event system |
+| [file][package_pip_file] | Deploy any type of files with the exception of templates |
+| [language][package_pip_language] | Language items |
+| [mediaProvider][package_pip_media-provider] | Detect and convert links to media providers |
+| [menu][package_pip_menu] | Side-wide and custom per-page menus |
+| [menuItem][package_pip_menu-item] | Menu items for menus created through the menu PIP |
+| [objectType][package_pip_object-type] | Flexible type registry based on definitions |
+| [objectTypeDefinition][package_pip_object-type-definition] | Groups objects and classes by functionality |
+| [option][package_pip_option] | Side-wide configuration options |
+| [page][package_pip_page] | Register page controllers and text-based pages |
+| [pip][package_pip_pip] | Package Installation Plugins |
+| [script][package_pip_script] | Execute arbitrary PHP code during installation, update and uninstallation |
+| [smiley][package_pip_smiley] | Smileys |
+| [sql][package_pip_sql] | Execute SQL instructions using a MySQL-flavored syntax (also see [database PHP API](package_database-php-api.html)) |
+| [style][package_pip_style] | Style |
+| [template][package_pip_template] | Frontend templates |
+| [templateListener][package_pip_template-listener] | Embed template code into templates without altering the original |
+| [userGroupOption][package_pip_user-group-option] | Permissions for user groups |
+| [userMenu][package_pip_user-menu] | User menu categories and items |
+| [userNotificationEvent][package_pip_user-notification-event] | Events of the user notification system |
+| [userOption][package_pip_user-option] | User settings |
+| [userProfileMenu][package_pip_user-profile-menu] | User profile tabs |
+
+{% include links.html %}
--- /dev/null
+---
+title: ACL Option Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_acl-option.html
+folder: package/pip
+parent: package_pip
+---
+
+Add customizable permissions for individual objects.
+
+## Option Components
+
+Each acl option is described as an `<option>` element with the mandatory attribute `name`.
+
+### `<categoryname>`
+
+<span class="label label-info">Optional</span>
+
+The name of the acl option category to which the option belongs.
+
+### `<objecttype>`
+
+The name of the acl object type (of the object type definition `com.woltlab.wcf.acl`).
+
+
+## Category Components
+
+Each acl option category is described as an `<category>` element with the mandatory attribute `name` that should follow the naming pattern `<permissionName>` or `<permissionType>.<permissionName>`, with `<permissionType>` generally having `user` or `mod` as value.
+
+### `<objecttype>`
+
+The name of the acl object type (of the object type definition `com.woltlab.wcf.acl`).
+
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/aclOption.xsd">
+ <import>
+ <categories>
+ <category name="user.example">
+ <objecttype>com.example.wcf.example</objecttype>
+ </category>
+ <category name="mod.example">
+ <objecttype>com.example.wcf.example</objecttype>
+ </category>
+ </categories>
+
+ <options>
+ <option name="canAddExample">
+ <categoryname>user.example</categoryname>
+ <objecttype>com.example.wcf.example</objecttype>
+ </option>
+ <option name="canDeleteExample">
+ <categoryname>mod.example</categoryname>
+ <objecttype>com.example.wcf.example</objecttype>
+ </option>
+ </options>
+ </import>
+
+ <delete>
+ <optioncategory name="old.example">
+ <objecttype>com.example.wcf.example</objecttype>
+ </optioncategory>
+ <option name="canDoSomethingWithExample">
+ <objecttype>com.example.wcf.example</objecttype>
+ </option>
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: ACP Menu Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_acp-menu.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new ACP menu items.
+
+## Components
+
+Each item is described as an `<acpmenuitem>` element with the mandatory attribute `name`.
+
+### `<parent>`
+
+<span class="label label-info">Optional</span>
+
+The item’s parent item.
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Specifies the order of this item within the parent item.
+
+### `<controller>`
+
+The fully qualified class name of the target controller.
+If not specified this item serves as a category.
+
+### `<link>`
+
+Additional components if `<controller>` is set,
+the full external link otherwise.
+
+### `<icon>`
+
+{% include tip.html content="Use an icon only for top-level and 4th-level items." %}
+
+Name of the Font Awesome icon class.
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the tab to be shown.
+
+### `<permissions>`
+
+<span class="label label-info">Optional</span>
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the tab to be shown.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/acpMenu.xsd">
+ <import>
+ <acpmenuitem name="foo.acp.menu.link.example">
+ <parent>wcf.acp.menu.link.application</parent>
+ </acpmenuitem>
+
+ <acpmenuitem name="foo.acp.menu.link.example.list">
+ <controller>foo\acp\page\ExampleListPage</controller>
+ <parent>foo.acp.menu.link.example</parent>
+ <permissions>admin.foo.canManageExample</permissions>
+ <showorder>1</showorder>
+ </acpmenuitem>
+
+ <acpmenuitem name="foo.acp.menu.link.example.add">
+ <controller>foo\acp\form\ExampleAddForm</controller>
+ <parent>foo.acp.menu.link.example.list</parent>
+ <permissions>admin.foo.canManageExample</permissions>
+ <icon>fa-plus</icon>
+ </acpmenuitem>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: ACP Search Provider Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_acp-search-provider.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers data provider for the admin panel search.
+
+## Components
+
+Each acp search result provider is described as an `<acpsearchprovider>` element with the mandatory attribute `name`.
+
+### `<classname>`
+
+The name of the class providing the search results,
+the class has to implement the `wcf\system\search\acp\IACPSearchResultProvider` interface.
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Determines at which position of the search result list the provided results are shown.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/acpSearchProvider.xsd">
+ <import>
+ <acpsearchprovider name="com.woltlab.wcf.example">
+ <classname>wcf\system\search\acp\ExampleACPSearchResultProvider</classname>
+ <showorder>1</showorder>
+ </acpsearchprovider>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: ACP Template Installation Plugin
+sidebar: sidebar
+permalink: package_pip_acp-template.html
+folder: package/pip
+parent: package_pip
+---
+
+Add templates for acp pages and forms by providing an archive containing the template files.
+
+{% include callout.html content="You cannot overwrite acp templates provided by other packages." type="warning" %}
+
+
+## Archive
+
+The `acpTemplate` package installation plugins expects a `.tar` (recommended) or `.tar.gz` archive.
+The templates must all be in the root of the archive.
+Do not include any directories in the archive.
+The file path given in the `instruction` element as its value must be relative to the `package.xml` file.
+
+
+## Attributes
+
+### `application`
+
+The `application` attribute determines to which application the installed acp templates belong and thus in which directory the templates are installed.
+The value of the `application` attribute has to be the abbreviation of an installed application.
+If no `application` attribute is given, the following rules are applied:
+
+- If the package installing the acp templates is an application, then the templates will be installed in this application's directory.
+- If the package installing the acp templates is no application, then the templates will be installed in WoltLab Suite Core's directory.
+
+
+## Example in `package.xml`
+
+```xml
+<instruction type="acpTemplate" />
+<!-- is the same as -->
+<instruction type="acpTemplate">acptemplates.tar</instruction>
+
+<!-- if an application "com.woltlab.example" is being installed, the following lines are equivalent -->
+<instruction type="acpTemplate" />
+<instruction type="acpTemplate" application="example" />
+```
--- /dev/null
+---
+title: BBCode Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_bbcode.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new BBCodes.
+
+## Components
+
+Each bbcode is described as an `<bbcode>` element with the mandatory attribute `name`.
+The `name` attribute must contain alphanumeric characters only and is exposed to the user.
+
+### `<htmlopen>`
+
+{% include callout.html content="Optional: Must not be provided if the BBCode is being processed a PHP class (`<classname>`)." type="info" %}
+
+The contents of this tag are literally copied into the opening tag of the bbcode.
+
+### `<htmlclose>`
+
+{% include callout.html content="Optional: Must not be provided if `<htmlopen>` is not given." type="info" %}
+
+Must match the `<htmlopen>` tag.
+Do not provide for self-closing tags.
+
+### `<classname>`
+
+The name of the class providing the bbcode output,
+the class has to implement the `wcf\system\bbcode\IBBCode` interface.
+
+BBCodes can be statically converted to HTML during input processing using a
+`wcf\system\html\metacode\converter\*MetaConverter` class. This class does not
+need to be registered.
+
+### `<wysiwygicon>`
+
+<span class="label label-info">Optional</span>
+
+Name of the Font Awesome icon class or path to a `gif`, `jpg`, `jpeg`, `png`, or `svg` image (placed inside the `icon/` directory) to show in the editor toolbar.
+
+### `<buttonlabel>`
+
+{% include callout.html content="Optional: Must be provided if an icon is given." type="info" %}
+
+Explanatory text to show when hovering the icon.
+
+### `<sourcecode>`
+
+{% include warning.html content="Do not set this to `1` if you don't specify a PHP class for processing. You must perform XSS sanitizing yourself!" %}
+
+If set to `1` contents of this BBCode will not be interpreted,
+but literally passed through instead.
+
+### `<isBlockElement>`
+
+Set to `1` if the output of this BBCode is a HTML block element (according to the HTML specification).
+
+### `<attributes>`
+
+Each bbcode is described as an `<attribute>` element with the mandatory attribute `name`.
+The `name` attribute is a 0-indexed integer.
+
+#### `<html>`
+
+{% include callout.html content="Optional: Must not be provided if the BBCode is being processed a PHP class (`<classname>`)." type="info" %}
+
+The contents of this tag are copied into the opening tag of the bbcode.
+`%s` is replaced by the attribute value.
+
+#### `<validationpattern>`
+
+<span class="label label-info">Optional</span>
+
+Defines a regular expression that is used to validate the value of the attribute.
+
+#### `<required>`
+
+<span class="label label-info">Optional</span>
+
+Specifies whether this attribute must be provided.
+
+#### `<usetext>`
+
+<span class="label label-info">Optional</span>
+{% include callout.html content="Should only be set to `1` for the attribute with name `0`." type="info" %}
+
+Specifies whether the text content of the BBCode should become this attribute's value.
+
+## Example
+
+```
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/bbcode.xsd">
+ <import>
+ <bbcode name="foo">
+ <classname>wcf\system\bbcode\FooBBCode</classname>
+ <attributes>
+ <attribute name="0">
+ <validationpattern>^\d+$</validationpattern>
+ <required>1</required>
+ </attribute>
+ </attributes>
+ </bbcode>
+
+ <bbcode name="example">
+ <htmlopen>div</htmlopen>
+ <htmlclose>div</htmlclose>
+ <isBlockElement>1</isBlockElement>
+ <wysiwygicon>fa-bath</wysiwygicon>
+ <buttonlabel>wcf.editor.button.example</buttonlabel>
+ </bbcode>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: Box Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_box.html
+folder: package/pip
+parent: package_pip
+---
+
+Deploy and manage boxes that can be placed anywhere on the site, they come in two flavors: system and content-based.
+
+## Components
+
+Each item is described as a `<box>` element with the mandatory attribute `name` that should follow the naming pattern `<packageIdentifier>.<BoxName>`, e.g. `com.woltlab.wcf.RecentActivity`.
+
+### `<name>`
+
+{% include languageCode.html %}
+
+The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple `<name>` elements.
+
+### `<boxType>`
+
+#### `system`
+
+The special `system` type is reserved for boxes that pull their properties and content from a registered PHP class. Requires the `<objectType>` element.
+
+#### `html`, `text` or `tpl`
+
+Provide arbitrary content, requires the `<content>` element.
+
+### `<objectType>`
+
+Required for boxes with `boxType = system`, must be registered through [the objectType PIP](package_pip_object-type.html) for the definition `com.woltlab.wcf.boxController`.
+
+### `<position>`
+
+The default display position of this box, can be any of the following:
+
+* bottom
+* contentBottom
+* contentTop
+* footer
+* footerBoxes
+* headerBoxes
+* hero
+* sidebarLeft
+* sidebarRight
+* top
+
+#### Placeholder Positions
+
+{% include image.html file="boxPlaceholders.png" alt="Visual illustration of placeholder positions" %}
+
+### `<showHeader>`
+
+Setting this to `0` will suppress display of the box title, useful for boxes containing advertisements or similar. Defaults to `1`.
+
+### `<visibleEverywhere>`
+
+Controls the display on all pages (`1`) or none (`0`), can be used in conjunction with `<visibilityExceptions>`.
+
+### `<visibilityExceptions>`
+
+Inverts the `<visibleEverywhere>` setting for the listed pages only.
+
+### `<cssClassName>`
+
+Provide a custom CSS class name that is added to the menu container, allowing further customization of the menu's appearance.
+
+### `<content>`
+
+{% include languageCode.html %}
+#### `<title>`
+
+The title element is required and controls the box title shown to the end users.
+
+#### `<content>`
+
+The content that should be used to populate the box, only used and required if the `boxType` equals `text`, `html` and `tpl`.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/box.xsd">
+ <import>
+ <box identifier="com.woltlab.wcf.RecentActivity">
+ <name language="de">Letzte Aktivitäten</name>
+ <name language="en">Recent Activities</name>
+ <boxType>system</boxType>
+ <objectType>com.woltlab.wcf.recentActivityList</objectType>
+ <position>contentBottom</position>
+ <showHeader>0</showHeader>
+ <visibleEverywhere>0</visibleEverywhere>
+ <visibilityExceptions>
+ <page>com.woltlab.wcf.Dashboard</page>
+ </visibilityExceptions>
+ <limit>10</limit>
+
+ <content language="de">
+ <title>Letzte Aktivitäten</title>
+ </content>
+ <content language="en">
+ <title>Recent Activities</title>
+ </content>
+ </box>
+ </import>
+
+ <delete>
+ <box identifier="com.woltlab.wcf.RecentActivity" />
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: Clipboard Action Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_clipboard_action.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers clipboard actions.
+
+## Components
+
+Each clipboard action is described as an `<action>` element with the mandatory attribute `name`.
+
+### `<actionclassname>`
+
+The name of the class used by the clipboard API to process the concrete action.
+The class has to implement the `wcf\system\clipboard\action\IClipboardAction` interface, best by extending `wcf\system\clipboard\action\AbstractClipboardAction`.
+
+### `<pages>`
+
+Element with `<page>` children whose value contains the class name of the controller of the page on which the clipboard action is available.
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Determines at which position of the clipboard action list the action is shown.
+
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/clipboardAction.xsd">
+ <import>
+ <action name="delete">
+ <actionclassname>wcf\system\clipboard\action\ExampleClipboardAction</actionclassname>
+ <showorder>1</showorder>
+ <pages>
+ <page>wcf\acp\page\ExampleListPage</page>
+ </pages>
+ </action>
+ <action name="foo">
+ <actionclassname>wcf\system\clipboard\action\ExampleClipboardAction</actionclassname>
+ <showorder>2</showorder>
+ <pages>
+ <page>wcf\acp\page\ExampleListPage</page>
+ </pages>
+ </action>
+ <action name="bar">
+ <actionclassname>wcf\system\clipboard\action\ExampleClipboardAction</actionclassname>
+ <showorder>3</showorder>
+ <pages>
+ <page>wcf\acp\page\ExampleListPage</page>
+ </pages>
+ </action>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: Core Object Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_core-object.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers `wcf\system\SingletonFactory` objects to be accessible in templates.
+
+## Components
+
+Each item is described as a `<coreobject>` element with the mandatory element `objectname`.
+
+### `<objectname>`
+
+The fully qualified class name of the class.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/coreObject.xsd">
+ <import>
+ <coreobject>
+ <objectname>wcf\system\example\ExampleHandler</objectname>
+ </coreobject>
+ </import>
+</data>
+```
+
+This object can be accessed in templates via `$__wcf->getExampleHandler()` (in general: the method name begins with `get` and ends with the unqualified class name).
--- /dev/null
+---
+title: Cronjob Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_cronjob.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new cronjobs.
+The cronjob schedular works similar to the `cron(8)` daemon, which might not available to web applications on regular webspaces.
+The main difference is that WoltLab Suite’s cronjobs do not guarantee execution at the specified points in time:
+WoltLab Suite’s cronjobs are triggered by regular visitors in an AJAX request, once the next execution point lies in the past.
+
+## Components
+
+Each cronjob is described as an `<cronjob>` element with the mandatory attribute `name`.
+
+### `<classname>`
+
+The name of the class providing the cronjob's behaviour,
+the class has to implement the `wcf\system\cronjob\ICronjob` interface.
+
+### `<description>`
+
+{% include languageCode.html requirement="optional" %}
+
+Provides a human readable description for the administrator.
+
+### `<start*>`
+
+All of the five `startMinute`, `startHour`, `startDom` (Day Of Month), `startMonth`, `startDow` (Day Of Week) are required.
+They correspond to the fields in `crontab(5)` of a cron daemon and accept the same syntax.
+
+### `<canBeEdited>`
+
+Controls whether the administrator may edit the fields of the cronjob.
+
+### `<canBeDisabled>`
+
+Controls whether the administrator may disable the cronjob.
+
+### `<options>`
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the template listener to be executed.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/cronjob.xsd">
+ <import>
+ <cronjob name="com.example.package.example">
+ <classname>wcf\system\cronjob\ExampleCronjob</classname>
+ <description>Serves as an example</description>
+ <description language="de">Stellt ein Beispiel dar</description>
+ <startminute>0</startminute>
+ <starthour>2</starthour>
+ <startdom>*/2</startdom>
+ <startmonth>*</startmonth>
+ <startdow>*</startdow>
+ <canbeedited>1</canbeedited>
+ <canbedisabled>1</canbedisabled>
+ </cronjob>
+ </import>
+</data>
+```
+
--- /dev/null
+---
+title: Event Listener Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_event-listener.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers event listeners.
+An explanation of events and event listeners can be found [here](php_api_events.html).
+
+## Components
+
+Each event listener is described as an `<eventlistener>` element with a `name` attribute.
+As the `name` attribute has only be introduced with WSC 3.0, it is not yet mandatory to allow backwards compatibility.
+If `name` is not given, the system automatically sets the name based on the id of the event listener in the database.
+
+### `<eventclassname>`
+
+The event class name is the name of the class in which the event is fired.
+
+### `<eventname>`
+
+The event name is the name given when the event is fired to identify different events within the same class.
+You can either give a single event name or a comma-separated list of event names in which case the event listener listens to all of the listed events.
+
+### `<listenerclassname>`
+
+The listener class name is the name of the class which is triggered if the relevant event is fired.
+The PHP class has to implement the `wcf\system\event\listener\IParameterizedEventListener` interface.
+
+{% include callout.html content="Legacy event listeners are only required to implement the deprecated `wcf\system\event\IEventListener` interface. When writing new code or update existing code, you should always implement the `wcf\system\event\listener\IParameterizedEventListener` interface!" type="warning" %}
+
+### `<inherit>`
+
+The inherit value can either be `0` (default value if the element is omitted) or `1` and determines if the event listener is also triggered for child classes of the given event class name.
+This is the case if `1` is used as the value.
+
+### `<environment>`
+
+The value of the environment element must be one of `user`, `admin` or `all` and defaults to `user` if no value is given.
+The value determines if the event listener will be executed in the frontend (`user`), the backend (`admin`) or both (`all`).
+
+### `<nice>`
+
+The nice value element can contain an integer value out of the interval `[-128,127]` with `0` being the default value if the element is omitted.
+The nice value determines the execution order of event listeners.
+Event listeners with smaller nice values are executed first.
+If the nice value of two event listeners is equal, they are sorted by the listener class name.
+
+{% include callout.html content="If you pass a value out of the mentioned interval, the value will be adjusted to the closest value in the interval." type="info" %}
+
+### `<options>`
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the event listener to be executed.
+
+### `<permissions>`
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the event listener to be executed.
+
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/eventListener.xsd">
+ <import>
+ <eventlistener name="inheritedAdminExample">
+ <eventclassname>wcf\acp\form\UserAddForm</eventclassname>
+ <eventname>assignVariables,readFormParameters,save,validate</eventname>
+ <listenerclassname>wcf\system\event\listener\InheritedAdminExampleListener</listenerclassname>
+ <inherit>1</inherit>
+ <environment>admin</environment>
+ </eventlistener>
+
+ <eventlistener name="nonInheritedUserExample">
+ <eventclassname>wcf\form\SettingsForm</eventclassname>
+ <eventname>assignVariables</eventname>
+ <listenerclassname>wcf\system\event\listener\NonInheritedUserExampleListener</listenerclassname>
+ </eventlistener>
+ </import>
+
+ <delete>
+ <eventlistener name="oldEventListenerName" />
+ </delete>
+</data>
+
+```
--- /dev/null
+---
+title: File Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_file.html
+folder: package/pip
+parent: package_pip
+---
+
+Adds any type of files with the exception of templates.
+
+{% include callout.html content="You cannot overwrite files provided by other packages." type="warning" %}
+
+The `application` attribute behaves like it does for [acp templates](package_pip_acp-template.html#application).
+
+
+## Archive
+
+The `acpTemplate` package installation plugins expects a `.tar` (recommended) or `.tar.gz` archive.
+The file path given in the `instruction` element as its value must be relative to the `package.xml` file.
+
+
+## Example in `package.xml`
+
+```xml
+<instruction type="file" />
+<!-- is the same as -->
+<instruction type="file">files.tar</instruction>
+
+<!-- if an application "com.woltlab.example" is being installed, the following lines are equivalent -->
+<instruction type="file" />
+<instruction type="file" application="example" />
+
+<!-- if the same application wants to install additional files, in WoltLab Suite Core's directory: -->
+<instruction type="file" application="wcf">files_wcf.tar</instruction>
+```
--- /dev/null
+---
+title: Language Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_language.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new language items.
+
+## Components
+
+{% include languageCode.html attribute="languagecode" %}
+
+The top level `<language>` node must contain a `languagecode` attribute.
+
+### `<category>`
+
+Each category must contain a `name` attribute containing two or three components consisting of alphanumeric character only, separated by a single full stop (`.`, U+002E).
+
+#### `<item>`
+
+Each language item must contain a `name` attribute containing at least three components consisting of alphanumeric character only, separated by a single full stop (`.`, U+002E). The `name` of the parent `<category>` node followed by a full stop must be a prefix of the `<item>`’s `name`.
+
+{% include tip.html content="Wrap the text content inside a CDATA to avoid escaping of special characters." %}
+{% include warning.html content="Do not use the `{lang}` tag inside a language item." %}
+
+The text content of the `<item>` node is the value of the language item. Language items that are not in the `wcf.global` category support template scripting.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/language.xsd" languagecode="de">
+ <category name="wcf.example">
+ <item name="wcf.example.foo"><![CDATA[<strong>Look!</strong>]]></item>
+ </category>
+</language>
+```
--- /dev/null
+---
+title: Media Provider Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_media-provider.html
+folder: package/pip
+parent: package_pip
+---
+
+{% include callout.html content="Available since WoltLab Suite 3.1" type="info" %}
+
+Media providers are responsible to detect and convert links to a 3rd party service inside messages.
+
+## Components
+
+Each item is described as a `<provider>` element with the mandatory attribute `name` that should equal the lower-cased provider name. If a provider provides multiple components that are (largely) unrelated to each other, it is recommended to use a dash to separate the name and the component, e. g. `youtube-playlist`.
+
+### `<title>`
+
+The title is displayed in the administration control panel and is only used there, the value is neither localizable nor is it ever exposed to regular users.
+
+### `<regex>`
+
+The regular expression used to identify links to this provider, it must not contain anchors or delimiters. It is strongly recommended to capture the primary object id using the `(?P<ID>...)` group.
+
+### `<className>`
+
+{% include callout.html content="`<className>` and `<html>` are mutually exclusive." type="warning" %}
+
+PHP-Callback-Class that is invoked to process the matched link in case that additional logic must be applied that cannot be handled through a simple replacement as defined by the `<html>` element.
+
+The callback-class must implement the interface `\wcf\system\bbcode\media\provider\IBBCodeMediaProvider`.
+
+### `<html>`
+
+{% include callout.html content="`<className>` and `<html>` are mutually exclusive." type="warning" %}
+
+Replacement HTML that gets populated using the captured matches in `<regex>`, variables are accessed as `{$VariableName}`. For example, the capture group `(?P<ID>...)` is accessed using `{$ID}`.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/mediaProvider.xsd">
+ <import>
+ <provider name="youtube">
+ <title>YouTube</title>
+ <regex><![CDATA[https?://(?:.+?\.)?youtu(?:\.be/|be\.com/(?:#/)?watch\?(?:.*?&)?v=)(?P<ID>[a-zA-Z0-9_-]+)(?:(?:\?|&)t=(?P<start>[0-9hms]+)$)?]]></regex>
+ <!-- advanced PHP callback -->
+ <className><![CDATA[wcf\system\bbcode\media\provider\YouTubeBBCodeMediaProvider]]></className>
+ </provider>
+
+ <provider name="youtube-playlist">
+ <title>YouTube Playlist</title>
+ <regex><![CDATA[https?://(?:.+?\.)?youtu(?:\.be/|be\.com/)playlist\?(?:.*?&)?list=(?P<ID>[a-zA-Z0-9_-]+)]]></regex>
+ <!-- uses a simple HTML replacement -->
+ <html><![CDATA[<div class="videoContainer"><iframe src="https://www.youtube.com/embed/videoseries?list={$ID}" allowfullscreen></iframe></div>]]></html>
+ </provider>
+ </import>
+
+ <delete>
+ <provider identifier="example" />
+ </delete>
+</data>
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Menu Item Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_menu-item.html
+folder: package/pip
+parent: package_pip
+---
+
+Adds menu items to existing menus.
+
+## Components
+
+Each item is described as an `<item>` element with the mandatory attribute `identifier` that should follow the naming pattern `<packageIdentifier>.<PageName>`, e.g. `com.woltlab.wcf.Dashboard`.
+
+### `<menu>`
+
+The target menu that the item should be added to, requires the internal identifier set by creating a menu through the [menu.xml][package_pip_menu].
+
+### `<title>`
+
+{% include languageCode.html %}
+
+The title is displayed as the link title of the menu item and can be fully customized by the administrator, thus is immutable after deployment. Supports multiple `<title>` elements to provide localized values.
+
+### `<page>`
+
+The page that the link should point to, requires the internal identifier set by creating a page through the [page.xml][package_pip_page].
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/menuItem.xsd">
+ <import>
+ <item identifier="com.woltlab.wcf.Dashboard">
+ <menu>com.woltlab.wcf.MainMenu</menu>
+ <title language="de">Dashboard</title>
+ <title language="en">Dashboard</title>
+ <page>com.woltlab.wcf.Dashboard</page>
+ </item>
+ </import>
+
+ <delete>
+ <item identifier="com.woltlab.wcf.FooterLinks" />
+ </delete>
+</data>
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Menu Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_menu.html
+folder: package/pip
+parent: package_pip
+---
+
+Deploy and manage menus that can be placed anywhere on the site.
+
+## Components
+
+Each item is described as a `<menu>` element with the mandatory attribute `identifier` that should follow the naming pattern `<packageIdentifier>.<MenuName>`, e.g. `com.woltlab.wcf.MainMenu`.
+
+### `<title>`
+
+{% include languageCode.html %}
+
+The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple `<title>` elements.
+
+### `<box>`
+
+The following elements of the [box PIP](package_pip_box.html) are supported, please refer to the documentation to learn more about them:
+
+* `<position>`
+* `<showHeader>`
+* `<visibleEverywhere>`
+* `<visibilityExceptions>`
+* `cssClassName`
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/menu.xsd">
+ <import>
+ <menu identifier="com.woltlab.wcf.FooterLinks">
+ <title language="de">Footer-Links</title>
+ <title language="en">Footer Links</title>
+
+ <box>
+ <position>footer</position>
+ <cssClassName>boxMenuLinkGroup</cssClassName>
+ <showHeader>0</showHeader>
+ <visibleEverywhere>1</visibleEverywhere>
+ </box>
+ </menu>
+ </import>
+
+ <delete>
+ <menu identifier="com.woltlab.wcf.FooterLinks" />
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: Object Type Definition Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_object-type-definition.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers an object type definition.
+An object type definition is a blueprint for a certain behaviour that is particularized by [objectTypes](package_pip_object-type.html).
+As an example: Tags can be attached to different types of content (such as forum posts or gallery images).
+The bulk of the work is implemented in a generalized fashion, with all the tags stored in a single database table.
+Certain things, such as permission checking, need to be particularized for the specific type of content, though.
+Thus tags (or rather “taggable content”) are registered as an object type definition.
+Posts are then registered as an object type, implementing the “taggable content” behaviour.
+
+Other types of object type definitions include attachments, likes, polls, subscriptions, or even the category system.
+
+## Components
+
+Each item is described as a `<definition>` element with the mandatory child `<name>` that should follow the naming pattern `<packageIdentifier>.<definition>`, e.g. `com.woltlab.wcf.example`.
+
+### `<interfacename>`
+
+<span class="label label-info">Optional</span>
+
+The name of the PHP interface [objectTypes](package_pip_object-type.html) have to implement.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/objectTypeDefinition.xsd">
+ <import>
+ <definition>
+ <name>com.woltlab.wcf.example</name>
+ <interfacename>wcf\system\example\IExampleObjectType</interfacename>
+ </definition>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: Object Type Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_object-type.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers an object type.
+Read about object types in the [objectTypeDefinition](package_pip_object-type-definition.html) PIP.
+
+## Components
+
+Each item is described as a `<type>` element with the mandatory child `<name>` that should follow the naming pattern `<packageIdentifier>.<definition>`, e.g. `com.woltlab.wcf.example`.
+
+### `<definitionname>`
+
+The `<name>` of the [objectTypeDefinition](package_pip_object-type-definition.html).
+
+### `<classname>`
+
+The name of the class providing the object types's behaviour,
+the class has to implement the `<interfacename>` interface of the object type definition.
+
+### `<*>`
+
+<span class="label label-info">Optional</span>
+
+Additional fields may be defined for specific definitions of object types.
+Refer to the documentation of these for further explanation.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/objectType.xsd">
+ <import>
+ <type>
+ <name>com.woltlab.wcf.example</name>
+ <definitionname>com.woltlab.wcf.rebuildData</definitionname>
+ <classname>wcf\system\worker\ExampleRebuildWorker</classname>
+ <nicevalue>130</nicevalue>
+ </type>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: Option Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_option.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new options.
+Options allow the administrator to configure the behaviour of installed packages.
+The specified values are exposed as PHP constants.
+
+## Category Components
+
+Each category is described as an `<category>` element with the mandatory attribute `name`.
+
+### `<parent>`
+
+<span class="label label-info">Optional</span>
+
+The category’s parent category.
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Specifies the order of this option within the parent category.
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the category to be shown to the administrator.
+
+## Option Components
+
+Each option is described as an `<option>` element with the mandatory attribute `name`.
+The `name` is transformed into a PHP constant name by uppercasing it.
+
+### `<categoryname>`
+
+The option’s category.
+
+### `<optiontype>`
+
+The type of input to be used for this option.
+Valid types are defined by the `wcf\system\option\*OptionType` classes.
+
+### `<defaultvalue>`
+
+The value that is set after installation of a package.
+Valid values are defined by the `optiontype`.
+
+### `<validationpattern>`
+
+<span class="label label-info">Optional</span>
+
+Defines a regular expression that is used to validate the value of a free form option (such as `text`).
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Specifies the order of this option within the category.
+
+### `<selectoptions>`
+
+<span class="label label-info">Optional</span>
+{% include callout.html content="Defined only for `select`, `multiSelect` and `radioButton` types." type="warning" %}
+
+Specifies a newline-separated list of selectable values.
+Each line consists of an internal handle, followed by a colon (`:`, U+003A), followed by a language item.
+The language item is shown to the administrator, the internal handle is what is saved and exposed to the code.
+
+### `<enableoptions>`
+
+<span class="label label-info">Optional</span>
+{% include callout.html content="Defined only for `boolean`, `select` and `radioButton` types." type="warning" %}
+
+Specifies a comma-separated list of options which should be visually enabled when this option is enabled.
+A leading exclamation mark (`!`, U+0021) will disable the specified option when this option is enabled.
+For `select` and `radioButton` types the list should be prefixed by the internal [`selectoptions`](#selectoptions) handle followed by a colon (`:`, U+003A).
+
+This setting is a visual helper for the administrator only.
+It does not have an effect on the server side processing of the option.
+
+### `<hidden>`
+
+<span class="label label-info">Optional</span>
+
+If `hidden` is set to `1` the option will not be shown to the administrator.
+It still can be modified programmatically.
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the option to be shown to the administrator.
+
+### `<supporti18n>`
+
+<span class="label label-info">Optional</span>
+
+Specifies whether this option supports localized input.
+
+### `<requirei18n>`
+
+<span class="label label-info">Optional</span>
+
+Specifies whether this option requires localized input (i.e. the administrator must specify a value for every installed language).
+
+### `<*>`
+
+<span class="label label-info">Optional</span>
+
+Additional fields may be defined by specific types of options.
+Refer to the documentation of these for further explanation.
+
+## Language Items
+
+All relevant language items have to be put into the `wcf.acp.option` language item category.
+
+### Categories
+
+If you install a category named `example.sub`, you have to provide the language item `wcf.acp.option.category.example.sub`, which is used when displaying the options.
+If you want to provide an optional description of the category, you have to provide the language item `wcf.acp.option.category.example.sub.description`.
+Descriptions are only relevant for categories whose parent has a parent itself, i.e. categories on the third level.
+
+### Options
+
+If you install an option named `module_example`, you have to provide the language item `wcf.acp.option.module_example`, which is used as a label for setting the option value.
+If you want to provide an optional description of the option, you have to provide the language item `wcf.acp.option.module_example.description`.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/option.xsd">
+ <import>
+ <categories>
+ <category name="example" />
+ <category name="example.sub">
+ <parent>example</parent>
+ <options>module_example</options>
+ </category>
+ </categories>
+
+ <options>
+ <option name="module_example">
+ <categoryname>module.community</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ </option>
+
+ <option name="example_integer">
+ <categoryname>example.sub</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>10</defaultvalue>
+ <minvalue>5</minvalue>
+ <maxvalue>40</maxvalue>
+ </option>
+
+ <option name="example_select">
+ <categoryname>example.sub</categoryname>
+ <optiontype>select</optiontype>
+ <defaultvalue>DESC</defaultvalue>
+ <selectoptions>ASC:wcf.global.sortOrder.ascending
+DESC:wcf.global.sortOrder.descending</selectoptions>
+ </option>
+ </options>
+ </import>
+
+ <delete>
+ <option name="outdated_example" />
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: Page Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_page.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers page controllers, making them available for selection and configuration, including but not limited to boxes and menus.
+
+## Components
+
+Each item is described as a `<page>` element with the mandatory attribute `identifier` that should follow the naming pattern `<packageIdentifier>.<PageName>`, e.g. `com.woltlab.wcf.MembersList`.
+
+### `<pageType>`
+
+#### `system`
+
+The special `system` type is reserved for pages that pull their properties and content from a registered PHP class. Requires the `<controller>` element.
+
+#### `html`, `text` or `tpl`
+
+Provide arbitrary content, requires the `<content>` element.
+
+### `<controller>`
+
+Fully qualified class name for the controller, must implement `wcf\page\IPage` or `wcf\form\IForm`.
+
+### `<handler>`
+
+Fully qualified class name that can be optionally set to provide additional methods, such as displaying a badge for unread content and verifying permissions per page object id.
+
+### `<name>`
+
+{% include languageCode.html %}
+
+The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple `<name>` elements.
+
+### `<parent>`
+
+Sets the default parent page using its internal identifier, this setting controls the breadcrumbs and active menu item hierarchy.
+
+### `<hasFixedParent>`
+
+Pages can be assigned any other page as parent page by default, set to `1` to make the parent setting immutable.
+
+### `<permissions>`
+
+{% include callout.html content="The comma represents a logical `or`, the check is successful if at least one permission is set." type="warning" %}
+
+Comma separated list of permission names that will be checked one after another until at least one permission is set.
+
+### `<options>`
+
+{% include callout.html content="The comma represents a logical `or`, the check is successful if at least one option is enabled." type="warning" %}
+
+Comma separated list of options that will be checked one after another until at least one option is set.
+
+### `<excludeFromLandingPage>`
+
+Some pages should not be used as landing page, because they may not always be
+available and/or accessible to the user. For example, the account management
+page is available to logged-in users only and any guest attempting to visit that
+page would be presented with a permission denied message.
+
+Set this to `1` to prevent this page from becoming a landing page ever.
+
+### `<content>`
+
+{% include languageCode.html %}
+
+#### `<title>`
+
+The title element is required and controls the page title shown to the end users.
+
+#### `<content>`
+
+The content that should be used to populate the page, only used and required if the `pageType` equals `text`, `html` and `tpl`.
+
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/page.xsd">
+ <import>
+ <page identifier="com.woltlab.wcf.MembersList">
+ <pageType>system</pageType>
+ <controller>wcf\page\MembersListPage</controller>
+ <name language="de">Mitglieder</name>
+ <name language="en">Members</name>
+ <permissions>user.profile.canViewMembersList</permissions>
+ <options>module_members_list</options>
+
+ <content language="en">
+ <title>Members</title>
+ </content>
+ <content language="de">
+ <title>Mitglieder</title>
+ </content>
+ </page>
+ </import>
+
+ <delete>
+ <page identifier="com.woltlab.wcf.MembersList" />
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: Package Installation Plugin Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_pip.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new package installation plugins.
+
+## Components
+
+Each package installation plugin is described as an `<pip>` element with a `name` attribute and a PHP classname as the text content.
+
+{% include callout.html content="The package installation plugin’s class file must be installed into the `wcf` application and must not include classes outside the `\wcf\*` hierarchy to allow for proper uninstallation!" type="warning" %}
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/packageInstallationPlugin.xsd">
+ <import>
+ <pip name="custom">wcf\system\package\plugin\CustomPackageInstallationPlugin</pip>
+ </import>
+ <delete>
+ <pip name="outdated" />
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: Script Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_script.html
+folder: package/pip
+parent: package_pip
+---
+
+Execute arbitrary PHP code during installation, update and uninstallation of the package.
+
+{% include callout.html content="You must install the PHP script through the [file package installation plugin](package_pip_file.html)." type="warning" %}
+
+{% include callout.html content="The installation will attempt to delete the script after successful execution." type="warning" %}
+
+## Attributes
+
+### `application`
+
+The `application` attribute must have the same value as the `application` attribute of the `file` package installation plugin instruction so that the correct file in the intended application directory is executed.
+For further information about the `application` attribute, refer to its documentation on the [acpTemplate package installation plugin page](package_pip_acp-template.html#application).
+
+
+## Expected value
+
+The `script`-PIP expects a relative path to a `.php` file.
+
+### Naming convention
+
+The PHP script is deployed by using the [file package installation plugin](package_pip_file.html).
+To prevent it from colliding with other install script (remember: You cannot overwrite files created by another plugin), we highly recommend to make use of these naming conventions:
+
+- Installation: `install_<package>_<version>.php` (example: `install_com.woltlab.wbb_5.0.0.php`)
+- Update: `update_<package>_<targetVersion>.php` (example: `update_com.woltlab.wbb_5.0.0_pl_1.php`)
+
+`<targetVersion>` equals the version number of the current package being installed.
+If you're updating from `1.0.0` to `1.0.1`, `<targetVersion>` should read `1.0.1`.
+
+
+## Execution environment
+
+The script is included using `include()` within [ScriptPackageInstallationPlugin::run()](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/ScriptPackageInstallationPlugin.class.php#L69).
+This grants you access to the class members, including `$this->installation`.
+
+You can retrieve the package id of the current package through `$this->installation->getPackageID()`.
--- /dev/null
+---
+title: Smiley Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_smiley.html
+folder: package/pip
+parent: package_pip
+---
+
+Installs new smileys.
+
+## Components
+
+Each smiley is described as an `<smiley>` element with the mandatory attribute `name`.
+
+### `<title>`
+
+Short human readable description of the smiley.
+
+### `<path(2x)?>`
+
+{% include important.html content="The files must be installed using the [file](package_pip_file.html) PIP." %}
+
+File path relative to the root of WoltLab Suite Core.
+`path2x` is optional and being used for High-DPI screens.
+
+### `<aliases>`
+
+<span class="label label-info">Optional</span>
+
+List of smiley aliases.
+Aliases must be separated by a line feed character (`\n`, U+000A).
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Determines at which position of the smiley list the smiley is shown.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/smiley.xsd">
+ <import>
+ <smiley name=":example:">
+ <title>example</title>
+ <path>images/smilies/example.png</path>
+ <path2x>images/smilies/example@2x.png</path2x>
+ <aliases><![CDATA[:alias:
+:more_aliases:]]></aliases>
+ </smiley>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: SQL Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_sql.html
+folder: package/pip
+parent: package_pip
+---
+
+Execute SQL instructions using a MySQL-flavored syntax.
+
+{% include callout.html content="This file is parsed by WoltLab Suite Core to allow reverting of certain changes, but not every syntax MySQL supports is recognized by the parser. To avoid any troubles, you should always use statements relying on the SQL standard." type="warning" %}
+
+
+## Expected Value
+
+The `sql` package installation plugin expects a relative path to a `.sql` file.
+
+
+## Features
+
+### Logging
+
+WoltLab Suite Core uses a SQL parser to extract queries and log certain actions.
+This allows WoltLab Suite Core to revert some of the changes you apply upon package uninstallation.
+
+The logged changes are:
+
+- `CREATE TABLE`
+- `ALTER TABLE … ADD COLUMN`
+- `ALTER TABLE … ADD … KEY`
+
+### Instance Number
+
+It is possible to use different instance numbers, e.g. two separate WoltLab Suite Core installations within one database.
+WoltLab Suite Core requires you to always use `wcf1_<tableName>` or `<app>1_<tableName>` (e.g. `blog1_blog` in WoltLab Suite Blog), the number (`1`) will be automatically replaced prior to execution.
+If you every use anything other but `1`, you will eventually break things, thus always use `1`!
+
+### Table Type
+
+WoltLab Suite Core will determine the type of database tables on its own:
+If the table contains a `FULLTEXT` index, it uses `MyISAM`, otherwise `InnoDB` is used.
+
+
+## Limitations
+
+### Logging
+
+WoltLab Suite Core cannot revert changes to the database structure which would cause to the data to be either changed or new data to be incompatible with the original format.
+Additionally, WoltLab Suite Core does not track regular SQL queries such as `DELETE` or `UPDATE`.
+
+### Triggers
+
+WoltLab Suite Core does not support trigger since MySQL does not support execution of triggers if the event was fired by a cascading foreign key action.
+If you really need triggers, you should consider adding them by custom SQL queries using a [script](package_pip_script.html).
+
+
+## Example
+
+`package.xml`:
+
+```xml
+<instruction type="sql">install.sql</instruction>
+```
+
+Example content:
+
+```sql
+CREATE TABLE wcf1_foo_bar (
+ fooID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ packageID INT(10) NOT NULL,
+ bar VARCHAR(255) NOT NULL DEFAULT '',
+ foobar VARCHAR(50) NOT NULL DEFAULT '',
+
+ UNIQUE KEY baz (bar, foobar)
+);
+
+ALTER TABLE wcf1_foo_bar ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
+```
--- /dev/null
+---
+title: Style Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_style.html
+folder: package/pip
+parent: package_pip
+---
+
+Install styles during package installation.
+
+The `style` package installation plugins expects a relative path to a `.tar` file, a`.tar.gz` file or a `.tgz` file.
+Please use the ACP's export mechanism to export styles.
+
+## Example in `package.xml`
+
+```xml
+<instruction type="style">style.tgz</instruction>
+```
--- /dev/null
+---
+title: Template Listener Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_template-listener.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers template listeners.
+Template listeners supplement [event listeners](package_pip_event-listener.html), which modify server side behaviour, by adding additional template code to display additional elements.
+The added template code behaves as if it was part of the original template (i.e. it has access to all local variables).
+
+## Components
+
+Each event listener is described as an `<templatelistener>` element with a `name` attribute.
+As the `name` attribute has only be introduced with WSC 3.0, it is not yet mandatory to allow backwards compatibility.
+If `name` is not given, the system automatically sets the name based on the id of the event listener in the database.
+
+### `<templatename>`
+
+The template name is the name of the template in which the event is fired. It correspondes to the `eventclassname` field of event listeners.
+
+### `<eventname>`
+
+The event name is the name given when the event is fired to identify different events within the same template.
+
+### `<templatecode>`
+
+The given template code is literally copied into the target template during compile time.
+The original template is not modified.
+If multiple template listeners listen to a single event their output is concatenated using the line feed character (`\n`, U+000A) in the order defined by the [`niceValue`](#niceValue).
+
+{% include callout.html content="It is recommend that the only code is an `{include}` of a template to enable changes by the administrator. Names of templates included by a template listener start with two underscores by convention." type="warning" %}
+
+### `<environment>`
+
+The value of the environment element can either be `admin` or `user` and is `user` if no value is given.
+The value determines if the template listener will be executed in the frontend (`user`) or the backend (`admin`).
+
+### `<nice>`
+
+<span class="label label-info">Optional</span>
+
+The nice value element can contain an integer value out of the interval `[-128,127]` with `0` being the default value if the element is omitted.
+The nice value determines the execution order of template listeners.
+Template listeners with smaller nice values are executed first.
+If the nice value of two template listeners is equal, the order is undefined.
+
+{% include callout.html content="If you pass a value out of the mentioned interval, the value will be adjusted to the closest value in the interval." type="info" %}
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the template listener to be executed.
+
+### `<permissions>`
+
+<span class="label label-info">Optional</span>
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the template listener to be executed.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/templatelistener.xsd">
+ <import>
+ <templatelistener name="example">
+ <environment>user</environment>
+ <templatename>headIncludeJavaScript</templatename>
+ <eventname>javascriptInclude</eventname>
+ <templatecode><![CDATA[{include file='__myCustomJavaScript'}]]></templatecode>
+ </templatelistener>
+ </import>
+
+ <delete>
+ <templatelistener name="oldTemplateListenerName" />
+ </delete>
+</data>
+```
--- /dev/null
+---
+title: Template Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_template.html
+folder: package/pip
+parent: package_pip
+---
+
+Add templates for frontend pages and forms by providing an archive containing the template files.
+
+{% include callout.html content="You cannot overwrite templates provided by other packages." type="warning" %}
+
+This package installation plugin behaves exactly like the [acpTemplate package installation plugin](package_pip_acp-template.html) except for installing frontend templates instead of backend/acp templates.
--- /dev/null
+---
+title: User Group Option Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_user-group-option.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new user group options (“permissions”).
+The behaviour of this package installation plugin closely follows the [option](package_pip_option.html) PIP.
+
+## Category Components
+
+The category definition works exactly like the option PIP.
+
+## Option Components
+
+The fields `hidden`, `supporti18n` and `requirei18n` do not apply.
+The following extra fields are defined:
+
+### `<(admin|mod|user)defaultvalue>`
+
+Defines the `defaultvalue`s for subsets of the groups:
+
+| Type | Description |
+| ----- | ---------------------------------------------------------------------------------------------- |
+| admin | Groups where the `admin.user.accessibleGroups` user group option includes every group. |
+| mod | Groups where the `mod.general.canUseModeration` is set to `true`. |
+| user | Groups where the internal group type is neither `UserGroup::EVERYONE` nor `UserGroup::GUESTS`. |
+
+### `<usersonly>`
+
+Makes the option unavailable for groups with the group type `UserGroup::GUESTS`.
+
+## Language Items
+
+All relevant language items have to be put into the `wcf.acp.group` language item category.
+
+### Categories
+
+If you install a category named `user.foo`, you have to provide the language item `wcf.acp.group.option.category.user.foo`, which is used when displaying the options.
+If you want to provide an optional description of the category, you have to provide the language item `wcf.acp.group.option.category.user.foo.description`.
+Descriptions are only relevant for categories whose parent has a parent itself, i.e. categories on the third level.
+
+### Options
+
+If you install an option named `user.foo.canBar`, you have to provide the language item `wcf.acp.group.option.user.foo.canBar`, which is used as a label for setting the option value.
+If you want to provide an optional description of the option, you have to provide the language item `wcf.acp.group.option.user.foo.canBar.description`.
--- /dev/null
+---
+title: User Menu Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_user-menu.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new user menu items.
+
+## Components
+
+Each item is described as an `<usermenuitem>` element with the mandatory attribute `name`.
+
+### `<parent>`
+
+<span class="label label-info">Optional</span>
+
+The item’s parent item.
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Specifies the order of this item within the parent item.
+
+### `<controller>`
+
+The fully qualified class name of the target controller.
+If not specified this item serves as a category.
+
+### `<link>`
+
+Additional components if `<controller>` is set,
+the full external link otherwise.
+
+### `<iconclassname>`
+
+{% include tip.html content="Use an icon only for top-level items." %}
+
+Name of the Font Awesome icon class.
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the menu item to be shown.
+
+### `<permissions>`
+
+<span class="label label-info">Optional</span>
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the menu item to be shown.
+
+### `<classname>`
+
+The name of the class providing the user menu item’s behaviour,
+the class has to implement the `wcf\system\menu\user\IUserMenuItemProvider` interface.
+
+
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userMenu.xsd">
+ <import>
+ <usermenuitem name="wcf.user.menu.foo">
+ <iconclassname>fa-home</iconclassname>
+ </usermenuitem>
+
+ <usermenuitem name="wcf.user.menu.foo.bar">
+ <controller>wcf\page\FooBarListPage</controller>
+ <parent>wcf.user.menu.foo</parent>
+ <permissions>user.foo.canBar</permissions>
+ <classname>wcf\system\menu\user\FooBarMenuItemProvider</classname>
+ </usermenuitem>
+
+ <usermenuitem name="wcf.user.menu.foo.baz">
+ <controller>wcf\page\FooBazListPage</controller>
+ <parent>wcf.user.menu.foo</parent>
+ <permissions>user.foo.canBaz</permissions>
+ <options>module_foo_bar</options>
+ </usermenuitem>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: User Notification Event Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_user-notification-event.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new user notification events.
+
+## Components
+
+Each package installation plugin is described as an `<event>` element with the mandatory child `<name>`.
+
+### `<objectType>`
+
+{% include warning.html content="The `(name, objectType)` pair must be unique." %}
+
+The given object type must implement the `com.woltlab.wcf.notification.objectType` definition.
+
+### `<classname>`
+
+The name of the class providing the event's behaviour,
+the class has to implement the `wcf\system\user\notification\event\IUserNotificationEvent` interface.
+
+### `<preset>`
+
+Defines whether this event is enabled by default.
+
+### `<presetmailnotificationtype>`
+
+{% include callout.html content="Avoid using this option, as sending unsolicited mail can be seen as spamming." type="info" %}
+
+One of `instant` or `daily`.
+Defines whether this type of email notifications is enabled by default.
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the notification type to be available.
+
+### `<permissions>`
+
+<span class="label label-info">Optional</span>
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the notification type to be available.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userNotificationEvent.xsd">
+ <import>
+ <event>
+ <name>like</name>
+ <objecttype>com.woltlab.example.comment.like.notification</objecttype>
+ <classname>wcf\system\user\notification\event\ExampleCommentLikeUserNotificationEvent</classname>
+ <preset>1</preset>
+ <options>module_like</options>
+ </event>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: User Option Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_user-option.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new user options (profile fields / user settings).
+The behaviour of this package installation plugin closely follows the [option](package_pip_option.html) PIP.
+
+## Category Components
+
+The category definition works exactly like the option PIP.
+
+## Option Components
+
+The fields `hidden`, `supporti18n` and `requirei18n` do not apply.
+The following extra fields are defined:
+
+### `<required>`
+
+Requires that a value is provided.
+
+### `<askduringregistration>`
+
+If set to `1` the field is shown during user registration in the frontend.
+
+### `<editable>`
+
+Bitfield with the following options (constants in `wcf\data\user\option\UserOption`)
+
+| Name | Value |
+| ---------------------------------------- | ----- |
+| EDITABILITY_OWNER | 1 |
+| EDITABILITY_ADMINISTRATOR | 2 |
+| EDITABILITY_OWNER_DURING_REGISTRATION | 4 |
+
+### `<visible>`
+
+Bitfield with the following options (constants in `wcf\data\user\option\UserOption`)
+
+| Name | Value |
+| ------------------------ | ----- |
+| VISIBILITY_OWNER | 1 |
+| VISIBILITY_ADMINISTRATOR | 2 |
+| VISIBILITY_REGISTERED | 4 |
+| VISIBILITY_GUEST | 8 |
+
+### `<searchable>`
+
+If set to `1` the field is searchable.
+
+### `<outputclass>`
+
+PHP class responsible for output formatting of this field.
+the class has to implement the `wcf\system\option\user\IUserOptionOutput` interface.
+
+## Language Items
+
+All relevant language items have to be put into the `wcf.user.option` language item category.
+
+### Categories
+
+If you install a category named `example`, you have to provide the language item `wcf.user.option.category.example`, which is used when displaying the options.
+If you want to provide an optional description of the category, you have to provide the language item `wcf.user.option.category.example.description`.
+Descriptions are only relevant for categories whose parent has a parent itself, i.e. categories on the third level.
+
+### Options
+
+If you install an option named `exampleOption`, you have to provide the language item `wcf.user.option.exampleOption`, which is used as a label for setting the option value.
+If you want to provide an optional description of the option, you have to provide the language item `wcf.user.option.exampleOption.description`.
--- /dev/null
+---
+title: User Profile Menu Package Installation Plugin
+sidebar: sidebar
+permalink: package_pip_user-profile-menu.html
+folder: package/pip
+parent: package_pip
+---
+
+Registers new user profile tabs.
+
+## Components
+
+Each tab is described as an `<userprofilemenuitem>` element with the mandatory attribute `name`.
+
+### `<classname>`
+
+The name of the class providing the tab’s behaviour,
+the class has to implement the `wcf\system\menu\user\profile\content\IUserProfileMenuContent` interface.
+
+### `<showorder>`
+
+<span class="label label-info">Optional</span>
+
+Determines at which position of the tab list the tab is shown.
+
+### `<options>`
+
+<span class="label label-info">Optional</span>
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the tab to be shown.
+
+### `<permissions>`
+
+<span class="label label-info">Optional</span>
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the tab to be shown.
+
+## Example
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userProfileMenu.xsd">
+ <import>
+ <userprofilemenuitem name="example">
+ <classname>wcf\system\menu\user\profile\content\ExampleProfileMenuContent</classname>
+ <showorder>3</showorder>
+ <options>module_example</options>
+ </userprofilemenuitem>
+ </import>
+</data>
+```
--- /dev/null
+---
+title: Caches
+sidebar: sidebar
+permalink: php_api_caches.html
+folder: php/api
+---
+
+WoltLab Suite offers two distinct types of caches:
+
+1. [Persistent caches](php_api_caches_persistent-caches.html) created by cache builders whose data can be stored using different cache sources.
+2. [Runtime caches](php_api_caches_runtime-caches.html) store objects for the duration of a single request.
+
+## Understanding Caching
+
+Every so often, plugins make use of cache builders or runtime caches to store
+their data, even if there is absolutely no need for them to do so. Usually, this
+involves a strong opinion about the total number of SQL queries on a page,
+including but not limited to some magic treshold numbers, which should not be
+exceeded for "performance reasons".
+
+This misconception can easily lead into thinking that SQL queries should be
+avoided or at least written to a cache, so that it doesn't need to be executed
+so often. Unfortunately, this completely ignores the fact that both a single
+query can take down your app (e. g. full table scan on millions of rows), but
+10 queries using a primary key on a table with a few hundred rows will not slow
+down your page.
+
+There are some queries that should go into caches by design, but most of the
+cache builders weren't initially there, but instead have been added because
+they were required to reduce the load _significantly_. You need to understand
+that caches always come at a cost, even a runtime cache does! In particular,
+they will always consume memory that is not released over the duration of the
+request lifecycle and potentially even leak memory by holding references to
+objects and data structures that are no longer required.
+
+Caching should always be a solution for a problem.
+
+### When to Use a Cache
+
+It's difficult to provide a definite answer or checklist when to use a cache
+and why it is required at this point, because the answer is: It depends. The
+permission cache for user groups is a good example for a valid cache, where
+we can achieve significant performance improvement compared to processing this
+data on every request.
+
+Its caches are build for each permutation of user group memberships that are
+encountered for a page request. Building this data is an expensive process that
+involves both inheritance and specific rules in regards to when a value for a
+permission overrules another value. The added benefit of this cache is that one
+cache usually serves a large number of users with the same group memberships and
+by computing these permissions once, we can serve many different requests. Also,
+the permissions are rather static values that change very rarely and thus we can
+expect a very high cache lifetime before it gets rebuild.
+
+### When not to Use a Cache
+
+I remember, a few years ago, there was a plugin that displayed a user's character
+from an online video game. The character sheet not only included a list of basic
+statistics, but also displayed the items that this character was wearing and or
+holding at the time.
+
+The data for these items were downloaded in bulk from the game's vendor servers
+and stored in a persistent cache file that periodically gets renewed. There is
+nothing wrong with the idea of caching the data on your own server rather than
+requesting them everytime from the vendor's servers - not only because they
+imposed a limit on the number of requests per hour.
+
+Unfortunately, the character sheet had a sub-par performance and the users were
+upset by the significant loading times compared to literally every other page
+on the same server. The author of the plugin was working hard to resolve this
+issue and was evaluating all kind of methods to improve the page performance,
+including deep-diving into the realm of micro-optimizations to squeeze out every
+last bit of performance that is possible.
+
+The real problem was the cache file itself, it turns out that it was holding the
+data for several thousand items with a total file size of about 13 megabytes.
+It doesn't look that much at first glance, after all this isn't the '90s anymore,
+but unserializing a 13 megabyte array is really slow and looking up items in such
+a large array isn't exactly fast either.
+
+The solution was rather simple, the data that was fetched from the vendor's API
+was instead written into a separate database table. Next, the persistent cache
+was removed and the character sheet would now request the item data for that
+specific character straight from the database. Previously, the character sheet
+took several seconds to load and after the change it was done in a fraction of
+a second. Although quite extreme, this illustrates a situation where the cache
+file was introduced in the design process, without evaluating if the cache -
+at least how it was implemented - was really necessary.
+
+Caching should always be a solution for a problem. Not the other way around.
+
+{% include links.html %}
--- /dev/null
+---
+title: Persistent Caches
+sidebar: sidebar
+permalink: php_api_caches_persistent-caches.html
+folder: php/api/cache
+parent: php_api_caches
+---
+
+Relational databases are designed around the principle of normalized data that
+is organized across clearly separated tables with defined releations between
+data rows. While this enables you to quickly access and modify individual rows
+and columns, it can create the problem that re-assembling this data into a more
+complex structure can be quite expensive.
+
+For example, the user group permissions are stored for each user group and each
+permissions separately, but in order to be applied, they need to be fetched and
+the cumulative values across all user groups of an user have to be calculated.
+These repetitive tasks on barely ever changing data make them an excellent
+target for caching, where all sub-sequent requests are accelerated because they
+no longer have to perform the same expensive calculations every time.
+
+It is easy to get lost in the realm of caching, especially when it comes to the
+decision if you should use a cache or not. When in doubt, you should opt to not
+use them, because they also come at a hidden cost that cannot be expressed through
+simple SQL query counts. If you haven't already, it is recommended that you read
+the [introduction article on caching][php_api_caches] first, it provides a bit
+of background on caches and examples that should help you in your decision.
+
+## `AbstractCacheBuilder`
+
+Every cache builder should derive from the base class [AbstractCacheBuilder](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/builder/AbstractCacheBuilder.class.php)
+that already implements the mandatory interface [ICacheBuilder](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/builder/ICacheBuilder.class.php).
+
+```php
+<?php
+namespace wcf\system\cache\builder;
+
+class ExampleCacheBuilder extends AbstractCacheBuilder {
+ // 3600 = 1hr
+ protected $maxLifetime = 3600;
+
+ public function rebuild(array $parameters) {
+ $data = [];
+
+ // fetch and process your data and assign it to `$data`
+
+ return $data;
+ }
+}
+```
+
+Reading data from your cache builder is quite simple and follows a consistent
+pattern. The callee only needs to know the name of the cache builder, which
+parameters it requires and how the returned data looks like. It does not need
+to know how the data is retrieve, where it was stored, nor if it had to be
+rebuild due to the maximum lifetime.
+
+```php
+<?php
+use wcf\system\cache\builder\ExampleCacheBuilder;
+
+$data = ExampleCacheBuilder::getInstance()->getData($parameters);
+```
+
+### `getData(array $parameters = [], string $arrayIndex = ''): array`
+
+Retrieves the data from the cache builder, the `$parameters` array is automatically
+sorted to allow sub-sequent requests for the same parameters to be recognized,
+even if their parameters are mixed. For example, `getData([1, 2])` and `getData([2, 1])`
+will have the same exact result.
+
+The optional `$arrayIndex` will instruct the cache builder to retrieve the data
+and examine if the returned data is an array that has the index `$arrayIndex`.
+If it is set, the potion below this index is returned instead.
+
+### `getMaxLifetime(): int`
+
+Returns the maximum lifetime of a cache in seconds. It can be controlled through
+the `protected $maxLifetime` property which defaults to `0`. Any cache that has
+a lifetime greater than 0 is automatically discarded when exceeding this age,
+otherwise it will remain forever until it is explicitly removed or invalidated.
+
+### `reset(array $parameters = []): void`
+
+Invalidates a cache, the `$parameters` array will again be ordered using the same
+rules that are applied for `getData()`.
+
+### `rebuild(array $parameters): array`
+
+_This method is protected._
+
+This is the only method that a cache builder deriving from `AbstractCacheBuilder`
+has to implement and it will be invoked whenever the cache is required to be
+rebuild for whatever reason.
+
+{% include links.html %}
--- /dev/null
+---
+title: Runtime Caches
+sidebar: sidebar
+permalink: php_api_caches_runtime-caches.html
+folder: php/api/cache
+parent: php_api_caches
+---
+
+Runtime caches store objects created during the runtime of the script and are automatically discarded after the script terminates.
+Runtime caches are especially useful when objects are fetched by different APIs, each requiring separate requests.
+By using a runtime cache, you have two advantages:
+
+1. If the API allows it, you can delay fetching the actual objects and initially only tell the runtime cache that at some point in the future of the current request, you need the objects with the given ids.
+ If multiple APIs do this one after another, all objects can be fetched using only one query instead of each API querying the database on its own.
+1. If an object with the same ID has already been fetched from database, this object is simply returned and can be reused instead of being fetched from database again.
+
+
+## `IRuntimeCache`
+
+Every runtime cache has to implement the [IRuntimeCache](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/runtime/IRuntimeCache.class.php) interface.
+It is recommended, however, that you extend [AbstractRuntimeCache](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/runtime/AbstractRuntimeCache.class.php), the default implementation of the runtime cache interface.
+In most instances, you only need to set the `AbstractRuntimeCache::$listClassName` property to the name of database object list class which fetches the cached objects from database (see [example](#example)).
+
+
+## Usage
+
+```php
+<?php
+use wcf\system\cache\runtime\UserRuntimeCache;
+
+$userIDs = [1, 2];
+
+// first (optional) step: tell runtime cache to remember user ids
+UserRuntimeCache::getInstance()->cacheObjectIDs($userIDs);
+
+// […]
+
+// second step: fetch the objects from database
+$users = UserRuntimeCache::getInstance()->getObjects($userIDs);
+
+// somewhere else: fetch only one user
+$userID = 1;
+
+UserRuntimeCache::getInstance()->cacheObjectID($userID);
+
+// […]
+
+// get user without the cache actually fetching it from database because it has already been loaded
+$user = UserRuntimeCache::getInstance()->getObject($userID);
+
+// somewhere else: fetch users directly without caching user ids first
+$users = UserRuntimeCache::getInstance()->getObjects([3, 4]);
+```
+
+
+## Example
+
+```php
+<?php
+namespace wcf\system\cache\runtime;
+use wcf\data\user\User;
+use wcf\data\user\UserList;
+
+/**
+ * Runtime cache implementation for users.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Cache\Runtime
+ * @since 3.0
+ *
+ * @method User[] getCachedObjects()
+ * @method User getObject($objectID)
+ * @method User[] getObjects(array $objectIDs)
+ */
+class UserRuntimeCache extends AbstractRuntimeCache {
+ /**
+ * @inheritDoc
+ */
+ protected $listClassName = UserList::class;
+}
+```
--- /dev/null
+---
+title: Comments
+sidebar: sidebar
+permalink: php_api_comments.html
+folder: php/api
+---
+
+## User Group Options
+
+You need to create the following permissions:
+
+| user group type | permission type | naming |
+| --------------- | --------------- | ------ |
+| user | creating comments | `user.foo.canAddComment` |
+| user | editing own comments | `user.foo.canEditComment` |
+| user | deleting own comments | `user.foo.canDeleteComment` |
+| moderator | moderating comments | `mod.foo.canModerateComment` |
+| moderator | editing comments | `mod.foo.canEditComment` |
+| moderator | deleting comments | `mod.foo.canDeleteComment` |
+
+Within their respective user group option category, the options should be listed in the same order as in the table above.
+
+
+### Language Items
+
+#### User Group Options
+
+The language items for the comment-related user group options generally have the same values:
+
+- `wcf.acp.group.option.user.foo.canAddComment`
+
+ German: `Kann Kommentare erstellen`
+
+ English: `Can create comments`
+
+- `wcf.acp.group.option.user.foo.canEditComment`
+
+ German: `Kann eigene Kommentare bearbeiten`
+
+ English: `Can edit their comments`
+
+- `wcf.acp.group.option.user.foo.canDeleteComment`
+
+ German: `Kann eigene Kommentare löschen`
+
+ English: `Can delete their comments`
+
+- `wcf.acp.group.option.mod.foo.canModerateComment`
+
+ German: `Kann Kommentare moderieren`
+
+ English: `Can moderate comments`
+
+- `wcf.acp.group.option.mod.foo.canEditComment`
+
+ German: `Kann Kommentare bearbeiten`
+
+ English: `Can edit comments`
+
+- `wcf.acp.group.option.mod.foo.canDeleteComment`
+
+ German: `Kann Kommentare löschen`
+
+ English: `Can delete comments`
--- /dev/null
+---
+title: Cronjobs
+sidebar: sidebar
+permalink: php_api_cronjobs.html
+folder: php/api
+---
+
+Cronjobs offer an easy way to execute actions periodically, like cleaning up the database.
+
+{% include callout.html content="The execution of cronjobs is not guaranteed but requires someone to access the page with JavaScript enabled." type="warning" %}
+
+This page focuses on the technical aspects of cronjobs, [the cronjob package installation plugin page](package_pip_cronjob.html) covers how you can actually register a cronjob.
+
+
+## Example
+
+```php
+<?php
+namespace wcf\system\cronjob;
+use wcf\data\cronjob\Cronjob;
+use wcf\system\WCF;
+
+/**
+ * Updates the last activity timestamp in the user table.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Cronjob
+ */
+class LastActivityCronjob extends AbstractCronjob {
+ /**
+ * @inheritDoc
+ */
+ public function execute(Cronjob $cronjob) {
+ parent::execute($cronjob);
+
+ $sql = "UPDATE wcf".WCF_N."_user user_table,
+ wcf".WCF_N."_session session
+ SET user_table.lastActivityTime = session.lastActivityTime
+ WHERE user_table.userID = session.userID
+ AND session.userID <> 0";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute();
+ }
+}
+
+```
+
+
+## `ICronjob` Interface
+
+Every cronjob needs to implement the `wcf\system\cronjob\ICronjob` interface which requires the `execute(Cronjob $cronjob)` method to be implemented.
+This method is called by [wcf\system\cronjob\CronjobScheduler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cronjob/CronjobScheduler.class.php) when executing the cronjobs.
+
+In practice, however, you should extend the `AbstractCronjob` class and also call the `AbstractCronjob::execute()` method as it fires an event which makes cronjobs extendable by plugins (see [event documentation](php_event.html)).
+
+
+## Executing Cronjobs Through CLI
+
+Cronjobs can be executed through the command-line interface (CLI):
+
+```
+php /path/to/wcf/cli.php << 'EOT'
+USERNAME
+PASSWORD
+cronjob execute
+EOT
+```
\ No newline at end of file
--- /dev/null
+---
+title: Event List
+sidebar: sidebar
+permalink: php_api_event_list.html
+folder: php
+parent: php_api_events
+---
+
+Events whose name is marked with an asterisk are called from a static method and thus do not provide any object, just the class name.
+
+## WoltLab Suite Core
+
+| Class | Event Name |
+|-------|------------|
+| `wcf\acp\action\UserExportGdprAction` | `export` |
+| `wcf\acp\form\StyleAddForm` | `setVariables` |
+| `wcf\acp\form\UserSearchForm` | `search` |
+| `wcf\action\AbstractAction` | `checkModules` |
+| `wcf\action\AbstractAction` | `checkPermissions` |
+| `wcf\action\AbstractAction` | `execute` |
+| `wcf\action\AbstractAction` | `executed` |
+| `wcf\action\AbstractAction` | `readParameters` |
+| `wcf\data\attachment\AttachmentAction` | `generateThumbnail` |
+| `wcf\data\session\SessionAction` | `keepAlive` |
+| `wcf\data\session\SessionAction` | `poll` |
+| `wcf\data\trophy\Trophy` | `renderTrophy` |
+| `wcf\data\user\online\UserOnline` | `getBrowser` |
+| `wcf\data\user\online\UserOnlineList` | `isVisible` |
+| `wcf\data\user\trophy\UserTrophy` | `getReplacements` |
+| `wcf\data\user\UserAction` | `beforeFindUsers` |
+| `wcf\data\user\UserAction` | `rename` |
+| `wcf\data\user\UserProfile` | `getAvatar` |
+| `wcf\data\user\UserProfile` | `isAccessible` |
+| `wcf\data\AbstractDatabaseObjectAction` | `finalizeAction` |
+| `wcf\data\AbstractDatabaseObjectAction` | `initializeAction` |
+| `wcf\data\AbstractDatabaseObjectAction` | `validateAction` |
+| `wcf\data\DatabaseObjectList` | `init` |
+| `wcf\form\AbstractForm` | `readFormParameters` |
+| `wcf\form\AbstractForm` | `save` |
+| `wcf\form\AbstractForm` | `saved` |
+| `wcf\form\AbstractForm` | `submit` |
+| `wcf\form\AbstractForm` | `validate` |
+| `wcf\form\AbstractModerationForm` | `prepareSave` |
+| `wcf\page\AbstractPage` | `assignVariables` |
+| `wcf\page\AbstractPage` | `checkModules` |
+| `wcf\page\AbstractPage` | `checkPermissions` |
+| `wcf\page\AbstractPage` | `readData` |
+| `wcf\page\AbstractPage` | `readParameters` |
+| `wcf\page\AbstractPage` | `show` |
+| `wcf\page\MultipleLinkPage` | `beforeReadObjects` |
+| `wcf\page\MultipleLinkPage` | `calculateNumberOfPages` |
+| `wcf\page\MultipleLinkPage` | `countItems` |
+| `wcf\page\SortablePage` | `validateSortField` |
+| `wcf\page\SortablePage` | `validateSortOrder` |
+| `wcf\system\bbcode\MessageParser` | `afterParsing` |
+| `wcf\system\bbcode\MessageParser` | `beforeParsing` |
+| `wcf\system\bbcode\SimpleMessageParser` | `afterParsing` |
+| `wcf\system\bbcode\SimpleMessageParser` | `beforeParsing` |
+| `wcf\system\box\AbstractBoxController` | `__construct` |
+| `wcf\system\box\AbstractBoxController` | `afterLoadContent` |
+| `wcf\system\box\AbstractBoxController` | `beforeLoadContent` |
+| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `afterLoadContent` |
+| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `beforeLoadContent` |
+| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `hasContent` |
+| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `readObjects` |
+| `wcf\system\cronjob\AbstractCronjob` | `execute` |
+| `wcf\system\email\Email` | `getJobs` |
+| `wcf\system\form\builder\container\wysiwyg\WysiwygFormContainer` | `populate` |
+| `wcf\system\html\input\filter\MessageHtmlInputFilter` | `setAttributeDefinitions` |
+| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `afterProcess` |
+| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `beforeEmbeddedProcess` |
+| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `beforeProcess` |
+| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `convertPlainLinks` |
+| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `getTextContent` |
+| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `parseEmbeddedContent` |
+| `wcf\system\html\input\node\HtmlInputNodeWoltlabMetacodeMarker` | `filterGroups` |
+| `wcf\system\html\output\node\HtmlOutputNodePre` | `selectHighlighter` |
+| `wcf\system\html\output\node\HtmlOutputNodeProcessor` | `beforeProcess` |
+| `wcf\system\image\adapter\ImagickImageAdapter` | `getResizeFilter` |
+| `wcf\system\menu\user\profile\UserProfileMenu` | `init` |
+| `wcf\system\menu\user\profile\UserProfileMenu` | `loadCache` |
+| `wcf\system\menu\TreeMenu` | `init` |
+| `wcf\system\menu\TreeMenu` | `loadCache` |
+| `wcf\system\message\QuickReplyManager` | `addFullQuote` |
+| `wcf\system\message\QuickReplyManager` | `allowedDataParameters` |
+| `wcf\system\message\QuickReplyManager` | `beforeRenderQuote` |
+| `wcf\system\message\QuickReplyManager` | `createMessage` |
+| `wcf\system\message\QuickReplyManager` | `createdMessage` |
+| `wcf\system\message\QuickReplyManager` | `getMessage` |
+| `wcf\system\message\QuickReplyManager` | `validateParameters` |
+| `wcf\system\option\OptionHandler` | `afterReadCache` |
+| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `construct` |
+| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `hasUninstall` |
+| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `install` |
+| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `uninstall` |
+| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `update` |
+| `wcf\system\package\plugin\ObjectTypePackageInstallationPlugin` | `addConditionFields` |
+| `wcf\system\package\PackageInstallationDispatcher` | `postInstall` |
+| `wcf\system\package\PackageUninstallationDispatcher` | `postUninstall` |
+| `wcf\system\reaction\ReactionHandler` | `getDataAttributes` |
+| `wcf\system\request\RouteHandler` | `didInit` |
+| `wcf\system\session\ACPSessionFactory` | `afterInit` |
+| `wcf\system\session\ACPSessionFactory` | `beforeInit` |
+| `wcf\system\session\SessionHandler` | `afterChangeUser` |
+| `wcf\system\session\SessionHandler` | `beforeChangeUser` |
+| `wcf\system\style\StyleCompiler` | `compile` |
+| `wcf\system\template\TemplateEngine` | `afterDisplay` |
+| `wcf\system\template\TemplateEngine` | `beforeDisplay` |
+| `wcf\system\upload\DefaultUploadFileSaveStrategy` | `generateThumbnails` |
+| `wcf\system\upload\DefaultUploadFileSaveStrategy` | `save` |
+| `wcf\system\user\authentication\UserAuthenticationFactory` | `init` |
+| `wcf\system\user\notification\UserNotificationHandler` | `createdNotification` |
+| `wcf\system\user\notification\UserNotificationHandler` | `fireEvent` |
+| `wcf\system\user\notification\UserNotificationHandler` | `markAsConfirmed` |
+| `wcf\system\user\notification\UserNotificationHandler` | `markAsConfirmedByIDs` |
+| `wcf\system\user\notification\UserNotificationHandler` | `removeNotifications` |
+| `wcf\system\user\notification\UserNotificationHandler` | `updateTriggerCount` |
+| `wcf\system\user\UserBirthdayCache` | `loadMonth` |
+| `wcf\system\worker\AbstractRebuildDataWorker` | `execute` |
+| `wcf\system\CLIWCF` | `afterArgumentParsing` |
+| `wcf\system\CLIWCF` | `beforeArgumentParsing` |
+| `wcf\system\WCF` | `initialized` |
+| `wcf\util\HeaderUtil` | `parseOutput`*|
+
+## WoltLab Suite Forum
+
+| Class | Event Name |
+|-------|------------|
+| `wbb\data\board\BoardAction` | `cloneBoard` |
+| `wbb\data\post\PostAction` | `quickReplyShouldMerge` |
+| `wbb\system\thread\ThreadHandler` | `didInit` |
--- /dev/null
+---
+title: Events
+sidebar: sidebar
+permalink: php_api_events.html
+folder: php/api
+---
+
+WoltLab Suite's event system allows manipulation of program flows and data without having to change any of the original source code.
+At many locations throughout the PHP code of WoltLab Suite Core and mainly through inheritance also in the applications and plugins, so called *events* are fired which trigger registered *event listeners* that get access to the object firing the event (or at least the class name if the event has been fired in a static method).
+
+This page focuses on the technical aspects of events and event listeners, [the eventListener package installation plugin page](package_pip_event-listener.html) covers how you can actually register an event listener.
+A comprehensive list of all available events is provided [here](php_api_event_list.html).
+
+
+## Introductory Example
+
+Let's start with a simple example to illustrate how the event system works.
+Consider this pre-existing class:
+
+```php
+<?php
+namespace wcf\system\example;
+use wcf\system\event\EventHandler;
+
+class ExampleComponent {
+ public $var = 1;
+
+ public function getVar() {
+ EventHandler::getInstance()->fireAction($this, 'getVar');
+
+ return $this->var;
+ }
+}
+```
+
+where an event with event name `getVar` is fired in the `getVar()` method.
+
+If you create an object of this class and call the `getVar()` method, the return value will be `1`, of course:
+
+```php
+<?php
+
+$example = new wcf\system\example\ExampleComponent();
+if ($example->getVar() == 1) {
+ echo "var is 1!";
+}
+else if ($example->getVar() == 2) {
+ echo "var is 2!";
+}
+else {
+ echo "No, var is neither 1 nor 2.";
+}
+
+// output: var is 1!
+```
+
+Now, consider that we have registered the following event listener to this event:
+
+```php
+<?php
+namespace wcf\system\event\listener;
+
+class ExampleEventListener implements IParameterizedEventListener {
+ public function execute($eventObj, $className, $eventName, array &$parameters) {
+ $eventObj->var = 2;
+ }
+}
+
+```
+
+Whenever the event in the `getVar()` method is called, this method (of the same event listener object) is called.
+In this case, the value of the method's first parameter is the `ExampleComponent` object passed as the first argument of the `EventHandler::fireAction()` call in `ExampleComponent::getVar()`.
+As `ExampleComponent::$var` is a public property, the event listener code can change it and set it to `2`.
+
+If you now execute the example code from above again, the output will change from `var is 1!` to `var is 2!` because prior to returning the value, the event listener code changes the value from `1` to `2`.
+
+This introductory example illustrates how event listeners can change data in a non-intrusive way.
+Program flow can be changed, for example, by throwing a `wcf\system\exception\PermissionDeniedException` if some additional constraint to access a page is not fulfilled.
+
+
+## Listening to Events
+
+In order to listen to events, you need to register the event listener and the event listener itself needs to implement the interface `wcf\system\event\listener\IParameterizedEventListener` which only contains the `execute` method (see example above).
+
+The first parameter `$eventObj` of the method contains the passed object where the event is fired or the name of the class in which the event is fired if it is fired from a static method.
+The second parameter `$className` always contains the name of the class where the event has been fired.
+The third parameter `$eventName` provides the name of the event within a class to uniquely identify the exact location in the class where the event has been fired.
+The last parameter `$parameters` is a reference to the array which contains additional data passed by the method firing the event.
+If no additional data is passed, `$parameters` is empty.
+
+
+## Firing Events
+
+If you write code and want plugins to have access at certain points, you can fire an event on your own.
+The only thing to do is to call the `wcf\system\event\EventHandler::fireAction($eventObj, $eventName, array &$parameters = [])` method and pass the following parameters:
+
+1. `$eventObj` should be `$this` if you fire from an object context, otherwise pass the class name `static::class`.
+2. `$eventName` identifies the event within the class and generally has the same name as the method.
+ In cases, were you might fire more than one event in a method, for example before and after a certain piece of code, you can use the prefixes `before*` and `after*` in your event names.
+3. `$parameters` is an optional array which allows you to pass additional data to the event listeners without having to make this data accessible via a property explicitly only created for this purpose.
+ This additional data can either be just additional information for the event listeners about the context of the method call or allow the event listener to manipulate local data if the code, where the event has been fired, uses the passed data afterwards.
+
+### Example: Using `$parameters` argument
+
+Consider the following method which gets some text that the methods parses.
+
+```php
+<?php
+namespace wcf\system\example;
+use wcf\system\event\EventHandler;
+
+class ExampleParser {
+ public function parse($text) {
+ // [some parsing done by default]
+
+ $parameters = ['text' => $text];
+ EventHandler::getInstance()->fireAction($this, 'parse', $parameters);
+
+ return $parameters['text'];
+ }
+}
+```
+
+After the default parsing by the method itself, the author wants to enable plugins to do additional parsing and thus fires an event and passes the parsed text as an additional parameter.
+Then, a plugin can deliver the following event listener
+
+```php
+<?php
+namespace wcf\system\event\listener;
+
+class ExampleParserEventListener implements IParameterizedEventListener {
+ public function execute($eventObj, $className, $eventName, array &$parameters) {
+ $text = $parameters['text'];
+
+ // [some additional parsing which changes $text]
+
+ $parameters['text'] = $text;
+ }
+}
+```
+
+which can access the text via `$parameters['text']`.
+
+This example can also be perfectly used to illustrate how to name multiple events in the same method.
+Let's assume that the author wants to enable plugins to change the text before and after the method does its own parsing and thus fires two events:
+
+```php
+<?php
+namespace wcf\system\example;
+use wcf\system\event\EventHandler;
+
+class ExampleParser {
+ public function parse($text) {
+ $parameters = ['text' => $text];
+ EventHandler::getInstance()->fireAction($this, 'beforeParsing', $parameters);
+ $text = $parameters['text'];
+
+ // [some parsing done by default]
+
+ $parameters = ['text' => $text];
+ EventHandler::getInstance()->fireAction($this, 'afterParsing', $parameters);
+
+ return $parameters['text'];
+ }
+}
+```
+
+
+## Advanced Example: Additional Form Field
+
+One common reason to use event listeners is to add an additional field to a pre-existing form (in combination with template listeners, which we will not cover here).
+We will assume that users are able to do both, create and edit the objects via this form.
+The points in the program flow of [AbstractForm](php_pages.html#abstractform) that are relevant here are:
+
+- adding object (after the form has been submitted):
+ 1. reading the value of the field
+ 2. validating the read value
+ 3. saving the additional value after successful validation and resetting locally stored value or assigning the current value of the field to the template after unsuccessful validation
+
+- editing object:
+ - on initial form request:
+ 1. reading the pre-existing value of the edited object
+ 2. assigning the field value to the template
+ - after the form has been submitted:
+ 1. reading the value of the field
+ 2. validating the read value
+ 3. saving the additional value after successful validation
+ 4. assigning the current value of the field to the template
+
+All of these cases can be covered the by following code in which we assume that `wcf\form\ExampleAddForm` is the form to create example objects and that `wcf\form\ExampleEditForm` extends `wcf\form\ExampleAddForm` and is used for editing existing example objects.
+
+```php
+<?php
+namespace wcf\system\event\listener;
+use wcf\form\ExampleAddForm;
+use wcf\form\ExampleEditForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+
+class ExampleAddFormListener implements IParameterizedEventListener {
+ protected $var = 0;
+
+ public function execute($eventObj, $className, $eventName, array &$parameters) {
+ $this->$eventName($eventObj);
+ }
+
+ protected function assignVariables() {
+ WCF::getTPL()->assign('var', $this->var);
+ }
+
+ protected function readData(ExampleEditForm $eventObj) {
+ if (empty($_POST)) {
+ $this->var = $eventObj->example->var;
+ }
+ }
+
+ protected function readFormParameters() {
+ if (isset($_POST['var'])) $this->var = intval($_POST['var']);
+ }
+
+ protected function save(ExampleAddForm $eventObj) {
+ $eventObj->additionalFields = array_merge($eventObj->additionalFields, ['var' => $this->var]);
+ }
+
+ protected function saved() {
+ $this->var = 0;
+ }
+
+ protected function validate() {
+ if ($this->var < 0) {
+ throw new UserInputException('var', 'isNegative');
+ }
+ }
+}
+```
+
+The `execute` method in this example just delegates the call to a method with the same name as the event so that this class mimics the structure of a form class itself.
+The form object is passed to the methods but is only given in the method signatures as a parameter here whenever the form object is actually used.
+Furthermore, the type-hinting of the parameter illustrates in which contexts the method is actually called which will become clear in the following discussion of the individual methods:
+
+- `assignVariables()` is called for the add and the edit form and simply assigns the current value of the variable to the template.
+- `readData()` reads the pre-existing value of `$var` if the form has not been submitted and thus is only relevant when editing objects which is illustrated by the explicit type-hint of `ExampleEditForm`.
+- `readFormParameters()` reads the value for both, the add and the edit form.
+- `save()` is, of course, also relevant in both cases but requires the form object to store the additional value in the `wcf\form\AbstractForm::$additionalFields` array which can be used if a `var` column has been added to the database table in which the example objects are stored.
+- `saved()` is only called for the add form as it clears the internal value so that in the `assignVariables()` call, the default value will be assigned to the template to create an "empty" form.
+ During edits, this current value is the actual value that should be shown.
+- `validate()` also needs to be called in both cases as the input data always has to be validated.
+
+Lastly, the following XML file has to be used to register the event listeners (you can find more information about how to register event listeners on [the eventListener package installation plugin page](package_pip_event-listener.html)):
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/eventListener.xsd">
+ <import>
+ <eventlistener name="exampleAddInherited">
+ <eventclassname>wcf\form\ExampleAddForm</eventclassname>
+ <eventname>assignVariables,readFormParameters,save,validate</eventname>
+ <listenerclassname>wcf\system\event\listener\ExampleAddFormListener</listenerclassname>
+ <inherit>1</inherit>
+ </eventlistener>
+
+ <eventlistener name="exampleAdd">
+ <eventclassname>wcf\form\ExampleAddForm</eventclassname>
+ <eventname>saved</eventname>
+ <listenerclassname>wcf\system\event\listener\ExampleAddFormListener</listenerclassname>
+ </eventlistener>
+
+ <eventlistener name="exampleEdit">
+ <eventclassname>wcf\form\ExampleEditForm</eventclassname>
+ <eventname>readData</eventname>
+ <listenerclassname>wcf\system\event\listener\ExampleAddFormListener</listenerclassname>
+ </eventlistener>
+ </import>
+</data>
+```
\ No newline at end of file
--- /dev/null
+---
+title: Form Node Dependencies
+sidebar: sidebar
+permalink: php_api_form_builder-dependencies.html
+folder: php/api/formBuilder
+parent: php_api_form_builder
+---
+
+Form node dependencies allow to make parts of a form dynamically available or unavailable depending on the values of form fields.
+Dependencies are always added to the object whose visibility is determined by certain form fields.
+They are **not** added to the form field’s whose values determine the visibility!
+An example is a text form field that should only be available if a certain option from a single selection form field is selected.
+Form builder’s dependency system supports such scenarios and also automatically making form containers unavailable once all of its children are unavailable.
+
+If a form node has multiple dependencies and one of them is not met, the form node is unavailable.
+A form node not being available due to dependencies has to the following consequences:
+
+- The form field value is not validated. It is, however, read from the request data as all request data needs to be read first so that the dependencies can determine whether they are met or not.
+- No data is collected for the form field and returned by `IFormDocument::getData()`.
+- In the actual form, the form field will be hidden via JavaScript.
+
+
+## `IFormFieldDependency`
+
+The basis of the dependencies is the `IFormFieldDependency` interface that has to be implemented by every dependency class.
+The interface requires the following methods:
+
+- `checkDependency()` checks if the dependency is met, thus if the dependant form field should be considered available.
+- `dependentNode(IFormNode $node)` and `getDependentNode()` can be used to set and get the node whose availability depends on the referenced form field.
+ `TFormNode::addDependency()` automatically calls `dependentNode(IFormNode $node)` with itself as the dependent node, thus the dependent node is automatically set by the API.
+- `field(IFormField $field)` and `getField()` can be used to set and get the form field that influences the availability of the dependent node.
+- `fieldId($fieldId)` and `getFieldId()` can be used to set and get the id of the form field that influences the availability of the dependent node.
+- `getHtml()` returns JavaScript code required to ensure the dependency in the form output.
+- `getId()` returns the id of the dependency used to identify multiple dependencies of the same form node.
+- `static create($id)` is the factory method that has to be used to create new dependencies with the given id.
+
+`AbstractFormFieldDependency` provides default implementations for all methods except for `checkDependency()`.
+
+Using `fieldId($fieldId)` instead of `field(IFormField $field)` makes sense when adding the dependency directly when setting up the form:
+
+```php
+$container->appendChildren([
+ FooField::create('a'),
+
+ BarField::create('b')
+ ->addDependency(
+ BazDependency::create('a')
+ ->fieldId('a')
+ )
+]);
+```
+
+Here, without an additional assignment, the first field with id `a` cannot be accessed thus `fieldId($fieldId)` should be used as the id of the relevant field is known.
+When the form is built, all dependencies that only know the id of the relevant field and do not have a reference for the actual object are populated with the actual form field objects.
+
+
+## Default Dependencies
+
+WoltLab Suite Core delivers the following two default dependency classes by default:
+
+- `NonEmptyFormFieldDependency` can be used to ensure that a node is only shown if the value of the referenced form field is not empty (being empty is determined using PHP’s `empty` function).
+- `ValueFormFieldDependency` can be used to ensure that a node is only shown if the value of the referenced form field is from a specified list of of values (see methods `values($values)` and `getValues()`).
+ Additionally, via `negate($negate = true)` and `isNegated()`, the locic can also be inverted by requiring the value of the referenced form field not to be from a specified list of values.
+
+
+## JavaScript Implementation
+
+To ensure that dependent node are correctly shown and hidden when changing the value of referenced form fields, every PHP dependency class has a corresponding JavaScript module that checks the dependency in the browser.
+Every JavaScript dependency has to extend `WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract` and implement the `checkDependency()` function, the JavaScript version of `IFormFieldDependency::checkDependency()`.
+
+All of the JavaScript dependency objects automatically register themselves during initialization with the `WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager` which takes care of checking the dependencies at the correct points in time.
+
+Additionally, the dependency manager also ensures that form containers in which all children are hidden due to dependencies are also hidden and, once any child becomes available again, makes the container also available again.
+Every form container has to create a matching form container dependency object from a module based on `WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract`.
+
+
+## Examples
+
+If `$booleanFormField` is an instance of `BooleanFormField` and the text form field `$textFormField` should only be available if “Yes” has been selected, the following condition has to be set up:
+
+```php
+$textFormField->addDependency(
+ NonEmptyFormFieldDependency::create('booleanFormField')
+ ->field($booleanFormField)
+);
+```
+
+If `$singleSelectionFormField` is an instance of `SingleSelectionFormField` that offers the options `1`, `2`, and `3` and `$textFormField` should only be available if `1` or `3` is selected, the following condition has to be set up:
+
+```php
+$textFormField->addDependency(
+ NonEmptyFormFieldDependency::create('singleSelectionFormField')
+ ->field($singleSelectionFormField)
+ ->values([1, 3])
+);
+```
+
+If, in contrast, `$singleSelectionFormField` has many available options and `7` is the only option for which `$textFormField` should **not** be available, `negate()` should be used:
+
+```php
+$textFormField->addDependency(
+ NonEmptyFormFieldDependency::create('singleSelectionFormField')
+ ->field($singleSelectionFormField)
+ ->values([7])
+ ->negate()
+);
+```
--- /dev/null
+---
+title: Form Builder Fields
+sidebar: sidebar
+permalink: php_api_form_builder-form_fields.html
+folder: php/api/formBuilder
+parent: php_api_form_builder
+---
+
+## Abstract Form Fields
+
+The following form field classes cannot be instantiated directly because they are abstract, but they can/must be used when creating own form field classes.
+
+
+### `AbstractFormField`
+
+`AbstractFormField` is the abstract default implementation of the `IFormField` interface and it is expected that every implementation of `IFormField` implements the interface by extending this class.
+
+
+### `AbstractNumericFormField`
+
+`AbstractNumericFormField` is the abstract implementation of a form field handling a single numeric value.
+The class implements `IImmutableFormField`, `IMaximumFormField`, `IMinimumFormField`, `INullableFormField`, `IPlaceholderFormField` and `ISuffixedFormField`.
+If the property `$integerValues` is `true`, the form field works with integer values, otherwise it works with floating point numbers.
+The methods `step($step = null)` and `getStep()` can be used to set and get the step attribute of the `input` element.
+The default step for form fields with integer values is `1`.
+Otherwise, the default step is `any`.
+
+
+## General Form Fields
+
+The following form fields are general reusable fields without any underlying context.
+
+
+### `BooleanFormField`
+
+`BooleanFormField` is used for boolean (`0` or `1`, `yes` or `no`) values.
+Objects of this class require a label.
+The return value of `getSaveValue()` is the integer representation of the boolean value, i.e. `0` or `1`.
+
+
+### `ClassNameFormField`
+
+`ClassNameFormField` is a [text form field](#textformfield) that supports additional settings, specific to entering a PHP class name:
+
+- `classExists($classExists = true)` and `getClassExists()` can be used to ensure that the entered class currently exists in the installation.
+ By default, the existance of the entered class is required.
+- `implementedInterface($interface)` and `getImplementedInterface()` can be used to ensure that the entered class implements the specified interface.
+ By default, no interface is required.
+- `parentClass($parentClass)` and `getParentClass()` can be used to ensure that the entered class extends the specified class.
+ By default, no parent class is required.
+- `instantiable($instantiable = true)` and `isInstantiable()` can be used to ensure that the entered class is instantiable.
+ By default, entered classes have to instantiable.
+
+Additionally, the default id of a `ClassNameFormField` object is `className`, the default label is `wcf.form.field.className`, and if either an interface or a parent class is required, a default description is set if no description has already been set (`wcf.form.field.className.description.interface` and `wcf.form.field.className.description.parentClass`, respectively).
+
+
+### `DateFormField`
+
+`DateFormField` is a form field to enter a date (and optionally a time).
+The following methods are specific to this form field class:
+
+- `earliestDate($earliestDate)` and `getEarliestDate()` can be used to get and set the earliest selectable/valid date and `latestDate($latestDate)` and `getLatestDate()` can be used to get and set the latest selectable/valid date.
+ The date passed to the setters must have the same format as set via `saveValueFormat()`.
+ If a custom format is used, that format has to be set via `saveValueFormat()` before calling any of the setters.
+- `saveValueFormat($saveValueFormat)` and `getSaveValueFormat()` can be used to specify the date format of the value returned by `getSaveValue()`.
+ By default, `U` is used as format.
+ The [PHP manual](https://secure.php.net/manual/en/function.date.php) provides an overview of supported formats.
+- `supportTime($supportsTime = true)` and `supportsTime()` can be used to toggle whether, in addition to a date, a time can also be specified.
+ By default, specifying a time is disabled.
+
+
+### `DescriptionFormField`
+
+`DescriptionFormField` is a [multi-line text form field](#multilinetextformfield) with `description` as the default id and `wcf.global.description` as the default label.
+
+
+### `FloatFormField`
+
+`FloatFormField` is an implementation of [AbstractNumericFormField](#abstractnumericformfield) for floating point numbers.
+
+
+### `IconFormField`
+
+`IconFormField` is a form field to select a FontAwesome icon.
+
+
+### `IntegerFormField`
+
+`IntegerFormField` is an implementation of [AbstractNumericFormField](#abstractnumericformfield) for integers.
+
+
+### `IsDisabledFormField`
+
+`IsDisabledFormField` is a [boolean form field](#booleanformfield) with `isDisabled` as the default id.
+
+
+### `ItemListFormField`
+
+`ItemListFormField` is a form field in which multiple values can be entered and returned in different formats as save value.
+The `saveValueType($saveValueType)` and `getSaveValueType()` methods are specific to this form field class and determine the format of the save value.
+The following save value types are supported:
+
+- `ItemListFormField::SAVE_VALUE_TYPE_ARRAY` adds a custom data processor that writes the form field data directly in the parameters array and not in the data sub-array of the parameters array.
+- `ItemListFormField::SAVE_VALUE_TYPE_CSV` lets the value be returned as a string in which the values are concatenated by commas.
+- `ItemListFormField::SAVE_VALUE_TYPE_NSV` lets the value be returned as a string in which the values are concatenated by `\n`.
+- `ItemListFormField::SAVE_VALUE_TYPE_SSV` lets the value be returned as a string in which the values are concatenated by spaces.
+
+By default, `ItemListFormField::SAVE_VALUE_TYPE_CSV` is used.
+
+If `ItemListFormField::SAVE_VALUE_TYPE_ARRAY` is used as save value type, `ItemListFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant array into the `$parameters` array directly using the object property as the array key.
+
+
+### `MultilineTextFormField`
+
+`MultilineTextFormField` is a [text form field](#textformfield) that supports multiple rows of text.
+The methods `rows($rows)` and `getRows()` can be used to set and get the number of rows of the `textarea` elements.
+The default number of rows is `10`.
+These methods do **not**, however, restrict the number of text rows that canbe entered.
+
+
+### `MultipleSelectionFormField`
+
+`MultipleSelectionFormField` is a form fields that allows the selection of multiple options out of a predefined list of available options.
+The class implements `IFilterableSelectionFormField`, `IImmutableFormField`, and `INullableFormField`.
+If the field is nullable and no option is selected, `null` is returned as the save value.
+
+
+### `RadioButtonFormField`
+
+`RadioButtonFormField` is a form fields that allows the selection of a single option out of a predefined list of available options using radiobuttons.
+The class implements `IImmutableFormField` and `ISelectionFormField`.
+
+
+### `RatingFormField`
+
+`RatingFormField` is a form field to set a rating for an object.
+The class implements `IImmutableFormField`, `IMaximumFormField`, `IMinimumFormField`, and `INullableFormField`.
+Form fields of this class have `rating` as their default id, `wcf.form.field.rating` as their default label, `1` as their default minimum, and `5` as their default maximum.
+For this field, the minimum and maximum refer to the minimum and maximum rating an object can get.
+When the field is shown, there will be `maximum() - minimum() + 1` icons be shown with additional CSS classes that can be set and gotten via `defaultCssClasses(array $cssClasses)` and `getDefaultCssClasses()`.
+If a rating values is set, the first `getValue()` icons will instead use the classes that can be set and gotten via `activeCssClasses(array $cssClasses)` and `getActiveCssClasses()`.
+By default, the only default class is `fa-star-o` and the active classes are `fa-star` and `orange`.
+
+
+### `ShowOrderFormField`
+
+`ShowOrderFormField` is a [single selection form field](#singleselectionformfield) for which the selected value determines the position at which an object is shown.
+The show order field provides a list of all siblings and the object will be positioned **after** the selected sibling.
+To insert objects at the very beginning, the `options()` automatically method prepends an additional option for that case so that only the existing siblings need to be passed.
+The default id of instances of this class is `showOrder` and their default label is `wcf.form.field.showOrder`.
+
+{% include callout.html content="It is important that the relevant object property is always kept updated. Whenever a new object is added or an existing object is edited or delete, the values of the other objects have to be adjusted to ensure consecutive numbering." type="info" %}
+
+
+### `SingleSelectionFormField`
+
+`SingleSelectionFormField` is a form fields that allows the selection of a single option out of a predefined list of available options.
+The class implements `IFilterableSelectionFormField`, `IImmutableFormField`, and `INullableFormField`.
+If the field is nullable and the current form field value is considered `empty` by PHP, `null` is returned as the save value.
+
+
+### `SortOrderFormField`
+
+`SingleSelectionFormField` is a [single selection form field](#singleselectionformfield) with default id `sortOrder`, default label `wcf.global.showOrder` and default options `ASC: wcf.global.sortOrder.ascending` and `DESC: wcf.global.sortOrder.descending`.
+
+
+### `TextFormField`
+
+`TextFormField` is a form field that allows entering a single line of text.
+The class implements `IImmutableFormField`, `II18nFormField`, `IMaximumLengthFormField`, `IMinimumLengthFormField`, and `IPlaceholderFormField`.
+
+
+### `TitleFormField`
+
+`TitleFormField` is a [text form field](#textformfield) with `title` as the default id and `wcf.global.title` as the default label.
+
+
+### `UrlFormField`
+
+`UrlFormField` is a [text form field](#textformfield) whose values are checked via `Url::is()`.
+
+
+
+## Specific Fields
+
+The following form fields are reusable fields that generally are bound to a certain API or `DatabaseObject` implementation.
+
+
+### `AclFormField`
+
+`AclFormField` is used for setting up acl values for specific objects.
+The class implements `IObjectTypeFormField` and requires an object type of the object type definition `com.woltlab.wcf.acl`.
+Additionally, the class provides the methods `categoryName($categoryName)` and `getCategoryName()` that allow setting a specific name or filter for the acl option categories whose acl options are shown.
+A category name of `null` signals that no category filter is used.
+
+`AclFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant ACL object type id into the `$parameters` array directly using `{$objectProperty}_aclObjectTypeID` as the array key.
+The relevant database object action method is expected, based on the given ACL object type id, to save the ACL option values appropriately.
+
+
+### `CaptchaFormField`
+
+`CaptchaFormField` is used to add captcha protection to the form.
+
+You must specify a captcha object type (`com.woltlab.wcf.captcha`) using the `objectType()` method.
+
+
+### `ContentLanguageFormField`
+
+`ContentLanguageFormField` is used to select the content language of an object.
+Fields of this class are only available if multilingualism is enabled and if there are content languages.
+The class implements `IImmutableFormField`.
+
+
+### `LabelFormField`
+
+`LabelFormField` is used to select a label from a specific label group.
+The class implements `IObjectTypeFormNode`.
+
+The `labelGroup(ViewableLabelGroup $labelGroup)` and `getLabelGroup()` methods are specific to this form field class and can be used to set and get the label group whose labels can be selected.
+Additionally, there is the static method `createFields($objectType, array $labelGroups, $objectProperty = 'labelIDs)` that can be used to create all relevant label form fields for a given list of label groups.
+In most cases, `LabelFormField::createFields()` should be used.
+
+
+### `OptionFormField`
+
+`OptionFormField` is an [item list form field](#itemlistformfield) to set a list of options.
+The class implements `IPackagesFormField` and only options of the set packages are considered available.
+The default label of instances of this class is `wcf.form.field.option` and their default id is `options`.
+
+
+### `SimpleAclFormField`
+
+`SimpleAclFormField` is used for setting up simple acl values (one `yes`/`no` option per user and user group) for specific objects.
+
+`SimpleAclFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant simple ACL data array into the `$parameters` array directly using the object property as the array key.
+
+
+### `SingleMediaSelectionFormField`
+
+`SingleMediaSelectionFormField` is used to select a specific media file.
+The class implements `IImmutableFormField`.
+
+The following methods are specific to this form field class:
+
+- `imageOnly($imageOnly = true)` and `isImageOnly()` can be used to set and check if only images may be selected.
+- `getMedia()` returns the media file based on the current field value if a field is set.
+
+
+### `TagFormField`
+
+`TagFormField` is a form field to enter tags.
+Arrays passed to `TagFormField::values()` can contain tag names as strings and `Tag` objects.
+The default label of instances of this class is `wcf.tagging.tags` and their default description is `wcf.tagging.tags.description`.
+
+`TagFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the array with entered tag names into the `$parameters` array directly using the object property as the array key.
+
+
+### `UploadFormField`
+
+`UploadFormField` is a form field that allows uploading files by the user.
+
+`UploadFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the array of `wcf\system\file\upload\UploadFile\UploadFile` into the `$parameters` array directly using the object property as the array key. Also it registers the removed files as an array of `wcf\system\file\upload\UploadFile\UploadFile` into the `$parameters` array directly using the object property with the suffix `_removedFiles` as the array key.
+
+The field supports additional settings:
+- `imageOnly($imageOnly = true)` and `isImageOnly()` can be used to ensure that the uploaded files are only images.
+- `allowSvgImage($allowSvgImages = true)` and `svgImageAllowed()` can be used to allow SVG images, if the image only mode is enabled (otherwise, the method will throw an exception). By default, SVG images are not allowed.
+
+#### Provide value from database object
+
+To provide values from a database object, you should implement the method `get{$objectProperty}UploadFileLocations()` to your database object class. This method must return an array of strings with the locations of the files.
+
+#### Process files
+
+To process files in the database object action class, you must [`rename`](https://secure.php.net/manual/en/function.rename.php) the file to the final destination. You get the temporary location, by calling the method `getLocation()` on the given `UploadFile` objects. After that, you call `setProcessed($location)` with `$location` contains the new file location. This method sets the `isProcessed` flag to true and saves the new location. For updating files, it is relevant, whether a given file is already processed or not. For this case, the `UploadFile` object has an method `isProcessed()` which indicates, whether a file is already processed or new uploaded.
+
+
+### `UserFormField`
+
+`UserFormField` is a form field to enter existing users.
+The class implements `IAutoFocusFormField`, `IImmutableFormField`, `IMultipleFormField`, and `INullableFormField`.
+While the user is presented the names of the specified users in the user interface, the field returns the ids of the users as data.
+The relevant `UserProfile` objects can be accessed via the `getUsers()` method.
+
+
+### `UserGroupOptionFormField`
+
+`UserGroupOptionFormField` is an [item list form field](#itemlistformfield) to set a list of user group options/permissions.
+The class implements `IPackagesFormField` and only user group options of the set packages are considered available.
+The default label of instances of this class is `wcf.form.field.userGroupOption` and their default id is `permissions`.
+
+
+### `UsernameFormField`
+
+`UsernameFormField` is used for entering one non-existing username.
+The class implements `IImmutableFormField`, `IMaximumLengthFormField`, `IMinimumLengthFormField`, `INullableFormField`, and `IPlaceholderFormField`.
+As usernames have a system-wide restriction of a minimum length of 3 and a maximum length of 100 characters, these values are also used as the default value for the field’s minimum and maximum length.
+
+
+
+## Wysiwyg form container
+
+To integrate a wysiwyg editor into a form, you have to create a `WysiwygFormContainer` object.
+This container takes care of creating all necessary form nodes listed below for a wysiwyg editor.
+
+{% include callout.html content="When creating the container object, its id has to be the id of the form field that will manage the actual text." type="warning" %}
+
+The following methods are specific to this form container class:
+
+- `addSettingsNode(IFormChildNode $settingsNode)` and `addSettingsNodes(array $settingsNodes)` can be used to add nodes to the settings tab container.
+- `attachmentData($objectType, $parentObjectID)` can be used to set the data relevant for attachment support.
+ By default, not attachment data is set, thus attachments are not supported.
+- `getAttachmentField()`, `getPollContainer()`, `getSettingsContainer()`, `getSmiliesContainer()`, and `getWysiwygField()` can be used to get the different components of the wysiwyg form container once the form has been built.
+- `enablePreviewButton($enablePreviewButton)` can be used to set whether the preview button for the message is shown or not.
+ By default, the preview button is shown.
+ This method is only relevant before the form is built.
+ Afterwards, the preview button availability can not be changed.
+ Only available since WoltLab Suite Core 5.3.
+- `getObjectId()` returns the id of the edited object or `0` if no object is edited.
+- `getPreselect()`, `preselect($preselect)` can be used to set the value of the wysiwyg tab menu's `data-preselect` attribute used to determine which tab is preselected.
+ By default, the preselect is `'true'` which is used to pre-select the first tab.
+- `messageObjectType($messageObjectType)` can be used to set the message object type.
+- `pollObjectType($pollObjectType)` can be used to set the poll object type.
+ By default, no poll object type is set, thus the poll form field container is not available.
+- `supportMentions($supportMentions)` can be used to set if mentions are supported.
+ By default, mentions are not supported.
+ This method is only relevant before the form is built.
+ Afterwards, mention support can only be changed via the wysiwyg form field.
+- `supportSmilies($supportSmilies)` can be used to set if smilies are supported.
+ By default, smilies are supported.
+ This method is only relevant before the form is built.
+ Afterwards, smiley availability can only be changed via changing the availability of the smilies form container.
+
+### `WysiwygAttachmentFormField`
+
+`WysiwygAttachmentFormField` provides attachment support for a wysiwyg editor via a tab in the menu below the editor.
+This class should not be used directly but only via `WysiwygFormContainer`.
+The methods `attachmentHandler(AttachmentHandler $attachmentHandler)` and `getAttachmentHandler()` can be used to set and get the `AttachmentHandler` object that is used for uploaded attachments.
+
+### `WysiwygPollFormContainer`
+
+`WysiwygPollFormContainer` provides poll support for a wysiwyg editor via a tab in the menu below the editor.
+This class should not be used directly but only via `WysiwygFormContainer`.
+`WysiwygPollFormContainer` contains all form fields that are required to create polls and requires edited objects to implement `IPollContainer`.
+
+The following methods are specific to this form container class:
+
+- `getEndTimeField()` returns the form field to set the end time of the poll once the form has been built.
+- `getIsChangeableField()` returns the form field to set if poll votes can be changed once the form has been built.
+- `getIsPublicField()` returns the form field to set if poll results are public once the form has been built.
+- `getMaxVotesField()` returns the form field to set the maximum number of votes once the form has been built.
+- `getOptionsField()` returns the form field to set the poll options once the form has been built.
+- `getQuestionField()` returns the form field to set the poll question once the form has been built.
+- `getResultsRequireVoteField()` returns the form field to set if viewing the poll results requires voting once the form has been built.
+- `getSortByVotesField()` returns the form field to set if the results are sorted by votes once the form has been built.
+
+### `WysiwygSmileyFormContainer`
+
+`WysiwygSmileyFormContainer` provides smiley support for a wysiwyg editor via a tab in the menu below the editor.
+This class should not be used directly but only via `WysiwygFormContainer`.
+`WysiwygSmileyFormContainer` creates a sub-tab for each smiley category.
+
+#### `WysiwygSmileyFormNode`
+
+`WysiwygSmileyFormNode` is contains the smilies of a specific category.
+This class should not be used directly but only via `WysiwygSmileyFormContainer`.
+
+### Example
+
+The following code creates a WYSIWYG editor component for a `message` object property.
+As smilies are supported by default and an attachment object type is given, the tab menu below the editor has two tabs: “Smilies” and “Attachments”.
+Additionally, mentions and quotes are supported.
+
+```php
+WysiwygFormContainer::create('message')
+ ->label('foo.bar.message')
+ ->messageObjectType('com.example.foo.bar')
+ ->attachmentData('com.example.foo.bar')
+ ->supportMentions()
+ ->supportQuotes()
+```
+
+
+### `WysiwygFormField`
+
+`WysiwygFormField` is used for wysiwyg editor form fields.
+This class should, in general, not be used directly but only via `WysiwygFormContainer`.
+The class implements `IMaximumLengthFormField`, `IMinimumLengthFormField`, and `IObjectTypeFormNode` and requires an object type of the object type definition `com.woltlab.wcf.message`.
+The following methods are specific to this form field class:
+
+- `autosaveId($autosaveId)` and `getAutosaveId()` can be used enable automatically saving the current editor contents in the browser using the given id.
+ An empty string signals that autosaving is disabled.
+- `lastEditTime($lastEditTime)` and `getLastEditTime()` can be used to set the last time the contents have been edited and saved so that the JavaScript can determine if the contents stored in the browser are older or newer.
+ `0` signals that no last edit time has been set.
+- `supportAttachments($supportAttachments)` and `supportsAttachments()` can be used to set and check if the form field supports attachments.
+
+ {% include callout.html content="It is not sufficient to simply signal attachment support via these methods for attachments to work. These methods are relevant internally to signal the Javascript code that the editor supports attachments. Actual attachment support is provided by `WysiwygAttachmentFormField`." type="warning" %}
+- `supportMentions($supportMentions)` and `supportsMentions()` can be used to set and check if the form field supports mentions of other users.
+
+`WysiwygFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant simple ACL data array into the `$parameters` array directly using the object property as the array key.
+
+
+### `TWysiwygFormNode`
+
+All form nodes that need to know the id of the `WysiwygFormField` field should use `TWysiwygFormNode`.
+This trait provides `getWysiwygId()` and `wysiwygId($wysiwygId)` to get and set the relevant wysiwyg editor id.
+
+
+
+## Single-Use Form Fields
+
+The following form fields are specific for certain forms and hardly reusable in other contexts.
+
+
+### `BBCodeAttributesFormField`
+
+`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the attributes of a BBCode.
+
+
+### `DevtoolsProjectExcludedPackagesFormField`
+
+`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the excluded packages of a devtools project.
+
+
+### `DevtoolsProjectInstructionsFormField`
+
+`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the installation and update instructions of a devtools project.
+
+
+### `DevtoolsProjectOptionalPackagesFormField`
+
+`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the optional packages of a devtools project.
+
+
+### `DevtoolsProjectRequiredPackagesFormField`
+
+`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the required packages of a devtools project.
--- /dev/null
+---
+title: Structure of Form Builder
+sidebar: sidebar
+permalink: php_api_form_builder-structure.html
+folder: php/api/formBuilder
+parent: php_api_form_builder
+---
+
+Forms built with form builder consist of three major structural elements listed from top to bottom:
+
+1. form document,
+1. form container,
+1. form field.
+
+The basis for all three elements are form nodes.
+
+{% include callout.html content="The form builder API uses fluent interfaces heavily, meaning that unless a method is a getter, it generally returns the objects itself to support method chaining." type="info" %}
+
+
+## Form Nodes
+
+- `IFormNode` is the base interface that any node of a form has to implement.
+- `IFormChildNode` extends `IFormNode` for such elements of a form that can be a child node to a parent node.
+- `IFormParentNode` extends `IFormNode` for such elements of a form that can be a parent to child nodes.
+- `IFormElement` extends `IFormNode` for such elements of a form that can have a description and a label.
+
+
+### `IFormNode` / `TFormNode`
+
+`IFormNode` is the base interface that any node of a form has to implement and it requires the following methods:
+
+- `addClass($class)`, `addClasses(array $classes)`, `removeClass($class)`, `getClasses()`, and `hasClass($class)` add, remove, get, and check for CSS classes of the HTML element representing the form node.
+ If the form node consists of multiple (nested) HTML elements, the classes are generally added to the top element.
+ `static validateClass($class)` is used to check if a given CSS class is valid.
+ By default, a form node has no CSS classes.
+- `addDependency(IFormFieldDependency $dependency)`, `removeDependency($dependencyId)`, `getDependencies()`, and `hasDependency($dependencyId)` add, remove, get, and check for dependencies of this form node on other form fields.
+ `checkDependencies()` checks if **all** of the node’s dependencies are met and returns a boolean value reflecting the check’s result.
+ The [form builder dependency documentation](php_api_form_builder-dependencies.html) provides more detailed information about dependencies and how they work.
+ By default, a form node has no dependencies.
+- `attribute($name, $value = null)`, `removeAttribute($name)`, `getAttribute($name)`, `getAttributes()`, `hasAttribute($name)` add, remove, get, and check for attributes of the HTML element represting the form node.
+ The attributes are added to the same element that the CSS classes are added to.
+ `static validateAttribute($name)` is used to check if a given attribute is valid.
+ By default, a form node has no attributes.
+- `available($available = true)` and `isAvailable()` can be used to set and check if the node is available.
+ The availability functionality can be used to easily toggle form nodes based, for example, on options without having to create a condition to append the relevant.
+ This way of checking availability makes it easier to set up forms.
+ By default, every form node is available.
+
+ The following aspects are important when working with availability:
+
+ - Unavailable fields produce no output, their value is not read, they are not validated and they are not checked for save values.
+ - Form fields are also able to mark themselves as unavailable, for example, a selection field without any options.
+ - Form containers are automatically unavailable if they contain no available children.
+
+ Availability sets the static availability for form nodes that does not change during the lifetime of a form.
+ In contrast, dependencies represent a dynamic availability for form nodes that depends on the current value of certain form fields.
+- `cleanup()` is called after the whole form is not used anymore to reset other APIs if the form fields depends on them and they expect such a reset.
+ This method is not intended to clean up the form field’s value as a new form document object is created to show a clean form.
+- `getDocument()` returns the `IFormDocument` object the node belongs to.
+ (As `IFormDocument` extends `IFormNode`, form document objects simply return themselves.)
+- `getHtml()` returns the HTML representation of the node.
+ `getHtmlVariables()` return template variables (in addition to the form node itself) to render the node’s HTML representation.
+- `id($id)` and `getId()` set and get the id of the form node.
+ Every id has to be unique within a form.
+ `getPrefixedId()` returns the prefixed version of the node’s id (see `IFormDocument::getPrefix()` and `IFormDocument::prefix()`).
+ `static validateId($id)` is used to check if a given id is valid.
+- `populate()` is called by `IFormDocument::build()` after all form nodes have been added.
+ This method should finilize the initialization of the form node after all parent-child relations of the form document have been established.
+ This method is needed because during the construction of a form node, it neither knows the form document it will belong to nor does it know its parent.
+- `validate()` checks, after the form is submitted, if the form node is valid.
+ A form node with children is valid if all of its child nodes are valid.
+ A form field is valid if its value is valid.
+- `static create($id)` is the factory method that has to be used to create new form nodes with the given id.
+
+`TFormNode` provides a default implementation of most of these methods.
+
+
+### `IFormChildNode` / `TFormChildNode`
+
+`IFormChildNode` extends `IFormNode` for such elements of a form that can be a child node to a parent node and it requires the `parent(IFormParentNode $parentNode)` and `getParent()` methods used to set and get the node’s parent node.
+`TFormChildNode` provides a default implementation of these two methods and also of `IFormNode::getDocument()`.
+
+
+### `IFormParentNode` / `TFormParentNode`
+
+`IFormParentNode` extends `IFormNode` for such elements of a form that can be a parent to child nodes.
+Additionally, the interface also extends `\Countable` and `\RecursiveIterator`.
+The interface requires the following methods:
+
+- `appendChild(IFormChildNode $child)`, `appendChildren(array $children)`, `insertAfter(IFormChildNode $child, $referenceNodeId)`, and `insertBefore(IFormChildNode $child, $referenceNodeId)` are used to insert new children either at the end or at specific positions.
+ `validateChild(IFormChildNode $child)` is used to check if a given child node can be added.
+ A child node cannot be added if it would cause an id to be used twice.
+- `children()` returns the direct children of a form node.
+- `getIterator()` return a recursive iterator for a form node.
+- `getNodeById($nodeId)` returns the node with the given id by searching for it in the node’s children and recursively in all of their children.
+ `contains($nodeId)` can be used to simply check if a node with the given id exists.
+- `hasValidationErrors()` checks if a form node or any of its children has a validation error (see `IFormField::getValidationErrors()`).
+- `readValues()` recursively calls `IFormParentNode::readValues()` and `IFormField::readValue()` on its children.
+
+
+### `IFormElement` / `TFormElement`
+
+`IFormElement` extends `IFormNode` for such elements of a form that can have a description and a label and it requires the following methods:
+
+- `label($languageItem = null, array $variables = [])` and `getLabel()` can be used to set and get the label of the form element.
+ `requiresLabel()` can be checked if the form element requires a label.
+ A label-less form element that requires a label will prevent the form from being rendered by throwing an exception.
+- `description($languageItem = null, array $variables = [])` and `getDescription()` can be used to set and get the description of the form element.
+
+
+### `IObjectTypeFormNode` / `TObjectTypeFormNode`
+
+`IObjectTypeFormField` has to be implemented by form nodes that rely on a object type of a specific object type definition in order to function.
+The implementing class has to implement the methods `objectType($objectType)`, `getObjectType()`, and `getObjectTypeDefinition()`.
+`TObjectTypeFormNode` provides a default implementation of these three methods.
+
+
+### `CustomFormNode`
+
+`CustomFormNode` is a form node whose contents can be set directly via `content($content)`.
+
+{% include callout.html content="This class should generally not be relied on. Instead, `TemplateFormNode` should be used." type="warning" %}
+
+
+### `TemplateFormNode`
+
+`TemplateFormNode` is a form node whose contents are read from a template.
+`TemplateFormNode` has the following additional methods:
+
+- `application($application)` and `getApplicaton()` can be used to set and get the abbreviation of the application the shown template belongs to.
+ If no template has been set explicitly, `getApplicaton()` returns `wcf`.
+- `templateName($templateName)` and `getTemplateName()` can be used to set and get the name of the template containing the node contents.
+ If no template has been set and the node is rendered, an exception will be thrown.
+- `variables(array $variables)` and `getVariables()` can be used to set and get additional variables passed to the template.
+
+
+## Form Document
+
+A form document object represents the form as a whole and has to implement the `IFormDocument` interface.
+WoltLab Suite provides a default implementation with the `FormDocument` class.
+`IFormDocument` should not be implemented directly but instead `FormDocument` should be extended to avoid issues if the `IFormDocument` interface changes in the future.
+
+`IFormDocument` extends `IFormParentNode` and requires the following additional methods:
+
+- `action($action)` and `getAction()` can be used set and get the `action` attribute of the `<form>` HTML element.
+- `addButton(IFormButton $button)` and `getButtons()` can be used add and get form buttons that are shown at the bottom of the form.
+ `addDefaultButton($addDefaultButton)` and `hasDefaultButton()` can be used to set and check if the form has the default button which is added by default unless specified otherwise.
+ Each implementing class may define its own default button.
+ `FormDocument` has a button with id `submitButton`, label `wcf.global.button.submit`, access key `s`, and CSS class `buttonPrimary` as its default button.
+- `ajax($ajax)` and `isAjax()` can be used to set and check if the form document is requested via an AJAX request or processes data via an AJAX request.
+ These methods are helpful for form fields that behave differently when providing data via AJAX.
+- `build()` has to be called once after all nodes have been added to this document to trigger `IFormNode::populate()`.
+- `formMode($formMode)` and `getFormMode()` sets the form mode.
+ Possible form modes are:
+
+ - `IFormDocument::FORM_MODE_CREATE` has to be used when the form is used to create a new object.
+ - `IFormDocument::FORM_MODE_UPDATE` has to be used when the form is used to edit an existing object.
+- `getData()` returns the array containing the form data and which is passed as the `$parameters` argument of the constructor of a database object action object.
+- `getDataHandler()` returns the data handler for this document that is used to process the field data into a parameters array for the constructor of a database object action object.
+- `getEnctype()` returns the encoding type of the form.
+ If the form contains a `IFileFormField`, `multipart/form-data` is returned, otherwise `null` is returned.
+- `loadValues(array $data, IStorableObject $object)` is used when editing an existing object to set the form field values by calling `IFormField::loadValue()` for all form fields.
+ Additionally, the form mode is set to `IFormDocument::FORM_MODE_UPDATE`.
+- `method($method)` and `getMethod()` can be used to set and get the `method` attribute of the `<form>` HTML element.
+ By default, the method is `post`.
+- `prefix($prefix)` and `getPrefix()` can be used to set and get a global form prefix that is prepended to form elements’ names and ids to avoid conflicts with other forms.
+ By default, the prefix is an empty string.
+ If a prefix of `foo` is set, `getPrefix()` returns `foo_` (additional trailing underscore).
+- `requestData(array $requestData)`, `getRequestData($index = null)`, and `hasRequestData($index = null)` can be used to set, get and check for specific request data.
+ In most cases, the relevant request data is the `$_POST` array.
+ In default AJAX requests handled by database object actions, however, the request data generally is in `AbstractDatabaseObjectAction::$parameters`.
+ By default, `$_POST` is the request data.
+
+The last aspect is relevant for `DialogFormDocument` objects.
+`DialogFormDocument` is a specialized class for forms in dialogs that, in contrast to `FormDocument` do not require an `action` to be set.
+Additionally, `DialogFormDocument` provides the `cancelable($cancelable = true)` and `isCancelable()` methods used to determine if the dialog from can be canceled.
+By default, dialog forms are cancelable.
+
+
+## Form Button
+
+A form button object represents a button shown at the end of the form that, for example, submits the form.
+Every form button has to implement the `IFormButton` interface that extends `IFormChildNode` and `IFormElement`.
+`IFormButton` requires four methods to be implemented:
+
+- `accessKey($accessKey)` and `getAccessKey()` can be used to set and get the access key with which the form button can be activated.
+ By default, form buttons have no access key set.
+- `submit($submitButton)` and `isSubmit()` can be used to set and check if the form button is a submit button.
+ A submit button is an `input[type=submit]` element.
+ Otherwise, the button is a `button` element.
+
+
+## Form Container
+
+A form container object represents a container for other form containers or form field directly.
+Every form container has to implement the `IFormContainer` interface which requires the following method:
+
+- `loadValues(array $data, IStorableObject $object)` is called by `IFormDocument::loadValuesFromObject()` to inform the container that object data is loaded.
+ This method is *not* intended to generally call `IFormField::loadValues()` on its form field children as these methods are already called by `IFormDocument::loadValuesFromObject()`.
+ This method is intended for specialized form containers with more complex logic.
+
+There are multiple default container implementations:
+
+1. `FormContainer` is the default implementation of `IFormContainer`.
+1. `TabMenuFormContainer` represents the container of tab menu, while
+1. `TabFormContainer` represents a tab of a tab menu and
+1. `TabTabMenuFormContainer` represents a tab of a tab menu that itself contains a tab menu.
+1. The children of `RowFormContainer` are shown in a row and should use `col-*` classes.
+1. The children of `RowFormFieldContainer` are also shown in a row but does not show the labels and descriptions of the individual form fields.
+ Instead of the individual labels and descriptions, the container's label and description is shown and both span all of fields.
+1. `SuffixFormFieldContainer` can be used for one form field with a second selection form field used as a suffix.
+
+The methods of the interfaces that `FormContainer` is implementing are well documented, but here is a short overview of the most important methods when setting up a form or extending a form with an event listener:
+
+- `appendChild(IFormChildNode $child)`, `appendChildren(array $children)`, and `insertBefore(IFormChildNode $child, $referenceNodeId)` are used to insert new children into the form container.
+- `description($languageItem = null, array $variables = [])` and `label($languageItem = null, array $variables = [])` are used to set the description and the label or title of the form container.
+
+
+## Form Field
+
+A form field object represents a concrete form field that allows entering data.
+Every form field has to implement the `IFormField` interface which extends `IFormChildNode` and `IFormElement`.
+
+`IFormField` requires the following additional methods:
+
+- `addValidationError(IFormFieldValidationError $error)` and `getValidationErrors()` can be used to get and set validation errors of the form field (see [form validation](php_api_form_builder-validation_data.html#form-validation)).
+- `addValidator(IFormFieldValidator $validator)`, `getValidators()`, `removeValidator($validatorId)`, and `hasValidator($validatorId)` can be used to get, set, remove, and check for validators for the form field (see [form validation](php_api_form_builder-validation_data.html#form-validation)).
+- `getFieldHtml()` returns the field's HTML output without the surrounding `dl` structure.
+- `objectProperty($objectProperty)` and `getObjectProperty()` can be used to get and set the object property that the field represents.
+ When setting the object property is set to an empty string, the previously set object property is unset.
+ If no object property has been set, the field’s (non-prefixed) id is returned.
+
+ The object property allows having different fields (requiring different ids) that represent the same object property which is handy when available options of the field’s value depend on another field.
+ Having object property allows to define different fields for each value of the other field and to use form field dependencies to only show the appropriate field.
+- `readValue()` reads the form field value from the request data after the form is submitted.
+- `required($required = true)` and `isRequired()` can be used to determine if the form field has to be filled out.
+ By default, form fields do not have to be filled out.
+- `value($value)` and `getSaveValue()` can be used to get and set the value of the form field to be used outside of the context of forms.
+ `getValue()`, in contrast, returns the internal representation of the form field’s value.
+ In general, the internal representation is only relevant when validating the value in additional validators.
+ `loadValue(array $data, IStorableObject $object)` extracts the form field value from the given data array (and additional, non-editable data from the object if the field needs them).
+
+`AbstractFormField` provides default implementations of many of the listed methods above and should be extended instead of implementing `IFormField` directly.
+
+An overview of the form fields provided by default can be found [here](php_api_form_builder-form_fields.html).
+
+
+### Form Field Interfaces and Traits
+
+WoltLab Suite Core provides a variety of interfaces and matching traits with default implementations for several common features of form fields:
+
+
+#### `IAutoFocusFormField` / `TAutoFocusFormField`
+
+`IAutoFocusFormField` has to be implemented by form fields that can be auto-focused.
+The implementing class has to implement the methods `autoFocus($autoFocus = true)` and `isAutoFocused()`.
+By default, form fields are not auto-focused.
+`TAutoFocusFormField` provides a default implementation of these two methods.
+
+
+#### `IFileFormField`
+
+`IFileFormField` has to be implemented by every form field that uploads files so that the `enctype` attribute of the form document is `multipart/form-data` (see `IFormDocument::getEnctype()`).
+
+
+#### `IFilterableSelectionFormField` / `TFilterableSelectionFormField`
+
+`IFilterableSelectionFormField` extends `ISelectionFormField` by the possibilty for users when selecting the value(s) to filter the list of available options.
+The implementing class has to implement the methods `filterable($filterable = true)` and `isFilterable()`.
+`TFilterableSelectionFormField` provides a default implementation of these two methods.
+
+
+#### `II18nFormField` / `TI18nFormField`
+
+`II18nFormField` has to be implemented by form fields if the form field value can be entered separately for all available languages.
+The implementing class has to implement the following methods:
+
+- `i18n($i18n = true)` and `isI18n()` can be used to set whether a specific instance of the class actually supports multilingual input.
+- `i18nRequired($i18nRequired = true)` and `isI18nRequired()` can be used to set whether a specific instance of the class requires separate values for all languages.
+- `languageItemPattern($pattern)` and `getLanguageItemPattern()` can be used to set the pattern/regular expression for the language item used to save the multilingual values.
+- `hasI18nValues()` and `hasPlainValue()` check if the current value is a multilingual or monolingual value.
+
+`TI18nFormField` provides a default implementation of these eight methods and additional default implementations of some of the `IFormField` methods.
+If multilingual input is enabled for a specific form field, classes using `TI18nFormField` register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the array with multilingual input into the `$parameters` array directly using `{$objectProperty}_i18n` as the array key.
+If multilingual input is enabled but only a monolingual value is entered, the custom form field data processor does nothing and the form field’s value is added by the `DefaultFormDataProcessor` into the `data` sub-array of the `$parameters` array.
+
+{% include callout.html content="`TI18nFormField` already provides a default implementation of `IFormField::validate()`." type="info" %}
+
+
+#### `IImmutableFormField` / `TImmutableFormField`
+
+`IImmutableFormField` has to be implemented by form fields that support being displayed but whose value cannot be changed.
+The implementing class has to implement the methods `immutable($immutable = true)` and `isImmutable()` that can be used to determine if the value of the form field is mutable or immutable.
+By default, form field are mutable.
+
+
+#### `IMaximumFormField` / `TMaximumFormField`
+
+`IMaximumFormField` has to be implemented by form fields if the entered value must have a maximum value.
+The implementing class has to implement the methods `maximum($maximum = null)` and `getMaximum()`.
+A maximum of `null` signals that no maximum value has been set.
+`TMaximumFormField` provides a default implementation of these two methods.
+
+{% include callout.html content="The implementing class has to validate the entered value against the maximum value manually." type="warning" %}
+
+
+#### `IMaximumLengthFormField` / `TMaximumLengthFormField`
+
+`IMaximumLengthFormField` has to be implemented by form fields if the entered value must have a maximum length.
+The implementing class has to implement the methods `maximumLength($maximumLength = null)`, `getMaximumLength()`, and `validateMaximumLength($text, Language $language = null)`.
+A maximum length of `null` signals that no maximum length has been set.
+`TMaximumLengthFormField` provides a default implementation of these two methods.
+
+{% include callout.html content="The implementing class has to validate the entered value against the maximum value manually by calling `validateMaximumLength()`." type="warning" %}
+
+
+#### `IMinimumFormField` / `TMinimumFormField`
+
+`IMinimumFormField` has to be implemented by form fields if the entered value must have a minimum value.
+The implementing class has to implement the methods `minimum($minimum = null)` and `getMinimum()`.
+A minimum of `null` signals that no minimum value has been set.
+`TMinimumFormField` provides a default implementation of these three methods.
+
+{% include callout.html content="The implementing class has to validate the entered value against the minimum value manually." type="warning" %}
+
+
+#### `IMinimumLengthFormField` / `TMinimumLengthFormField`
+
+`IMinimumLengthFormField` has to be implemented by form fields if the entered value must have a minimum length.
+The implementing class has to implement the methods `minimumLength($minimumLength = null)`, `getMinimumLength()`, and `validateMinimumLength($text, Language $language = null)`.
+A minimum length of `null` signals that no minimum length has been set.
+`TMinimumLengthFormField` provides a default implementation of these three methods.
+
+{% include callout.html content="The implementing class has to validate the entered value against the minimum value manually by calling `validateMinimumLength()`." type="warning" %}
+
+
+#### `IMultipleFormField` / `TMultipleFormField`
+
+`IMinimumLengthFormField` has to be implemented by form fields that support selecting or setting multiple values.
+The implementing class has to implement the following methods:
+
+- `multiple($multiple = true)` and `allowsMultiple()` can be used to set whether a specific instance of the class actually should support multiple values.
+ By default, multiple values are not supported.
+- `minimumMultiples($minimum)` and `getMinimumMultiples()` can be used to set the minimum number of values that have to be selected/entered.
+ By default, there is no required minimum number of values.
+- `maximumMultiples($minimum)` and `getMaximumMultiples()` can be used to set the maximum number of values that have to be selected/entered.
+ By default, there is no maximum number of values.
+ `IMultipleFormField::NO_MAXIMUM_MULTIPLES` is returned if no maximum number of values has been set and it can also be used to unset a previously set maximum number of values.
+
+`TMultipleFormField` provides a default implementation of these six methods and classes using `TMultipleFormField` register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the `HtmlInputProcessor` object with the text into the `$parameters` array directly using `{$objectProperty}_htmlInputProcessor` as the array key.
+
+{% include callout.html content="The implementing class has to validate the values against the minimum and maximum number of values manually." type="warning" %}
+
+
+#### `INullableFormField` / `TNullableFormField`
+
+`INullableFormField` has to be implemented by form fields that support `null` as their (empty) value.
+The implementing class has to implement the methods `nullable($nullable = true)` and `isNullable()`.
+`TNullableFormField` provides a default implementation of these two methods.
+
+`null` should be returned by `IFormField::getSaveValue()` is the field is considered empty and the form field has been set as nullable.
+
+
+#### `IPackagesFormField` / `TPackagesFormField`
+
+`IPackagesFormField` has to be implemented by form fields that, in some way, considers packages whose ids may be passed to the field object.
+The implementing class has to implement the methods `packageIDs(array $packageIDs)` and `getPackageIDs()`.
+`TPackagesFormField` provides a default implementation of these two methods.
+
+
+#### `IPlaceholderFormField` / `TPlaceholderFormField`
+
+`IPlaceholderFormField` has to be implemented by form fields that support a placeholder value for empty fields.
+The implementing class has to implement the methods `placeholder($languageItem = null, array $variables = [])` and `getPlaceholder()`.
+`TPlaceholderFormField` provides a default implementation of these two methods.
+
+
+#### `ISelectionFormField` / `TSelectionFormField`
+
+`ISelectionFormField` has to be implemented by form fields with a predefined set of possible values.
+The implementing class has to implement the getter and setter methods `options($options, $nestedOptions = false, $labelLanguageItems = true)` and `getOptions()` and additionally two methods related to nesting, i.e. whether the selectable options have a hierarchy:
+`supportsNestedOptions()` and `getNestedOptions()`.
+`TSelectionFormField` provides a default implementation of these four methods.
+
+
+#### `ISuffixedFormField` / `TSuffixedFormField`
+
+`ISuffixedFormField` has to be implemented by form fields that support supports displaying a suffix behind the actual input field.
+The implementing class has to implement the methods `suffix($languageItem = null, array $variables = [])` and `getSuffix()`.
+`TSuffixedFormField` provides a default implementation of these two methods.
+
+
+#### `TDefaultIdFormField`
+
+Form fields that have a default id have to use `TDefaultIdFormField` and have to implement the method `getDefaultId()`.
+
+
+## Displaying Forms
+
+The only thing to do in a template to display the **whole** form including all of the necessary JavaScript is to put
+
+```smarty
+{@$form->getHtml()}
+```
+
+into the template file at the relevant position.
--- /dev/null
+---
+title: Form Validation and Form Data
+sidebar: sidebar
+permalink: php_api_form_builder-validation_data.html
+folder: php/api/formBuilder
+parent: php_api_form_builder
+---
+
+## Form Validation
+
+Every form field class has to implement `IFormField::validate()` according to their internal logic of what constitutes a valid value.
+If a certain constraint for the value is no met, a form field validation error object is added to the form field.
+Form field validation error classes have to implement the interface `IFormFieldValidationError`.
+
+In addition to intrinsic validations like checking the length of the value of a text form field, in many cases, there are additional constraints specific to the form like ensuring that the text is not already used by a different object of the same database object class.
+Such additional validations can be added to (and removed from) the form field via implementations of the `IFormFieldValidator` interface.
+
+
+### `IFormFieldValidationError` / `FormFieldValidationError`
+
+`IFormFieldValidationError` requires the following methods:
+
+- `__construct($type, $languageItem = null, array $information = [])` creates a new validation error object for an error with the given type and message stored in the given language items.
+ The information array is used when generating the error message.
+- `getHtml()` returns the HTML element representing the error that is shown to the user.
+- `getMessage()` returns the error message based on the language item and information array given in the constructor.
+- `getInformation()` and `getType()` are getters for the first and third parameter of the constructor.
+
+`FormFieldValidationError` is a default implementation of the interface that shows the error in an `small.innerError` HTML element below the form field.
+
+Form field validation errors are added to form fields via the `IFormField::addValidationError(IFormFieldValidationError $error)` method.
+
+
+### `IFormFieldValidator` / `FormFieldValidator`
+
+`IFormFieldValidator` requires the following methods:
+
+- `__construct($id, callable $validator)` creates a new validator with the given id that passes the validated form field to the given callable that does the actual validation.
+ `static validateId($id)` is used to check if the given id is valid.
+- `__invoke(IFormField $field)` is used when the form field is validated to execute the validator.
+- `getId()` returns the id of the validator.
+
+`FormFieldValidator` is a default implementation of the interface.
+
+Form field validators are added to form fields via the `addValidator(IFormFieldValidator $validator)` method.
+
+
+## Form Data
+
+After a form is successfully validated, the data of the form fields (returned by `IFormDocument::getData()`) have to be extracted which is the job of the `IFormDataHandler` object returned by `IFormDocument::getDataHandler()`.
+Form data handlers themselves, however, are only iterating through all `IFormDataProcessor` instances that have been registered with the data handler.
+
+
+### `IFormDataHandler` / `FormDataHandler`
+
+`IFormDataHandler` requires the following methods:
+
+- `addProcessor(IFormDataProcessor $processor)` adds a new data processor to the data handler.
+- `getFormData(IFormDocument $document)` returns the data of the given form by applying all registered data handlers on the form.
+- `getObjectData(IFormDocument $document, IStorableObject $object)` returns the data of the given object which will be used to populate the form field values of the given form.
+
+`FormDataHandler` is the default implementation of this interface and should also be extended instead of implementing the interface directly.
+
+
+### `IFormDataProcessor` / `DefaultFormDataProcessor`
+
+`IFormDataProcessor` requires the following methods:
+
+- `processFormData(IFormDocument $document, array $parameters)` is called by `IFormDataHandler::getFormData()`.
+ The method processes the given parameters array and returns the processed version.
+- `processObjectData(IFormDocument $document, array $data, IStorableObject $object)` is called by `IFormDataHandler::getObjectData()`.
+ The method processes the given object data array and returns the processed version.
+
+When `FormDocument` creates its `FormDataHandler` instance, it automatically registers an `DefaultFormDataProcessor` object as the first data processor.
+`DefaultFormDataProcessor` puts the save value of all form fields that are available and have a save value into `$parameters['data']` using the form field’s object property as the array key.
+
+{% include callout.html content="`IFormDataProcessor` should not be implemented directly. Instead, `AbstractFormDataProcessor` should be extended." type="warning" %}
+
+{% include callout.html content="All form data is put into the `data` sub-array so that the whole `$parameters` array can be passed to a database object action object that requires the actual database object data to be in the `data` sub-array." type="info" %}
+
+
+
+### Additional Data Processors
+
+#### `CustomFormDataProcessor`
+
+As mentioned above, the data in the `data` sub-array is intended to directly create or update the database object with.
+As these values are used in the database query directly, these values cannot contain arrays.
+Several form fields, however, store and return their data in form of arrays.
+Thus, this data cannot be returned by `IFormField::getSaveValue()` so that `IFormField::hasSaveValue()` returns `false` and the form field’s data is not collected by the standard `DefaultFormDataProcessor` object.
+
+Instead, such form fields register a `CustomFormDataProcessor` in their `IFormField::populate()` method that inserts the form field value into the `$parameters` array directly.
+This way, the relevant database object action method has access to the data to save it appropriately.
+
+The constructor of `CustomFormDataProcessor` requires an id (that is primarily used in error messages during the validation of the second parameter) and callables for `IFormDataProcessor::processFormData()` and `IFormDataProcessor::processObjectData()` which are passed the same parameters as the `IFormDataProcessor` methods.
+Only one of the callables has to be given, the other one then defaults to simply returning the relevant array unchanged.
+
+
+#### `VoidFormDataProcessor`
+
+Some form fields might only exist to toggle the visibility of other form fields (via dependencies) but the data of form field itself is irrelevant.
+As `DefaultFormDataProcessor` collects the data of all form fields, an additional data processor in the form of a `VoidFormDataProcessor` can be added whose constructor `__construct($property, $isDataProperty = true)` requires the name of the relevant object property/form id and whether the form field value is stored in the `data` sub-array or directory in the `$parameters` array.
+When the data processor is invoked, it checks whether the relevant entry in the `$parameters` array exists and voids it by removing it from the array.
--- /dev/null
+---
+title: Form Builder
+sidebar: sidebar
+permalink: php_api_form_builder.html
+folder: php/api
+---
+
+{% include callout.html content="Form builder is only available since WoltLab Suite Core 5.2." type="info" %}
+
+{% include callout.html content="The [migration guide for WoltLab Suite Core 5.2](migration_wsc-31_form-builder.html) provides some examples of how to migrate existing forms to form builder that can also help in understanding form builder if the old way of creating forms is familiar." type="info" %}
+
+
+## Advantages of Form Builder
+
+WoltLab Suite 5.2 introduces a new powerful way of creating forms: form builder.
+Before taking a closer look at form builder, let us recap how forms are created in previous versions:
+In general, for each form field, there is a corresponding property of the form's PHP class whose value has to be read from the request data, validated, and passed to the database object action to store the value in a database table.
+When editing an object, the property's value has to be set using the value of the corresponding property of the edited object.
+In the form's template, you have to write the `<form>` element with all of its children: the `<section>` elements, the `<dl>` elements, and, of course, the form fields themselves.
+In summary, this way of creating forms creates much duplicate or at least very similar code and makes it very time consuming if the structure of forms in general or a specific type of form field has to be changed.
+
+Form builder, in contrast, relies on PHP objects representing each component of the form, from the form itself down to each form field.
+This approach makes creating forms as easy as creating some PHP objects, populating them with all the relevant data, and one line of code in the template to print the form.
+
+
+## Form Builder Components
+
+Form builder consists of several components that are presented on the following pages:
+
+1. [Structure of form builder](php_api_form_builder-structure.html)
+1. [Form validation and form data](php_api_form_builder-validation_data.html)
+1. [Form node dependencies](php_api_form_builder-dependencies.html)
+
+{% include callout.html content="In general, form builder provides default implementation of interfaces by providing either abstract classes or traits.
+ It is expected that the interfaces are always implemented using these abstract classes and traits!
+ This way, if new methods are added to the interfaces, default implementations can be provided by the abstract classes and traits without causing backwards compatibility problems." type="warning" %}
+
+
+## `AbstractFormBuilderForm`
+
+To make using form builder easier, `AbstractFormBuilderForm` extends `AbstractForm` and provides most of the code needed to set up a form (of course without specific fields, those have to be added by the concrete form class), like reading and validating form values and using a database object action to use the form data to create or update a database object.
+
+In addition to the existing methods inherited by `AbstractForm`, `AbstractFormBuilderForm` provides the following methods:
+
+- `buildForm()` builds the form in the following steps:
+
+ 1. Call `AbtractFormBuilderForm::createForm()` to create the `IFormDocument` object and add the form fields.
+ 2. Call `IFormDocument::build()` to build the form.
+ 3. Call `AbtractFormBuilderForm::finalizeForm()` to finalize the form like adding dependencies.
+
+ Additionally, between steps 1 and 2 and after step 3, the method provides two events, `createForm` and `buildForm` to allow plugins to register event listeners to execute additional code at the right point in time.
+- `createForm()` creates the `FormDocument` object and sets the form mode.
+ Classes extending `AbstractFormBuilderForm` have to override this method (and call `parent::createForm()` as the first line in the overridden method) to add concrete form containers and form fields to the bare form document.
+- `finalizeForm()` is called after the form has been built and the complete form hierarchy has been established.
+ This method should be overridden to add dependencies, for example.
+- `setFormAction()` is called at the end of `readData()` and sets the form document’s action based on the controller class name and whether an object is currently edited.
+- If an object is edited, at the beginning of `readData()`, `setFormObjectData()` is called which calls `IFormDocument::loadValuesFromObject()`.
+ If values need to be loaded from additional sources, this method should be used for that.
+
+`AbstractFormBuilderForm` also provides the following (public) properties:
+
+- `$form` contains the `IFormDocument` object created in `createForm()`.
+- `$formAction` is either `create` (default) or `edit` and handles which method of the database object is called by default (`create` is called for `$formAction = 'create'` and `update` is called for `$formAction = 'edit'`) and is used to set the value of the `action` template variable.
+- `$formObject` contains the `IStorableObject` if the form is used to edit an existing object.
+ For forms used to create objects, `$formObject` is always `null`.
+ Edit forms have to manually identify the edited object based on the request data and set the value of `$formObject`.
+- `$objectActionName` can be used to set an alternative action to be executed by the database object action that deviates from the default action determined by the value of `$formAction`.
+- `$objectActionClass` is the name of the database object action class that is used to create or update the database object.
+
+
+## `DialogFormDocument`
+
+Form builder forms can also be used in dialogs.
+For such forms, `DialogFormDocument` should be used which provides the additional methods `cancelable($cancelable = true)` and `isCancelable()` to set and check if the dialog can be canceled.
+If a dialog form can be canceled, a cancel button is added.
+
+If the dialog form is fetched via an AJAX request, `IFormDocument::ajax()` has to be called.
+AJAX forms are registered with `WoltLabSuite/Core/Form/Builder/Manager` which also supports getting all of the data of a form via the `getData(formId)` function.
+The `getData()` function relies on all form fields creating and registering a `WoltLabSuite/Core/Form/Builder/Field/Field` object that provides the data of a specific field.
+
+To make it as easy as possible to work with AJAX forms in dialogs, `WoltLabSuite/Core/Form/Builder/Dialog` (abbreviated as `FormBuilderDialog` from now on) should generally be used instead of `WoltLabSuite/Core/Form/Builder/Manager` directly.
+The constructor of `FormBuilderDialog` expects the following parameters:
+
+- `dialogId`: id of the dialog element
+- `className`: PHP class used to get the form dialog (and save the data if `options.submitActionName` is set)
+- `actionName`: name of the action/method of `className` that returns the dialog; the method is expected to return an array with `formId` containg the id of the returned form and `dialog` containing the rendered form dialog
+- `options`: additional options:
+ - `actionParameters` (default: empty): additional parameters sent during AJAX requests
+ - `destroyOnClose` (default: `false`): if `true`, whenever the dialog is closed, the form is destroyed so that a new form is fetched if the dialog is opened again
+ - `dialog`: additional dialog options used as `options` during dialog setup
+ - `onSubmit`: callback when the form is submitted (takes precedence over `submitActionName`)
+ - `submitActionName` (default: not set): name of the action/method of `className` called when the form is submitted
+
+The three public functions of `FormBuilderDialog` are:
+
+- `destroy()` destroys the dialog, the form, and all of the form fields.
+- `getData()` returns a Promise that returns the form data.
+- `open()` opens the dialog.
+
+Example:
+
+```javascript
+require(['WoltLabSuite/Core/Form/Builder/Dialog'], function(FormBuilderDialog) {
+ var dialog = new FormBuilderDialog(
+ 'testDialog',
+ 'wcf\\data\\test\\TestAction',
+ 'getDialog',
+ {
+ destroyOnClose: true,
+ dialog: {
+ title: 'Test Dialog'
+ },
+ submitActionName: 'saveDialog'
+ }
+ );
+
+ elById('testDialogButton').addEventListener('click', function() {
+ dialog.open();
+ });
+});
+```
--- /dev/null
+---
+title: Package Installation Plugins
+sidebar: sidebar
+permalink: php_api_package_installation_plugins.html
+folder: php/api
+---
+
+A package installation plugin (PIP) defines the behavior to handle a specific [instruction](package_package-xml.html#instruction) during package installation, update or uninstallation.
+
+## `AbstractPackageInstallationPlugin`
+
+Any package installation plugin has to implement the [IPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/IPackageInstallationPlugin.class.php) interface.
+It is recommended however, to extend the abstract implementation [AbstractPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/AbstractPackageInstallationPlugin.class.php) of this interface instead of directly implementing the interface.
+The abstract implementation will always provide sane methods in case of any API changes.
+
+### Class Members
+
+Package Installation Plugins have a few notable class members easing your work:
+
+#### `$installation`
+
+This member contains an instance of [PackageInstallationDispatcher](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php) which provides you with all meta data related to the current package being processed.
+The most common usage is the retrieval of the package ID via `$this->installation->getPackageID()`.
+
+#### `$application`
+
+Represents the abbreviation of the target application, e.g. `wbb` (default value: `wcf`), used for the name of database table in which the installed data is stored.
+
+
+## `AbstractXMLPackageInstallationPlugin`
+
+[AbstractPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/AbstractPackageInstallationPlugin.class.php) is the default implementation for all package installation plugins based upon a single XML document.
+It handles the evaluation of the document and provide you an object-orientated approach to handle its data.
+
+### Class Members
+
+#### `$className`
+
+Value must be the qualified name of a class deriving from [DatabaseObjectEditor](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/DatabaseObjectEditor.class.php) which is used to create and update objects.
+
+#### `$tagName`
+
+Specifies the tag name within a `<import>` or `<delete>` section of the XML document used for each installed object.
+
+#### `prepareImport(array $data)`
+
+The passed array `$data` contains the parsed value from each evaluated tag in the `<import>` section:
+
+- `$data['elements']` contains a list of tag names and their value.
+- `$data['attributes']` contains a list of attributes present on the tag identified by [$tagName](#tagname).
+
+This method should return an one-dimensional array, where each key maps to the corresponding database column name (key names are case-sensitive).
+It will be passed to either `DatabaseObjectEditor::create()` or `DatabaseObjectEditor::update()`.
+
+Example:
+
+```php
+<?php
+return [
+ 'environment' => $data['elements']['environment'],
+ 'eventName' => $data['elements']['eventname'],
+ 'name' => $data['attributes']['name']
+];
+```
+
+#### `validateImport(array $data)`
+
+The passed array `$data` equals the data returned by [prepareImport()](#prepareimportarray-data).
+This method has no return value, instead you should throw an exception if the passed data is invalid.
+
+
+#### `findExistingItem(array $data)`
+
+The passed array `$data` equals the data returned by [prepareImport()](#prepareimportarray-data).
+This method is expected to return an array with two keys:
+
+- `sql` contains the SQL query with placeholders.
+- `parameters` contains an array with values used for the SQL query.
+
+#### 2.5.3. Example
+
+```php
+<?php
+$sql = "SELECT *
+ FROM wcf".WCF_N."_".$this->tableName."
+ WHERE packageID = ?
+ AND name = ?
+ AND templateName = ?
+ AND eventName = ?
+ AND environment = ?";
+$parameters = [
+ $this->installation->getPackageID(),
+ $data['name'],
+ $data['templateName'],
+ $data['eventName'],
+ $data['environment']
+];
+
+return [
+ 'sql' => $sql,
+ 'parameters' => $parameters
+];
+```
+
+#### `handleDelete(array $items)`
+
+The passed array `$items` contains the original node data, similar to [prepareImport()](#prepareimportarray-data).
+You should make use of this data to remove the matching element from database.
+
+Example:
+```php
+<?php
+$sql = "DELETE FROM wcf".WCF_N."_".$this->tableName."
+ WHERE packageID = ?
+ AND environment = ?
+ AND eventName = ?
+ AND name = ?
+ AND templateName = ?";
+$statement = WCF::getDB()->prepareStatement($sql);
+foreach ($items as $item) {
+ $statement->execute([
+ $this->installation->getPackageID(),
+ $item['elements']['environment'],
+ $item['elements']['eventname'],
+ $item['attributes']['name'],
+ $item['elements']['templatename']
+ ]);
+}
+```
+
+#### `postImport()`
+
+Allows you to (optionally) run additionally actions after all elements were processed.
+
+
+## `AbstractOptionPackageInstallationPlugin`
+
+[AbstractOptionPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php) is an abstract implementation for options, used for:
+
+- ACL Options
+- Options
+- User Options
+- User Group Options
+
+### Differences to `AbstractXMLPackageInstallationPlugin`
+
+#### `$reservedTags`
+
+`$reservedTags` is a list of reserved tag names so that any tag encountered but not listed here will be added to the database column `additionalData`.
+This allows options to store arbitrary data which can be accessed but were not initially part of the PIP specifications.
--- /dev/null
+---
+title: Sitemaps
+sidebar: sidebar
+permalink: php_api_sitemaps.html
+folder: php/api
+---
+
+{% include callout.html content="This feature is available with WoltLab Suite 3.1 or newer only." type="warning" %}
+
+Since version 3.1, WoltLab Suite Core is capable of automatically creating a sitemap.
+This sitemap contains all static pages registered via the page package installation plugin and which may be indexed by search engines (checking the `allowSpidersToIndex` parameter and page permissions) and do not expect an object ID.
+Other pages have to be added to the sitemap as a separate object.
+
+The only prerequisite for sitemap objects is that the objects are instances of `wcf\data\DatabaseObject` and that there is a `wcf\data\DatabaseObjectList` implementation.
+
+First, we implement the PHP class, which provides us all database objects and optionally checks the permissions for a single object.
+The class must implement the interface `wcf\system\sitemap\object\ISitemapObjectObjectType`.
+However, in order to have some methods already implemented and ensure backwards compatibility, you should use the abstract class `wcf\system\sitemap\object\AbstractSitemapObjectObjectType`.
+The abstract class takes care of generating the `DatabaseObjectList` class name and list directly and implements optional methods with the default values.
+The only method that you have to implement yourself is the `getObjectClass()` method which returns the fully qualified name of the `DatabaseObject` class.
+The `DatabaseObject` class must implement the interface `wcf\data\ILinkableObject`.
+
+Other optional methods are:
+
+* The `getLastModifiedColumn()` method returns the name of the column in the database where the last modification date is stored.
+ If there is none, this method must return `null`.
+* The `canView()` method checks whether the passed `DatabaseObject` is visible to the current user with the current user always being a guest.
+* The `getObjectListClass()` method returns a non-standard `DatabaseObjectList` class name.
+* The `getObjectList()` method returns the `DatabaseObjectList` instance.
+ You can, for example, specify additional query conditions in the method.
+
+As an example, the implementation for users looks like this:
+
+```php
+<?php
+namespace wcf\system\sitemap\object;
+use wcf\data\user\User;
+use wcf\data\DatabaseObject;
+use wcf\system\WCF;
+
+/**
+ * User sitemap implementation.
+ *
+ * @author Joshua Ruesweg
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Sitemap\Object
+ * @since 3.1
+ */
+class UserSitemapObject extends AbstractSitemapObjectObjectType {
+ /**
+ * @inheritDoc
+ */
+ public function getObjectClass() {
+ return User::class;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLastModifiedColumn() {
+ return 'lastActivityTime';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function canView(DatabaseObject $object) {
+ return WCF::getSession()->getPermission('user.profile.canViewUserProfile');
+ }
+}
+```
+
+Next, the sitemap object must be registered as an object type:
+
+```xml
+<type>
+ <name>com.example.plugin.sitemap.object.user</name>
+ <definitionname>com.woltlab.wcf.sitemap.object</definitionname>
+ <classname>wcf\system\sitemap\object\UserSitemapObject</classname>
+ <priority>0.5</priority>
+ <changeFreq>monthly</changeFreq>
+ <rebuildTime>259200</rebuildTime>
+</type>
+```
+
+In addition to the fully qualified class name, the object type definition `com.woltlab.wcf.sitemap.object` and the object type name, the parameters `priority`, `changeFreq` and `rebuildTime` must also be specified.
+`priority` ([https://www.sitemaps.org/protocol.html#prioritydef](https://www.sitemaps.org/protocol.html#prioritydef)) and `changeFreq` ([https://www.sitemaps.org/protocol.html#changefreqdef](https://www.sitemaps.org/protocol.html#changefreqdef)) are specifications in the sitemaps protocol and can be changed by the user in the ACP.
+The `priority` should be `0.5` by default, unless there is an important reason to change it.
+The parameter `rebuildTime` specifies the number of seconds after which the sitemap should be regenerated.
+
+Finally, you have to create the language variable for the sitemap object.
+The language variable follows the pattern `wcf.acp.sitemap.objectType.{objectTypeName}` and is in the category `wcf.acp.sitemap`.
\ No newline at end of file
--- /dev/null
+---
+title: User Activity Points
+sidebar: sidebar
+permalink: php_api_user_activity_points.html
+folder: php/api
+---
+
+Users get activity points whenever they create content to award them for their contribution.
+Activity points are used to determine the rank of a user and can also be used for user conditions, for example for automatic user group assignments.
+
+To integrate activity points into your package, you have to register an object type for the defintion `com.woltlab.wcf.user.activityPointEvent` and specify a default number of points:
+
+```xml
+<type>
+ <name>com.example.foo.activityPointEvent.bar</name>
+ <definitionname>com.woltlab.wcf.user.activityPointEvent</definitionname>
+ <points>10</points>
+</type>
+```
+
+The number of points awarded for this type of activity point event can be changed by the administrator in the admin control panel.
+For this form and the user activity point list shown in the frontend, you have to provide the language item
+
+```
+wcf.user.activityPoint.objectType.com.example.foo.activityPointEvent.bar
+```
+
+that contains the name of the content for which the activity points are awarded.
+
+If a relevant object is created, you have to use `UserActivityPointHandler::fireEvent()` which expects the name of the activity point event object type, the id of the object for which the points are awarded (though the object id is not used at the moment) and the user who gets the points:
+
+```php
+UserActivityPointHandler::getInstance()->fireEvent(
+ 'com.example.foo.activityPointEvent.bar',
+ $bar->barID,
+ $bar->userID
+);
+```
+
+To remove activity points once objects are deleted, you have to use `UserActivityPointHandler::removeEvents()` which also expects the name of the activity point event object type and additionally an array mapping the id of the user whose activity points will be reduced to the number of objects that are removed for the relevant user:
+
+```php
+UserActivityPointHandler::getInstance()->removeEvents(
+ 'com.example.foo.activityPointEvent.bar',
+ [
+ 1 => 1, // remove points for one object for user with id `1`
+ 4 => 2 // remove points for two objects for user with id `4`
+ ]
+);
+```
--- /dev/null
+---
+title: User Notifications
+sidebar: sidebar
+permalink: php_api_user_notifications.html
+folder: php/api
+---
+
+WoltLab Suite includes a powerful user notification system that supports notifications directly shown on the website and emails sent immediately or on a daily basis.
+
+
+## `objectType.xml`
+
+For any type of object related to events, you have to define an object type for the object type definition `com.woltlab.wcf.notification.objectType`:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/objectType.xsd">
+ <import>
+ <type>
+ <name>com.woltlab.example.foo</name>
+ <definitionname>com.woltlab.wcf.notification.objectType</definitionname>
+ <classname>example\system\user\notification\object\type\FooUserNotificationObjectType</classname>
+ <category>com.woltlab.example</category>
+ </type>
+ </import>
+</data>
+```
+
+The referenced class `FooUserNotificationObjectType` has to implement the [IUserNotificationObjectType](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/object/type/IUserNotificationObjectType.class.php) interface, which should be done by extending [AbstractUserNotificationObjectType](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/object/type/AbstractUserNotificationObjectType.class.php).
+
+
+```php
+<?php
+namespace example\system\user\notification\object\type;
+use example\data\foo\Foo;
+use example\data\foo\FooList;
+use example\system\user\notification\object\FooUserNotificationObject;
+use wcf\system\user\notification\object\type\AbstractUserNotificationObjectType;
+
+/**
+ * Represents a foo as a notification object type.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license WoltLab License <http://www.woltlab.com/license-agreement.html>
+ * @package WoltLabSuite\Example\System\User\Notification\Object\Type
+ */
+class FooUserNotificationObjectType extends AbstractUserNotificationObjectType {
+ /**
+ * @inheritDoc
+ */
+ protected static $decoratorClassName = FooUserNotificationObject::class;
+
+ /**
+ * @inheritDoc
+ */
+ protected static $objectClassName = Foo::class;
+
+ /**
+ * @inheritDoc
+ */
+ protected static $objectListClassName = FooList::class;
+}
+```
+
+You have to set the class names of the database object (`$objectClassName`) and the related list (`$objectListClassName`).
+Additionally, you have to create a class that implements the [IUserNotificationObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/object/IUserNotificationObject.class.php) whose name you have to set as the value of the `$decoratorClassName` property.
+
+```php
+<?php
+namespace example\system\user\notification\object;
+use example\data\foo\Foo;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\system\user\notification\object\IUserNotificationObject;
+
+/**
+ * Represents a foo as a notification object.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license WoltLab License <http://www.woltlab.com/license-agreement.html>
+ * @package WoltLabSuite\Example\System\User\Notification\Object
+ *
+ * @method Foo getDecoratedObject()
+ * @mixin Foo
+ */
+class FooUserNotificationObject extends DatabaseObjectDecorator implements IUserNotificationObject {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = Foo::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->getDecoratedObject()->getTitle();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getURL() {
+ return $this->getDecoratedObject()->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAuthorID() {
+ return $this->getDecoratedObject()->userID;
+ }
+}
+```
+
+- The `getTitle()` method returns the title of the object.
+ In this case, we assume that the `Foo` class has implemented the [ITitledObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/ITitledObject.class.php) interface so that the decorated `Foo` can handle this method call itself.
+- The `getURL()` method returns the link to the object.
+ As for the `getTitle()`, we assume that the `Foo` class has implemented the [ILinkableObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/ILinkableObject.class.php) interface so that the decorated `Foo` can also handle this method call itself.
+- The `getAuthorID()` method returns the id of the user who created the decorated `Foo` object.
+ We assume that `Foo` objects have a `userID` property that contains this id.
+
+
+## `userNotificationEvent.xml`
+
+Each event that you fire in your package needs to be registered using the [user notification event package installation plugin](package_pip_user-notification-event.html).
+An example file might look like this:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userNotificationEvent.xsd">
+ <import>
+ <event>
+ <name>bar</name>
+ <objecttype>com.woltlab.example.foo</objecttype>
+ <classname>example\system\user\notification\event\FooUserNotificationEvent</classname>
+ <preset>1</preset>
+ </event>
+ </import>
+</data>
+```
+
+Here, you reference the user notification object type created via `objectType.xml`.
+The referenced class in the `<classname>` element has to implement the [IUserNotificationEvent](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php) interface by extending the [AbstractUserNotificationEvent](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php) class or the [AbstractSharedUserNotificationEvent](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/event/AbstractSharedUserNotificationEvent.class.php) class if you want to pre-load additional data before processing notifications.
+In `AbstractSharedUserNotificationEvent::prepare()`, you can, for example, tell runtime caches to prepare to load certain objects which then are loaded all at once when the objects are needed.
+
+```php
+<?php
+namespace example\system\user\notification\event;
+use example\system\cache\runtime\BazRuntimeCache;
+use example\system\user\notification\object\FooUserNotificationObject;
+use wcf\system\email\Email;
+use wcf\system\request\LinkHandler;
+use wcf\system\user\notification\event\AbstractSharedUserNotificationEvent;
+
+/**
+ * Notification event for foos.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license WoltLab License <http://www.woltlab.com/license-agreement.html>
+ * @package WoltLabSuite\Example\System\User\Notification\Event
+ *
+ * @method FooUserNotificationObject getUserNotificationObject()
+ */
+class FooUserNotificationEvent extends AbstractSharedUserNotificationEvent {
+ /**
+ * @inheritDoc
+ */
+ protected $stackable = true;
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function checkAccess() {
+ $this->getUserNotificationObject()->setBaz(BazRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->bazID));
+
+ if (!$this->getUserNotificationObject()->isAccessible()) {
+ // do some cleanup, if necessary
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function getEmailMessage($notificationType = 'instant') {
+ $this->getUserNotificationObject()->setBaz(BazRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->bazID));
+
+ $messageID = '<com.woltlab.example.baz/'.$this->getUserNotificationObject()->bazID.'@'.Email::getHost().'>';
+
+ return [
+ 'application' => 'example',
+ 'in-reply-to' => [$messageID],
+ 'message-id' => 'com.woltlab.example.foo/'.$this->getUserNotificationObject()->fooID,
+ 'references' => [$messageID],
+ 'template' => 'email_notification_foo'
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ * @since 5.0
+ */
+ public function getEmailTitle() {
+ $this->getUserNotificationObject()->setBaz(BazRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->bazID));
+
+ return $this->getLanguage()->getDynamicVariable('example.foo.notification.mail.title', [
+ 'userNotificationObject' => $this->getUserNotificationObject()
+ ]);
+ }
+
+ /** @noinspection PhpMissingParentCallCommonInspection */
+ /**
+ * @inheritDoc
+ */
+ public function getEventHash() {
+ return sha1($this->eventID . '-' . $this->getUserNotificationObject()->bazID);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ return LinkHandler::getInstance()->getLink('Foo', [
+ 'application' => 'example',
+ 'object' => $this->getUserNotificationObject()->getDecoratedObject()
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMessage() {
+ $authors = $this->getAuthors();
+ $count = count($authors);
+
+ if ($count > 1) {
+ if (isset($authors[0])) {
+ unset($authors[0]);
+ }
+ $count = count($authors);
+
+ return $this->getLanguage()->getDynamicVariable('example.foo.notification.message.stacked', [
+ 'author' => $this->author,
+ 'authors' => array_values($authors),
+ 'count' => $count,
+ 'guestTimesTriggered' => $this->notification->guestTimesTriggered,
+ 'message' => $this->getUserNotificationObject(),
+ 'others' => $count - 1
+ ]);
+ }
+
+ return $this->getLanguage()->getDynamicVariable('example.foo.notification.message', [
+ 'author' => $this->author,
+ 'userNotificationObject' => $this->getUserNotificationObject()
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ $count = count($this->getAuthors());
+ if ($count > 1) {
+ return $this->getLanguage()->getDynamicVariable('example.foo.notification.title.stacked', [
+ 'count' => $count,
+ 'timesTriggered' => $this->notification->timesTriggered
+ ]);
+ }
+
+ return $this->getLanguage()->get('example.foo.notification.title');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function prepare() {
+ BazRuntimeCache::getInstance()->cacheObjectID($this->getUserNotificationObject()->bazID);
+ }
+}
+```
+
+- The `$stackable` property is `false` by default and has to be explicitly set to `true` if stacking of notifications should be enabled.
+ Stacking of notification does not create new notifications for the same event for a certain object if the related action as been triggered by different users.
+ For example, if something is liked by one user and then liked again by another user before the recipient of the notification has confirmed it, the existing notification will be amended to include both users who liked the content.
+ Stacking can thus be used to avoid cluttering the notification list of users.
+- The `checkAccess()` method makes sure that the active user still has access to the object related to the notification.
+ If that is not the case, the user notification system will automatically deleted the user notification based on the return value of the method.
+ If you have any cached values related to notifications, you should also reset these values here.
+- The `getEmailMessage()` method return data to create the instant email or the daily summary email.
+ For instant emails (`$notificationType = 'instant'`), you have to return an array like the one shown in the code above with the following components:
+ - `application`:
+ abbreviation of application
+ - `in-reply-to` (optional):
+ message id of the notification for the parent item and used to improve the ordering in threaded email clients
+ - `message-id` (optional):
+ message id of the notification mail and has to be used in `in-reply-to` and `references` for follow up mails
+ - `references` (optional):
+ all of the message ids of parent items (i.e. recursive in-reply-to)
+ - `template`:
+ name of the template used to render the email body, should start with `email_`
+ - `variables` (optional):
+ template variables passed to the email template where they can be accessed via `$notificationContent[variables]`
+
+ For daily emails (`$notificationType = 'daily'`), only `application`, `template`, and `variables` are supported.
+- The `getEmailTitle()` returns the title of the instant email sent to the user.
+ By default, `getEmailTitle()` simply calls `getTitle()`.
+- The `getEventHash()` method returns a hash by which user notifications are grouped.
+ Here, we want to group them not by the actual `Foo` object but by its parent `Baz` object and thus overwrite the default implementation provided by `AbstractUserNotificationEvent`.
+- The `getLink()` returns the link to the `Foo` object the notification belongs to.
+- The `getMessage()` method and the `getTitle()` return the message and the title of the user notification respectively.
+ By checking the value of `count($this->getAuthors())`, we check if the notification is stacked, thus if the event has been triggered for multiple users so that different languages items are used.
+ If your notification event does not support stacking, this distinction is not necessary.
+- The `prepare()` method is called for each user notification before all user notifications are rendered.
+ This allows to tell runtime caches to prepare to load objects later on (see [Runtime Caches](php_api_caches_runtime-caches.html)).
+
+
+## Firing Events
+
+When the action related to a user notification is executed, you can use `UserNotificationHandler::fireEvent()` to create the notifications:
+
+```php
+$recipientIDs = []; // fill with user ids of the recipients of the notification
+UserNotificationHandler::getInstance()->fireEvent(
+ 'bar', // event name
+ 'com.woltlab.example.foo', // event object type name
+ new FooUserNotificationObject(new Foo($fooID)), // object related to the event
+ $recipientIDs
+);
+```
+
+
+## Marking Notifications as Confirmed
+
+In some instances, you might want to manually mark user notifications as confirmed without the user manually confirming them, for example when they visit the page that is related to the user notification.
+In this case, you can use `UserNotificationHandler::markAsConfirmed()`:
+
+```php
+$recipientIDs = []; // fill with user ids of the recipients of the notification
+$fooIDs = []; // fill with ids of related foo objects
+UserNotificationHandler::getInstance()->markAsConfirmed(
+ 'bar', // event name
+ 'com.woltlab.example.foo', // event object type name
+ $recipientIDs,
+ $fooIDs
+);
+```
--- /dev/null
+---
+title: Apps for WoltLab Suite
+sidebar: sidebar
+permalink: php_apps.html
+folder: php
+---
+
+## Introduction
+
+Apps are among the most powerful components in WoltLab Suite. Unlike plugins
+that extend an existing functionality and pages, apps have their own frontend
+with a dedicated namespace, database table prefixes and template locations.
+
+However, apps are meant to be a logical (and to some extent physical) separation
+from other parts of the framework, including other installed apps. They offer
+an additional layer of isolation and enable you to re-use class and template
+names that are already in use by the Core itself.
+
+If you've come here, thinking about the question if your next package should be
+an app instead of a regular plugin, the result is almost always: _No._
+
+## Differences to Plugins
+
+Apps do offer a couple of unique features that are not available to plugins and
+there are valid reasons to use one instead of a plugin, but they also increase
+both the code and system complexity. There is a performance penalty for each
+installed app, regardless if it is actively used in a request or not, simplying
+being there forces the Core to include it in many places, for example, class
+resolution or even simple tasks such as constructing a link.
+
+### Unique Namespace
+
+Each app has its own unique namespace that is entirely separated from the Core
+and any other installed apps. The namespace is derived from the last part of the
+package identifier, for example, `com.example.foo` will yield the namespace `foo`.
+
+The namespace is always relative to the installation directory of the app, it
+doesn't matter if the app is installed on `example.com/foo/` or in `example.com/bar/`,
+the namespace will always resolve to the right directory.
+
+This app namespace is also used for ACP templates, frontend templates and files:
+
+```xml
+<!-- somewhere in the package.xml -->
+<instructions type="file" application="foo" />
+```
+
+### Unique Database Table Prefix
+
+All database tables make use of a generic prefix that is derived from one of the
+installed apps, including `wcf` which resolves to the Core itself. Following the
+aforementioned example, the new prefix `fooN_` will be automatically registered
+and recognized in any generated statement.
+
+Any `DatabaseObject` that uses the app's namespace is automatically assumed to
+use the app's database prefix. For instance, `foo\data\bar\Bar` is implicitly
+mapped to the database table `fooN_bar`.
+
+The app prefix is recognized in SQL-PIPs and statements that reference one of
+its database tables are automatically rewritten to use the Core's instance number.
+
+### Separate Domain and Path Configuration
+
+Any controller that is provided by a plugin is served from the configured domain
+and path of the corresponding app, such as plugins for the Core are always
+served from the Core's directory. Apps are different and use their own domain
+and/or path to present their content, additionally, this allows the app to re-use
+a controller name that is already provided by the Core or any other app itself.
+
+## Creating an App
+
+{% include callout.html content="This is a non-reversible operation! Once a package has been installed, its type cannot be changed without uninstalling and reinstalling the entire package, an app will always be an app and vice versa." type="danger" %}
+
+### `package.xml`
+
+The `package.xml` supports two additional elements in the `<packageinformation>`
+block that are unique to applications.
+
+#### `<isapplication>1</isapplication>`
+
+This element is responsible to flag a package as an app.
+
+#### `<applicationdirectory>example</applicationdirectory>`
+
+Sets the suggested name of the application directory when installing it, the
+path result in `<path-to-the-core>/example/`. If you leave this element out,
+the app identifier (`com.example.foo -> foo`) will be used instead.
+
+### Minimum Required Files
+
+An example project with the [source code can be found on GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/basic-app/),
+it includes everything that is required for a basic app.
+
+{% include links.html %}
--- /dev/null
+---
+title: Code Style
+sidebar: sidebar
+permalink: php_code-style.html
+folder: php
+---
+
+{% include callout.html content="The following code style conventions are used by us for our own packages. While you do not have to follow every rule, you are encouraged to do so." type="info" %}
+
+For information about how to document your code, please refer to the [documentation page](php_code-style_documentation.html).
+
+
+## General Code Style
+
+### Naming conventions
+
+The relevant naming conventions are:
+
+- **Upper camel case**:
+ The first letters of all compound words are written in upper case.
+- **Lower camel case**:
+ The first letters of compound words are written in upper case, except for the first letter which is written in lower case.
+- **Screaming snake case**:
+ All letters are written in upper case and compound words are separated by underscores.
+
+
+| Type | Convention | Example |
+|------|------------|---------|
+| Variable | lower camel case | `$variableName` |
+| Class | upper camel case | `class UserGroupEditor` |
+| Properties | lower camel case | `public $propertyName` |
+| Method | lower camel case | `public function getObjectByName()` |
+| Constant | screaming snake case | `MODULE_USER_THING` |
+
+### Arrays
+
+For arrays, use the short array syntax introduced with PHP 5.4.
+The following example illustrates the different cases that can occur when working with arrays and how to format them:
+
+```php
+<?php
+
+$empty = [];
+
+$oneElement = [1];
+$multipleElements = [1, 2, 3];
+
+$oneElementWithKey = ['firstElement' => 1];
+$multipleElementsWithKey = [
+ 'firstElement' => 1,
+ 'secondElement' => 2,
+ 'thirdElement' => 3
+];
+```
+
+### Ternary Operator
+
+The ternary operator can be used for short conditioned assignments:
+
+```php
+<?php
+
+$name = isset($tagArgs['name']) ? $tagArgs['name'] : 'default';
+```
+
+The condition and the values should be short so that the code does not result in a very long line which thus decreases the readability compared to an `if-else` statement.
+
+Parentheses may only be used around the condition and not around the whole statement:
+
+```php
+<?php
+
+// do not do it like this
+$name = (isset($tagArgs['name']) ? $tagArgs['name'] : 'default');
+```
+
+Parentheses around the conditions may not be used to wrap simple function calls:
+
+```php
+<?php
+
+// do not do it like this
+$name = (isset($tagArgs['name'])) ? $tagArgs['name'] : 'default';
+```
+
+but have to be used for comparisons or other binary operators:
+
+```php
+<?php
+
+$value = ($otherValue > $upperLimit) ? $additionalValue : $otherValue;
+```
+
+If you need to use more than one binary operator, use an `if-else` statement.
+
+The same rules apply to assigning array values:
+
+```php
+<?php
+
+$values = [
+ 'first' => $firstValue,
+ 'second' => $secondToggle ? $secondValueA : $secondValueB,
+ 'third' => ($thirdToogle > 13) ? $thirdToogleA : $thirdToogleB
+];
+```
+
+or return values:
+
+```php
+<?php
+
+return isset($tagArgs['name']) ? $tagArgs['name'] : 'default';
+```
+
+### Whitespaces
+
+You have to put a whitespace *in front* of the following things:
+
+- equal sign in assignments: `$x = 1;`
+- comparison operators: `$x == 1`
+- opening bracket of a block `public function test() {`
+
+You have to put a whitespace *behind* the following things:
+
+- equal sign in assignments: `$x = 1;`
+- comparison operators: `$x == 1`
+- comma in a function/method parameter list if the comma is not followed by a line break: `public function test($a, $b) {`
+- `if`, `for`, `foreach`, `while`: `if ($x == 1)`
+
+
+## Classes
+
+### Referencing Class Names
+
+If you have to reference a class name inside a php file, you have to use the `class` keyword.
+
+```php
+<?php
+
+// not like this
+$className = 'wcf\data\example\Example';
+
+// like this
+use wcf\data\example\Example;
+$className = Example::class;
+```
+
+### Static Getters (of `DatabaseObject` Classes)
+
+Some database objects provide static getters, either if they are decorators or for a unique combination of database table columns, like `wcf\data\box\Box::getBoxByIdentifier()`:
+
+```php
+<?php
+namespace wcf\data\box;
+use wcf\data\DatabaseObject;
+use wcf\system\WCF;
+
+class Box extends DatabaseObject {
+ /**
+ * Returns the box with the given identifier.
+ *
+ * @param string $identifier
+ * @return Box|null
+ */
+ public static function getBoxByIdentifier($identifier) {
+ $sql = "SELECT *
+ FROM wcf".WCF_N."_box
+ WHERE identifier = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$identifier]);
+
+ return $statement->fetchObject(self::class);
+ }
+}
+```
+
+Such methods should always either return the desired object or `null` if the object does not exist.
+`wcf\system\database\statement\PreparedStatement::fetchObject()` already takes care of this distinction so that its return value can simply be returned by such methods.
+
+The name of such getters should generally follow the convention `get{object type}By{column or other description}`.
+
+### Long method calls
+
+In some instances, methods with many argument have to be called which can result in lines of code like this one:
+
+```php
+<?php
+
+\wcf\system\search\SearchIndexManager::getInstance()->set('com.woltlab.wcf.article', $articleContent->articleContentID, $articleContent->content, $articleContent->title, $articles[$articleContent->articleID]->time, $articles[$articleContent->articleID]->userID, $articles[$articleContent->articleID]->username, $articleContent->languageID, $articleContent->teaser);
+```
+
+which is hardly readable.
+Therefore, the line must be split into multiple lines with each argument in a separate line:
+
+```php
+<?php
+
+\wcf\system\search\SearchIndexManager::getInstance()->set(
+ 'com.woltlab.wcf.article',
+ $articleContent->articleContentID,
+ $articleContent->content,
+ $articleContent->title,
+ $articles[$articleContent->articleID]->time,
+ $articles[$articleContent->articleID]->userID,
+ $articles[$articleContent->articleID]->username,
+ $articleContent->languageID,
+ $articleContent->teaser
+);
+```
+
+In general, this rule applies to the following methods:
+
+- `wcf\system\edit\EditHistoryManager::add()`
+- `wcf\system\message\quote\MessageQuoteManager::addQuote()`
+- `wcf\system\message\quote\MessageQuoteManager::getQuoteID()`
+- `wcf\system\search\SearchIndexManager::set()`
+- `wcf\system\user\object\watch\UserObjectWatchHandler::updateObject()`
+- `wcf\system\user\notification\UserNotificationHandler::fireEvent()`
--- /dev/null
+---
+title: Documentation
+sidebar: sidebar
+permalink: php_code-style_documentation.html
+folder: php
+parent: php_code-style
+---
+
+{% include callout.html content="The following documentation conventions are used by us for our own packages. While you do not have to follow every rule, you are encouraged to do so." type="info" %}
+
+
+## Database Objects
+
+### Database Table Columns as Properties
+
+As the database table columns are not explicit properties of the classes extending `wcf\data\DatabaseObject` but rather stored in `DatabaseObject::$data` and accessible via `DatabaseObject::__get($name)`, the IDE we use, PhpStorm, is neither able to autocomplete such property access nor to interfere the type of the property.
+
+To solve this problem, `@property-read` tags must be added to the class documentation which registers the database table columns as public read-only properties:
+
+```
+ * @property-read propertyType $propertyName property description
+```
+
+The properties have to be in the same order as the order in the database table.
+
+The following table provides templates for common description texts so that similar database table columns have similar description texts.
+
+| property | description template and example |
+|----------|----------------------------------|
+| unique object id | `unique id of the {object name}`<br>**example:** `unique id of the acl option`|
+| id of the delivering package | `id of the package which delivers the {object name}`<br>**example:** `id of the package which delivers the acl option`|
+| show order for nested structure | `position of the {object name} in relation to its siblings`<br>**example:** `position of the ACP menu item in relation to its siblings`|
+| show order within different object | `position of the {object name} in relation to the other {object name}s in the {parent object name}`<br>**example:** `position of the label in relation to the other labels in the label group`|
+| required permissions | `comma separated list of user group permissions of which the active user needs to have at least one to see (access, …) the {object name}`<br>**example:**`comma separated list of user group permissions of which the active user needs to have at least one to see the ACP menu item`|
+| required options | `comma separated list of options of which at least one needs to be enabled for the {object name} to be shown (accessible, …)`<br>**example:**`comma separated list of options of which at least one needs to be enabled for the ACP menu item to be shown`|
+| id of the user who has created the object | ``id of the user who created (wrote, …) the {object name} (or `null` if the user does not exist anymore (or if the {object name} has been created by a guest))``<br>**example:**``id of the user who wrote the comment or `null` if the user does not exist anymore or if the comment has been written by a guest``|
+| name of the user who has created the object | ``name of the user (or guest) who created (wrote, …) the {object name}``<br>**example:**``name of the user or guest who wrote the comment``|
+| additional data | `array with additional data of the {object name}`<br>**example:**`array with additional data of the user activity event`|
+| time-related columns | `timestamp at which the {object name} has been created (written, …)`<br>**example:**`timestamp at which the comment has been written`|
+| boolean options | ``is `1` (or `0`) if the {object name} … (and thus …), otherwise `0` (or `1`)``<br>**example:**``is `1` if the ad is disabled and thus not shown, otherwise `0` ``|
+| `$cumulativeLikes` | ``cumulative result of likes (counting `+1`) and dislikes (counting `-1`) for the {object name}``<br>**example:**``cumulative result of likes (counting `+1`) and dislikes (counting `-1`) for the article``|
+| `$comments` | `number of comments on the {object name}`<br>**example:**`number of comments on the article`|
+| `$views` | `number of times the {object name} has been viewed`<br>**example:**`number of times the article has been viewed`|
+| text field with potential language item name as value | `{text type} of the {object name} or name of language item which contains the {text type}`<br>**example:**`description of the cronjob or name of language item which contains the description`|
+| `$objectTypeID` | ``id of the `{object type definition name}` object type``<br>**example:**``id of the `com.woltlab.wcf.modifiableContent` object type``|
+
+
+## Database Object Editors
+
+### Class Tags
+
+Any database object editor class comment must have to following tags to properly support autocompletion by IDEs:
+
+```php
+/**
+ * …
+ * @method static {DBO class name} create(array $parameters = [])
+ * @method {DBO class name} getDecoratedObject()
+ * @mixin {DBO class name}
+ */
+```
+
+The only exception to this rule is if the class overwrites the `create()` method which itself has to be properly documentation then.
+
+The first and second line makes sure that when calling the `create()` or `getDecoratedObject()` method, the return value is correctly recognized and not just a general `DatabaseObject` instance.
+The third line tells the IDE (if `@mixin` is supported) that the database object editor decorates the database object and therefore offers autocompletion for properties and methods from the database object class itself.
+
+
+## Runtime Caches
+
+### Class Tags
+
+Any class implementing the [IRuntimeCache](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/runtime/IRuntimeCache.class.php) interface must have the following class tags:
+
+```php
+/**
+ * …
+ * @method {DBO class name}[] getCachedObjects()
+ * @method {DBO class name} getObject($objectID)
+ * @method {DBO class name}[] getObjects(array $objectIDs)
+ */
+```
+
+These tags ensure that when calling any of the mentioned methods, the return value refers to the concrete database object and not just generically to [DatabaseObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/DatabaseObject.class.php).
--- /dev/null
+---
+title: Database Access
+sidebar: sidebar
+permalink: php_database-access.html
+folder: php
+---
+
+[Database Objects][php_database-objects] provide a convenient and object-oriented approach to work with the database, but there can be use-cases that require raw access including writing methods for model classes. This section assumes that you have either used [prepared statements](https://en.wikipedia.org/wiki/Prepared_statement) before or at least understand how it works.
+
+## The PreparedStatement Object
+
+The database access is designed around [PreparedStatement](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/database/statement/PreparedStatement.class.php), built on top of PHP's `PDOStatement` so that you call all of `PDOStatement`'s methods, and each query requires you to obtain a statement object.
+
+```php
+<?php
+$statement = \wcf\system\WCF::getDB()->prepareStatement("SELECT * FROM wcf".WCF_N."_example");
+$statement->execute();
+while ($row = $statement->fetchArray()) {
+ // handle result
+}
+```
+
+### Query Parameters
+
+The example below illustrates the usage of parameters where each value is replaced with the generic `?`-placeholder. Values are provided by calling `$statement->execute()` with a continuous, one-dimensional array that exactly match the number of question marks.
+
+```php
+<?php
+$sql = "SELECT *
+ FROM wcf".WCF_N."_example
+ WHERE exampleID = ?
+ OR bar IN (?, ?, ?, ?, ?)";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+$statement->execute([
+ $exampleID,
+ $list, $of, $values, $for, $bar
+]);
+while ($row = $statement->fetchArray()) {
+ // handle result
+}
+```
+
+### Fetching a Single Result
+
+{% include callout.html content="Do not attempt to use `fetchSingleRow()` or `fetchSingleColumn()` if the result contains more than one row." type="danger" %}
+
+You can opt-in to retrieve only a single row from database and make use of shortcut methods to reduce the code that you have to write.
+
+```php
+<?php
+$sql = "SELECT *
+ FROM wcf".WCF_N."_example
+ WHERE exampleID = ?";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql, 1);
+$statement->execute([$exampleID]);
+$row = $statement->fetchSingleRow();
+```
+
+There are two distinct differences when comparing with the example on query parameters above:
+
+1. The method `prepareStatement()` receives a secondary parameter that will be appended to the query as `LIMIT 1`.
+2. Data is read using `fetchSingleRow()` instead of `fetchArray()` or similar methods, that will read one result and close the cursor.
+
+### Fetch by Column
+
+{% include callout.html content="There is no way to return another column from the same row if you use `fetchColumn()` to retrieve data." type="warning" %}
+
+Fetching an array is only useful if there is going to be more than one column per result row, otherwise accessing the column directly is much more convenient and increases the code readability.
+
+```php
+<?php
+$sql = "SELECT bar
+ FROM wcf".WCF_N."_example";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+$statement->execute();
+while ($bar = $statement->fetchColumn()) {
+ // handle result
+}
+$bar = $statement->fetchSingleColumn();
+```
+
+Similar to fetching a single row, you can also issue a query that will select a single row, but reads only one column from the result row.
+
+```php
+<?php
+$sql = "SELECT bar
+ FROM wcf".WCF_N."_example
+ WHERE exampleID = ?";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql, 1);
+$statement->execute([$exampleID]);
+$bar = $statement->fetchSingleColumn();
+```
+
+### Fetching All Results
+
+If you want to fetch all results of a query but only store them in an array without directly processing them, in most cases, you can rely on built-in methods.
+
+To fetch all rows of query, you can use `PDOStatement::fetchAll()` with `\PDO::FETCH_ASSOC` as the first parameter:
+
+```php
+<?php
+$sql = "SELECT *
+ FROM wcf".WCF_N."_example";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+$statement->execute();
+$rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
+```
+
+As a result, you get an array containing associative arrays with the rows of the `wcf{WCF_N}_example` database table as content.
+
+If you only want to fetch a list of the values of a certain column, you can use `\PDO::FETCH_COLUMN` as the first parameter:
+
+```php
+<?php
+$sql = "SELECT exampleID
+ FROM wcf".WCF_N."_example";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+$statement->execute();
+$exampleIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
+```
+
+As a result, you get an array with all `exampleID` values.
+
+The `PreparedStatement` class adds an additional methods that covers another common use case in our code:
+Fetching two columns and using the first column's value as the array key and the second column's value as the array value.
+This case is covered by `PreparedStatement::fetchMap()`:
+
+```php
+<?php
+$sql = "SELECT exampleID, userID
+ FROM wcf".WCF_N."_example_mapping";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+$statement->execute();
+$map = $statement->fetchMap('exampleID', 'userID');
+```
+
+`$map` is a one-dimensional array where each `exampleID` value maps to the corresponding `userID` value.
+
+{% include callout.html content="If there are multiple entries for a certain `exampleID` value with different `userID` values, the existing entry in the array will be overwritten and contain the last read value from the database table. Therefore, this method should generally only be used for unique combinations." type="warning" %}
+
+If you do not have a combination of columns with unique pairs of values, but you want to get a list of `userID` values with the same `exampleID`, you can set the third parameter of `fetchMap()` to `false` and get a list:
+
+```php
+<?php
+$sql = "SELECT exampleID, userID
+ FROM wcf".WCF_N."_example_mapping";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+$statement->execute();
+$map = $statement->fetchMap('exampleID', 'userID', false);
+```
+
+Now, as a result, you get a two-dimensional array with the array keys being the `exampleID` values and the array values being arrays with all `userID` values from rows with the respective `exampleID` value.
+
+
+
+## Building Complex Conditions
+
+Building conditional conditions can turn out to be a real mess and it gets even worse with SQL's `IN (…)` which requires as many placeholders as there will be values. The solutions is `PreparedStatementConditionBuilder`, a simple but useful helper class with a bulky name, it is also the class used when accessing `DatabaseObjecList::getConditionBuilder()`.
+
+```php
+<?php
+$conditions = new \wcf\system\database\util\PreparedStatementConditionBuilder();
+$conditions->add("exampleID = ?", [$exampleID]);
+if (!empty($valuesForBar)) {
+ $conditions->add("(bar IN (?) OR baz = ?)", [$valuesForBar, $baz]);
+}
+```
+
+The `IN (?)` in the example above is automatically expanded to match the number of items contained in `$valuesForBar`. Be aware that the method will generate an invalid query if `$valuesForBar` is empty!
+
+## INSERT or UPDATE in Bulk
+
+Prepared statements not only protect against SQL injection by separating the logical query and the actual data, but also provides the ability to reuse the same query with different values. This leads to a performance improvement as the code does not have to transmit the query with for every data set and only has to parse and analyze the query once.
+
+```php
+<?php
+$data = ['abc', 'def', 'ghi'];
+
+$sql = "INSERT INTO wcf".WCF_N."_example
+ (bar)
+ VALUES (?)";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+
+\wcf\system\WCF::getDB()->beginTransaction();
+foreach ($data as $bar) {
+ $statement->execute([$bar]);
+}
+\wcf\system\WCF::getDB()->commitTransaction();
+```
+
+It is generally advised to wrap bulk operations in a transaction as it allows the database to optimize the process, including fewer I/O operations.
+
+```php
+<?php
+$data = [
+ 1 => 'abc',
+ 3 => 'def',
+ 4 => 'ghi'
+];
+
+$sql = "UPDATE wcf".WCF_N."_example
+ SET bar = ?
+ WHERE exampleID = ?";
+$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
+
+\wcf\system\WCF::getDB()->beginTransaction();
+foreach ($data as $exampleID => $bar) {
+ $statement->execute([
+ $bar,
+ $exampleID
+ ]);
+}
+\wcf\system\WCF::getDB()->commitTransaction();
+```
+
+{% include links.html %}
--- /dev/null
+---
+title: Database Objects
+sidebar: sidebar
+permalink: php_database-objects.html
+folder: php
+---
+
+WoltLab Suite uses a unified interface to work with database rows using an object based approach instead of using native arrays holding arbitrary data. Each database table is mapped to a model class that is designed to hold a single record from that table and expose methods to work with the stored data, for example providing assistance when working with normalized datasets.
+
+Developers are required to provide the proper DatabaseObject implementations themselves, they're not automatically generated, all though the actual code that needs to be written is rather small. The following examples assume the fictional database table `wcf1_example`, `exampleID` as the auto-incrementing primary key and the column `bar` to store some text.
+
+
+## DatabaseObject
+
+The basic model derives from `wcf\data\DatabaseObject` and provides a convenient constructor to fetch a single row or construct an instance using pre-loaded rows.
+
+```php
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObject;
+
+class Example extends DatabaseObject {}
+```
+
+The class is intended to be empty by default and there only needs to be code if you want to add additional logic to your model. Both the class name and primary key are determined by `DatabaseObject` using the namespace and class name of the derived class. The example above uses the namespace `wcf\…` which is used as table prefix and the class name `Example` is converted into `exampleID`, resulting in the database table name `wcfN_example` with the primary key `exampleID`.
+
+You can prevent this automatic guessing by setting the class properties `$databaseTableName` and `$databaseTableIndexName` manually.
+
+
+## DatabaseObjectDecorator
+
+If you already have a `DatabaseObject` class and would like to extend it with additional data or methods, for example by providing a class `ViewableExample` which features view-related changes without polluting the original object, you can use `DatabaseObjectDecorator` which a default implementation of a decorator for database objects.
+
+```php
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObjectDecorator;
+
+class ViewableExample extends DatabaseObjectDecorator {
+ protected static $baseClass = Example::class;
+
+ public function getOutput() {
+ $output = '';
+
+ // [determine output]
+
+ return $output;
+ }
+}
+```
+
+It is mandatory to set the static `$baseClass` property to the name of the decorated class.
+
+Like for any decorator, you can directly access the decorated object's properties and methods for a decorated object by accessing the property or calling the method on the decorated object.
+You can access the decorated objects directly via `DatabaseObjectDecorator::getDecoratedObject()`.
+
+
+## DatabaseObjectEditor
+
+{% include callout.html content="This is the low-level interface to manipulate data rows, it is recommended to use `AbstractDatabaseObjectAction`." type="info" %}
+
+Adding, editing and deleting models is done using the `DatabaseObjectEditor` class that decorates a `DatabaseObject` and uses its data to perform the actions.
+
+```php
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObjectEditor;
+
+class ExampleEditor extends DatabaseObjectEditor {
+ protected static $baseClass = Example::class;
+}
+```
+
+The editor class requires you to provide the fully qualified name of the model, that is the class name including the complete namespace. Database table name and index key will be pulled directly from the model.
+
+### Create a new row
+
+Inserting a new row into the database table is provided through `DatabaseObjectEditor::create()` which yields a `DatabaseObject` instance after creation.
+
+```php
+<?php
+$example = \wcf\data\example\ExampleEditor::create([
+ 'bar' => 'Hello World!'
+]);
+
+// output: Hello World!
+echo $example->bar;
+```
+
+### Updating an existing row
+
+{% include callout.html content="The internal state of the decorated `DatabaseObject` is not altered at any point, the values will still be the same after editing or deleting the represented row. If you need an object with the latest data, you'll have to discard the current object and refetch the data from database." type="warning" %}
+
+```php
+<?php
+$example = new \wcf\data\example\Example($id);
+$exampleEditor = new \wcf\data\example\ExampleEditor($example);
+$exampleEditor->update([
+ 'bar' => 'baz'
+]);
+
+// output: Hello World!
+echo $example->bar;
+
+// re-creating the object will query the database again and retrieve the updated value
+$example = new \wcf\data\example\Example($example->id);
+
+// output: baz
+echo $example->bar;
+```
+
+### Deleting a row
+
+{% include callout.html content="Similar to the update process, the decorated `DatabaseObject` is not altered and will then point to an inexistent row." type="warning" %}
+
+```php
+<?php
+$example = new \wcf\data\example\Example($id);
+$exampleEditor = new \wcf\data\example\ExampleEditor($example);
+$exampleEditor->delete();
+```
+
+
+## DatabaseObjectList
+
+Every row is represented as a single instance of the model, but the instance creation deals with single rows only. Retrieving larger sets of rows would be quite inefficient due to the large amount of queries that will be dispatched. This is solved with the `DatabaseObjectList` object that exposes an interface to query the database table using arbitrary conditions for data selection. All rows will be fetched using a single query and the resulting rows are automatically loaded into separate models.
+
+```php
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObjectList;
+
+class ExampleList extends DatabaseObjectList {
+ public $className = Example::class;
+}
+```
+
+The following code listing illustrates loading a large set of examples and iterating over the list to retrieve the objects.
+
+```php
+<?php
+$exampleList = new \wcf\data\example\ExampleList();
+// add constraints using the condition builder
+$exampleList->getConditionBuilder()->add('bar IN (?)', [['Hello World!', 'bar', 'baz']]);
+// actually read the rows
+$exampleList->readObjects();
+foreach ($exampleList as $example) {
+ echo $example->bar;
+}
+
+// retrieve the models directly instead of iterating over them
+$examples = $exampleList->getObjects();
+
+// just retrieve the number of rows
+$exampleCount = $exampleList->countObjects();
+```
+
+`DatabaseObjectList` implements both [SeekableIterator](https://secure.php.net/manual/en/class.seekableiterator.php) and [Countable](https://secure.php.net/manual/en/class.countable.php).
+
+Additionally, `DatabaseObjectList` objects has the following three public properties that are useful when fetching data with lists:
+
+- `$sqlLimit` determines how many rows are fetched.
+ If its value is `0` (which is the default value), all results are fetched.
+ So be careful when dealing with large tables and you only want a limited number of rows:
+ Set `$sqlLimit` to a value larger than zero!
+- `$sqlOffset`:
+ Paginated pages like a thread list use this feature a lot, it allows you to skip a given number of results.
+ Imagine you want to display 20 threads per page but there are a total of 60 threads available.
+ In this case you would specify `$sqlLimit = 20` and `$sqlOffset = 20` which will skip the first 20 threads, effectively displaying thread 21 to 40.
+- `$sqlOrderBy` determines by which column(s) the rows are sorted in which order.
+ Using our example in `$sqlOffset` you might want to display the 20 most recent threads on page 1, thus you should specify the order field and its direction, e.g. `$sqlOrderBy = 'thread.lastPostTime DESC'` which returns the most recent thread first.
+
+For more advanced usage, there two additional fields that deal with the type of objects returned.
+First, let's go into a bit more detail what setting the `$className` property actually does:
+
+1. It is the type of database object in which the rows are wrapped.
+2. It determines which database table is actually queried and which index is used (see the `$databaseTableName` and `$databaseTableIndexName` properties of `DatabaseObject`).
+
+Sometimes you might use the database table of some database object but wrap the rows in another database object.
+This can be achieved by setting the `$objectClassName` property to the desired class name.
+
+In other cases, you might want to wrap the created objects in a database object decorator which can be done by setting the `$decoratorClassName` property to the desired class name:
+
+```php
+<?php
+$exampleList = new \wcf\data\example\ExampleList();
+$exampleList->decoratorClassName = \wcf\data\example\ViewableExample::class;
+```
+
+Of course, you do not have to set the property after creating the list object, you can also set it by creating a dedicated class:
+
+```php
+<?php
+namespace wcf\data\example;
+
+class ViewableExampleList extends ExampleList {
+ public $decoratorClassName = ViewableExample::class;
+}
+```
+
+
+## AbstractDatabaseObjectAction
+
+Row creation and manipulation can be performed using the aforementioned `DatabaseObjectEditor` class, but this approach has two major issues:
+
+1. Row creation, update and deletion takes place silently without notifying any other components.
+2. Data is passed to the database adapter without any further processing.
+
+The `AbstractDatabaseObjectAction` solves both problems by wrapping around the editor class and thus provide an additional layer between the action that should be taken and the actual process. The first problem is solved by a fixed set of events being fired, the second issue is addressed by having a single entry point for all data editing.
+
+```php
+<?php
+namespace wcf\data\example;
+use wcf\data\AbstractDatabaseObjectAction;
+
+class ExampleAction extends AbstractDatabaseObjectAction {
+ public $className = ExampleEditor::class;
+}
+```
+
+### Executing an Action
+
+{% include callout.html content="The method `AbstractDatabaseObjectAction::validateAction()` is internally used for AJAX method invocation and must not be called programmatically." type="warning" %}
+
+The next example represents the same functionality as seen for `DatabaseObjectEditor`:
+
+```php
+<?php
+use wcf\data\example\ExampleAction;
+
+// create a row
+$exampleAction = new ExampleAction([], 'create', [
+ 'data' => ['bar' => 'Hello World']
+]);
+$example = $exampleAction->executeAction()['returnValues'];
+
+// update a row using the id
+$exampleAction = new ExampleAction([1], 'update', [
+ 'data' => ['bar' => 'baz']
+]);
+$exampleAction->executeAction();
+
+// delete a row using a model
+$exampleAction = new ExampleAction([$example], 'delete');
+$exampleAction->executeAction();
+```
+
+You can access the return values both by storing the return value of `executeAction()` or by retrieving it via `getReturnValues()`.
+
+<span class="label label-info">Events</span> `initializeAction`, `validateAction` and `finalizeAction`
+
+### Custom Method with AJAX Support
+
+This section is about adding the method `baz()` to `ExampleAction` and calling it via AJAX.
+
+#### AJAX Validation
+
+Methods of an action cannot be called via AJAX, unless they have a validation method. This means that `ExampleAction` must define both a `public function baz()` and `public function validateBaz()`, the name for the validation method is constructed by upper-casing the first character of the method name and prepending `validate`.
+
+The lack of the companion `validate*` method will cause the AJAX proxy to deny the request instantaneously. Do not add a validation method if you don't want it to be callable via AJAX ever!
+
+#### create, update and delete
+
+The methods `create`, `update` and `delete` are available for all classes deriving from `AbstractDatabaseObjectAction` and directly pass the input data to the `DatabaseObjectEditor`. These methods deny access to them via AJAX by default, unless you explicitly enable access. Depending on your case, there are two different strategies to enable AJAX access to them.
+
+```
+<?php
+namespace wcf\data\example;
+use wcf\data\AbstractDatabaseObjectAction;
+
+class ExampleAction extends AbstractDatabaseObjectAction {
+ // `create()` can now be called via AJAX if the requesting user posses the listed permissions
+ protected $permissionsCreate = ['admin.example.canManageExample'];
+
+ public function validateUpdate() {
+ // your very own validation logic that does not make use of the
+ // built-in `$permissionsUpdate` property
+
+ // you can still invoke the built-in permissions check if you like to
+ parent::validateUpdate();
+ }
+}
+```
+
+#### Allow Invokation by Guests
+
+Invoking methods is restricted to logged-in users by default and the only way to override this behavior is to alter the property `$allowGuestAccess`. It is a simple string array that is expected to hold all methods that should be accessible by users, excluding their companion validation methods.
+
+#### ACP Access Only
+
+Method access is usually limited by permissions, but sometimes there might be the need for some added security to avoid mistakes. The `$requireACP` property works similar to `$allowGuestAccess`, but enforces the request to originate from the ACP together with a valid ACP session, ensuring that only users able to access the ACP can actually invoke these methods.
--- /dev/null
+---
+title: Exceptions
+sidebar: sidebar
+permalink: php_exceptions.html
+folder: php
+---
+
+## SPL Exceptions
+
+The [Standard PHP Library (SPL)](https://secure.php.net/manual/en/book.spl.php) provides some [exceptions](https://secure.php.net/manual/en/spl.exceptions.php) that should be used whenever possible.
+
+
+## Custom Exceptions
+
+{% include callout.html content="Do not use `wcf\system\exception\SystemException` anymore, use specific exception classes!" type="warning" %}
+
+The following table contains a list of custom exceptions that are commonly used.
+
+| exception | (examples) when to use |
+|-----------|------------------------|
+| `wcf\system\exception\IllegalLinkException` | access to a page that belongs to a non-existing object, executing actions on specific non-existing objects (is shown as http 404 error to the user) |
+| `wcf\system\exception\ImplementationException` | a class does not implement an expected interface |
+| `wcf\system\exception\InvalidObjectTypeException` | object type is not of an expected object type definition |
+| `wcf\system\exception\InvalidSecurityTokenException` | given security token does not match the security token of the active user's session |
+| `wcf\system\exception\ParentClassException` | a class does not extend an expected (parent) class |
+| `wcf\system\exception\PermissionDeniedException` | page access without permission, action execution without permission (is shown as http 403 error to the user) |
+| `wcf\system\exception\UserInputException` | user input does not pass validation |
--- /dev/null
+---
+title: General Data Protection Regulation (GDPR)
+sidebar: sidebar
+permalink: php_gdpr.html
+folder: php
+---
+
+## Introduction
+
+The General Data Protection Regulation (GDPR) of the European Union enters into
+force on May 25, 2018. It comes with a set of restrictions when handling users'
+personal data as well as to provide an interface to export this data on demand.
+
+If you're looking for a guide on the implications of the GDPR and what you will
+need or consider to do, please read the article [Implementation of the GDPR](https://www.woltlab.com/article/106-implementation-of-the-gdpr/)
+on woltlab.com.
+
+## Including Data in the Export
+
+The `wcf\acp\action\UserExportGdprAction` introduced with WoltLab Suite 3.1.3
+already includes the Core itself as well as all official apps, but you'll need to
+include any personal data stored for your plugin or app by yourself.
+
+The event `export` is fired before any data is sent out, but after any Core data
+has been dumped to the `$data` property.
+
+### Example code
+
+```php
+<?php
+namespace wcf\system\event\listener;
+use wcf\acp\action\UserExportGdprAction;
+use wcf\data\user\UserProfile;
+
+class MyUserExportGdprActionListener implements IParameterizedEventListener {
+ public function execute(/** @var UserExportGdprAction $eventObj */$eventObj, $className, $eventName, array &$parameters) {
+ /** @var UserProfile $user */
+ $user = $eventObj->user;
+
+ $eventObj->data['my.fancy.plugin'] = [
+ 'superPersonalData' => "This text is super personal and should be included in the output",
+ 'weirdIpAddresses' => $eventObj->exportIpAddresses('app'.WCF_N.'_non_standard_column_names_for_ip_addresses', 'ipAddressColumnName', 'timeColumnName', 'userIDColumnName')
+ ];
+ $eventObj->exportUserProperties[] = 'shouldAlwaysExportThisField';
+ $eventObj->exportUserPropertiesIfNotEmpty[] = 'myFancyField';
+ $eventObj->exportUserOptionSettings[] = 'thisSettingIsAlwaysExported';
+ $eventObj->exportUserOptionSettingsIfNotEmpty[] = 'someSettingContainingPersonalData';
+ $eventObj->ipAddresses['my.fancy.plugin'] = ['wcf'.WCF_N.'_my_fancy_table', 'wcf'.WCF_N.'_i_also_store_ipaddresses_here'];
+ $eventObj->skipUserOptions[] = 'thisLooksLikePersonalDataButItIsNot';
+ $eventObj->skipUserOptions[] = 'thisIsAlsoNotPersonalDataPleaseIgnoreIt';
+ }
+}
+```
+
+### `$data`
+
+Contains the entire data that will be included in the exported JSON file, some
+fields may already exist (such as `'com.woltlab.wcf'`) and while you may add or
+edit any fields within, you should restrict yourself to only append data from
+your plugin or app.
+
+### `$exportUserProperties`
+
+Only a whitelist of columns in `wcfN_user` is exported by default, if your plugin
+or app adds one or more columns to this table that do hold personal data, then
+you will have to append it to this array. The listed properties will always be
+included regardless of their content.
+
+### `$exportUserPropertiesIfNotEmpty`
+
+Only a whitelist of columns in `wcfN_user` is exported by default, if your plugin
+or app adds one or more columns to this table that do hold personal data, then
+you will have to append it to this array. Empty values will not be added to the
+output.
+
+### `$exportUserOptionSettings`
+
+Any user option that exists within a `settings.*` category is automatically
+excluded from the export, with the notable exception of the `timezone` option.
+You can opt-in to include your setting by appending to this array, if it contains
+any personal data. The listed settings are always included regardless of their
+content.
+
+### `$exportUserOptionSettingsIfNotEmpty`
+
+Any user option that exists within a `settings.*` category is automatically
+excluded from the export, with the notable exception of the `timezone` option.
+You can opt-in to include your setting by appending to this array, if it contains
+any personal data.
+
+### `$ipAddresses`
+
+List of database table names per package identifier that contain ip addresses.
+The contained ip addresses will be exported when the ip logging module is enabled.
+
+It expects the database table to use the column names `ipAddress`, `time` and
+`userID`. If your table does not match this pattern for whatever reason, you'll
+need to manually probe for `LOG_IP_ADDRESS` and then call `exportIpAddresses()`
+to retrieve the list. Afterwards you are responsible to append these ip addresses
+to the `$data` array to have it exported.
+
+### `$skipUserOptions`
+
+All user options are included in the export by default, unless they start with
+`can*` or `admin*`, or are blacklisted using this array. You should append any
+of your plugin's or app's user option that should not be exported, for example
+because it does not contain personal data, such as internal data.
+
+{% include links.html %}
--- /dev/null
+---
+title: Page Types
+sidebar: sidebar
+permalink: php_pages.html
+folder: php
+---
+
+## AbstractPage
+
+The default implementation for pages to present any sort of content, but are designed to handle `GET` requests only. They usually follow a fixed method chain that will be invoked one after another, adding logical sections to the request flow.
+
+### Method Chain
+
+#### \__run()
+
+This is the only method being invoked from the outside and starts the whole chain.
+
+#### readParameters()
+
+Reads and sanitizes request parameters, this should be the only method to ever read user-supplied input. Read data should be stored in class properties to be accessible at a later point, allowing your code to safely assume that the data has been sanitized and is safe to work with.
+
+A typical example is the board page from the forum app that reads the id and attempts to identify the request forum.
+
+```php
+public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->boardID = intval($_REQUEST['id']);
+ $this->board = BoardCache::getInstance()->getBoard($this->boardID);
+ if ($this->board === null) {
+ throw new IllegalLinkException();
+ }
+
+ // check permissions
+ if (!$this->board->canEnter()) {
+ throw new PermissionDeniedException();
+ }
+}
+```
+
+<span class="label label-info">Events</span> `readParameters`
+
+#### show()
+
+Used to be the method of choice to handle permissions and module option checks, but has been used almost entirely as an internal method since the introduction of the properties `$loginRequired`, `$neededModules` and `$neededPermissions`.
+
+<span class="label label-info">Events</span> `checkModules`, `checkPermissions` and `show`
+
+#### readData()
+
+Central method for data retrieval based on class properties including those populated with user data in `readParameters()`. It is strongly recommended to use this method to read data in order to properly separate the business logic present in your class.
+
+<span class="label label-info">Events</span> `readData`
+
+#### assignVariables()
+
+Last method call before the template engine kicks in and renders the template. All though some properties are bound to the template automatically, you still need to pass any custom variables and class properties to the engine to make them available in templates.
+
+Following the example in `readParameters()`, the code below adds the board data to the template.
+
+```php
+public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'board' => $this->board,
+ 'boardID' => $this->boardID
+ ]);
+}
+```
+
+<span class="label label-info">Events</span> `assignVariables`
+
+## AbstractForm
+
+Extends the AbstractPage implementation with additional methods designed to handle form submissions properly.
+
+### Method Chain
+
+#### \__run()
+
+*Inherited from AbstractPage.*
+
+#### readParameters()
+
+*Inherited from AbstractPage.*
+
+#### show()
+
+*Inherited from AbstractPage.*
+
+#### submit()
+
+{% include callout.html content="The methods `submit()` up until `save()` are only invoked if either `$_POST` or `$_FILES` are not empty, otherwise they won't be invoked and the execution will continue with `readData()`." type="warning" %}
+
+This is an internal method that is responsible of input processing and validation.
+
+<span class="label label-info">Events</span> `submit`
+
+#### readFormParameters()
+
+This method is quite similar to `readParameters()` that is being called earlier, but is designed around reading form data submitted through POST requests. You should avoid accessing `$_GET` or `$_REQUEST` in this context to avoid mixing up parameters evaluated when retrieving the page on first load and when submitting to it.
+
+<span class="label label-info">Events</span> `readFormParameters`
+
+#### validate()
+
+Deals with input validation and automatically catches exceptions deriving from `wcf\system\exception\UserInputException`, resulting in a clean and consistent error handling for the user.
+
+<span class="label label-info">Events</span> `validate`
+
+#### save()
+
+Saves the processed data to database or any other source of your choice. Please keep in mind to invoke `$this->saved()` before resetting the form data.
+
+<span class="label label-info">Events</span> `save`
+
+#### saved()
+
+{% include callout.html content="This method is not called automatically and must be invoked manually by executing `$this->saved()` inside `save()`." type="warning" %}
+
+The only purpose of this method is to fire the event `saved` that signals that the form data has been processed successfully and data has been saved. It is somewhat special as it is dispatched after the data has been saved, but before the data is purged during form reset. This is by default the last event that has access to the processed data.
+
+<span class="label label-info">Events</span> `saved`
+
+#### readData()
+
+*Inherited from AbstractPage.*
+
+#### assignVariables()
+
+*Inherited from AbstractPage.*
--- /dev/null
+---
+title: Tutorial Series
+sidebar: sidebar
+permalink: tutorial_tutorial-series.html
+folder: tutorial
+---
+
+In this tutorial series, we will code a package that allows administrators to create a registry of people.
+In this context, "people" does not refer to users registered on the website but anybody living, dead or fictional.
+
+We will start this tutorial series by creating a base structure for the package and then continue by adding further features step by step using different APIs.
+Note that in the context of this example, not every added feature might make perfect sense but the goal of this tutorial is not to create a useful package but to introduce you to WoltLab Suite.
+
+- [Part 1: Base Structure](tutorial_tutorial-series_part-1-base-structure.html)
+- [Part 2: Event Listeners and Template Listeners](tutorial_tutorial-series_part-2-event-listeners-and-template-listeners.html)
+- [Part 3: Person Page and Comments](tutorial_tutorial-series_part-3-person-page-and-comments.html)
--- /dev/null
+---
+title: "Tutorial Series Part 1: Base Structure"
+sidebar: sidebar
+permalink: tutorial_tutorial-series_part-1-base-structure.html
+folder: tutorial/tutorial-series
+parent: tutorial_tutorial-series
+---
+
+In the first part of this tutorial series, we will lay out what the basic version of package should be able to do and how to implement these functions.
+
+
+## Package Functionality
+
+The package should provide the following possibilities/functions:
+
+- Sortable list of all people in the ACP
+- Ability to add, edit and delete people in the ACP
+- Restrict the ability to add, edit and delete people (in short: manage people) in the ACP
+- Sortable list of all people in the front end
+
+
+## Used Components
+
+We will use the following package installation plugins:
+
+- [acpTemplate package installation plugin](package_pip_acp-template.html),
+- [acpMenu package installation plugin](package_pip_acp-menu.html),
+- [file package installation plugin](package_pip_file.html),
+- [language package installation plugin](package_pip_language.html),
+- [menuItem package installation plugin](package_pip_menu-item.html),
+- [page package installation plugin](package_pip_page.html),
+- [sql package installation plugin](package_pip_sql.html),
+- [template package installation plugin](package_pip_template.html),
+- [userGroupOption package installation plugin](package_pip_user-group-option.html),
+
+use [database objects](php_database-objects.html), create [pages](php_pages.html) and use [templates](view_templates.html).
+
+
+## Package Structure
+
+The package will have the following file structure:
+
+```
+├── acpMenu.xml
+├── acptemplates
+│ ├── personAdd.tpl
+│ └── personList.tpl
+├── files
+│ └── lib
+│ ├── acp
+│ │ ├── form
+│ │ │ ├── PersonAddForm.class.php
+│ │ │ └── PersonEditForm.class.php
+│ │ └── page
+│ │ └── PersonListPage.class.php
+│ ├── data
+│ │ └── person
+│ │ ├── PersonAction.class.php
+│ │ ├── Person.class.php
+│ │ ├── PersonEditor.class.php
+│ │ └── PersonList.class.php
+│ └── page
+│ └── PersonListPage.class.php
+├── install.sql
+├── language
+│ ├── de.xml
+│ └── en.xml
+├── menuItem.xml
+├── package.xml
+├── page.xml
+├── templates
+│ └── personList.tpl
+└── userGroupOption.xml
+```
+
+
+## Person Modeling
+
+### Database Table
+
+As the first step, we have to model the people we want to manage with this package.
+As this is only an introductory tutorial, we will keep things simple and only consider the first and last name of a person.
+Thus, the database table we will store the people in only contains three columns:
+
+1. `personID` is the unique numeric identifier of each person created,
+1. `firstName` contains the first name of the person,
+1. `lastName` contains the last name of the person.
+
+The first file for our package is the `install.sql` file used to create such a database table during package installation:
+
+{% highlight sql %}
+{% include tutorial/tutorial-series/part-1/install.sql %}
+{% endhighlight %}
+
+### Database Object
+
+#### `Person`
+
+In our PHP code, each person will be represented by an object of the following class:
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php %}
+{% endhighlight %}
+
+The important thing here is that `Person` extends `DatabaseObject`.
+Additionally, we implement the `IRouteController` interface, which allows us to use `Person` objects to create links, and we implement PHP's magic [__toString()](https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring) method for convenience.
+
+For every database object, you need to implement three additional classes:
+an action class, an editor class and a list class.
+
+#### `PersonAction`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php %}
+{% endhighlight %}
+
+This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
+This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
+`$permissionsDelete` has to be set to the permission needed in order to delete a person.
+We will later use the [userGroupOption package installation plugin](package_pip_user-group-option.html) to create the `admin.content.canManagePeople` permission.
+`$requireACP` restricts deletion of people to the ACP.
+
+#### `PersonEditor`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php %}
+{% endhighlight %}
+
+This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
+setting the static `$baseClass` property to the database object class name.
+
+#### `PersonList`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php %}
+{% endhighlight %}
+
+Due to the default implementation of `DatabaseObjectList`, our `PersonList` class just needs to extend it and everything else is either automatically set by the code of `DatabaseObjectList` or, in the case of properties and methods, provided by that class.
+
+
+## ACP
+
+Next, we will take care of the controllers and views for the ACP.
+In total, we need three each:
+
+1. page to list people,
+1. form to add people, and
+1. form to edit people.
+
+Before we create the controllers and views, let us first create the menu items for the pages in the ACP menu.
+
+### ACP Menu
+
+We need to create three menu items:
+
+1. a “parent” menu item on the second level of the ACP menu item tree,
+1. a third level menu item for the people list page, and
+1. a fourth level menu item for the form to add new people.
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-1/acpMenu.xml %}
+{% endhighlight %}
+
+We choose `wcf.acp.menu.link.content` as the parent menu item for the first menu item `wcf.acp.menu.link.person` because the people we are managing is just one form of content.
+The fourth level menu item `wcf.acp.menu.link.person.add` will only be shown as an icon and thus needs an additional element `icon` which takes a FontAwesome icon class as value.
+
+### People List
+
+To list the people in the ACP, we need a `PersonListPage` class and a `personList` template.
+
+#### `PersonListPage`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php %}
+{% endhighlight %}
+
+As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
+
+1. We need to set the active ACP menu item via the `$activeMenuItem`.
+1. `$neededPermissions` contains a list of permissions of which the user needs to have at least one in order to see the person list.
+ We use the same permission for both the menu item and the page.
+1. The database object list class whose name is provided via `$objectListClassName` and that handles fetching the people from database is the `PersonList` class, which we have already created.
+1. To validate the sort field passed with the request, we set `$validSortFields` to the available database table columns.
+
+#### `personList.tpl`
+
+{% highlight smarty %}
+{% include tutorial/tutorial-series/part-1/acptemplates/personList.tpl %}
+{% endhighlight %}
+
+We will go piece by piece through the template code:
+
+1. We include the `header` template and set the page title `wcf.acp.person.list`.
+ You have to include this template for every page!
+1. We set the content header and additional provide a button to create a new person in the content header navigation.
+1. As not all people are listed on the same page if many people have been created, we need a pagination for which we use the `pages` template plugin.
+ The `{hascontent}{content}{/content}{/hascontent}` construct ensures the `.paginationTop` element is only shown if the `pages` template plugin has a return value, thus if a pagination is necessary.
+1. Now comes the main part of the page, the list of the people, which will only be displayed if any people exist.
+ Otherwise, an info box is displayed using the generic `wcf.global.noItems` language item.
+ The `$objects` template variable is automatically assigned by `wcf\page\MultipleLinkPage` and contains the `PersonList` object used to read the people from database.
+
+ The table itself consists of a `thead` and a `tbody` element and is extendable with more columns using the template events `columnHeads` and `columns`.
+ In general, every table should provide these events.
+ The default structure of a table is used here so that the first column of the content rows contains icons to edit and to delete the row (and provides another standard event `rowButtons`) and that the second column contains the ID of the person.
+ The table can be sorted by clicking on the head of each column.
+ The used variables `$sortField` and `$sortOrder` are automatically assigned to the template by `SortablePage`.
+1. The `.contentFooter` element is only shown if people exist as it basically repeats the `.contentHeaderNavigation` and `.paginationTop` element.
+1. The JavaScript code here fulfills two duties:
+ Handling clicks on the delete icons and forwarding the requests via AJAX to the `PersonAction` class, and setting up some code that triggers if all people shown on the current page are deleted via JavaScript to either reload the page or show the `wcf.global.noItems` info box.
+1. Lastly, the `footer` template is included that terminates the page.
+ You also have to include this template for every page!
+
+Now, we have finished the page to manage the people so that we can move on to the forms with which we actually create and edit the people.
+
+### Person Add Form
+
+Like the person list, the form to add new people requires a controller class and a template.
+
+#### `PersonAddForm`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php %}
+{% endhighlight %}
+
+The properties here consist of two types:
+the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the “data” properties `$firstName` and `$lastName`, which will contain the data entered by the user of the person to be created.
+
+Now, let's go through each method in execution order:
+
+1. `readFormParameters()` is called after the form has been submitted and reads the entered first and last name and sanitizes the values by calling `StringUtil::trim()`.
+1. `validate()` is called after the form has been submitted and is used to validate the input data.
+ In case of invalid data, the method is expected to throw a `UserInputException`.
+ Here, the validation for first and last name is the same and quite basic:
+ We check that any name has been entered and that it is not longer than the database table column permits.
+1. `save()` is called after the form has been submitted and the entered data has been validated and it creates the new person via `PersonAction`.
+ Please note that we do not just pass the first and last name to the action object but merge them with the `$this->additionalFields` array which can be used by event listeners of plugins to add additional data.
+ After creating the object, the `saved()` method is called which fires an event for plugins and the data properties are cleared so that the input fields on the page are empty so that another new person can be created.
+ Lastly, a `success` variable is assigned to the template which will show a message that the person has been successfully created.
+1. `assignVariables()` assigns the values of the “data” properties to the template and additionally assigns an `action` variable.
+ This `action` variable will be used in the template to distinguish between adding a new person and editing an existing person so that which minimal adjustments, we can use the template for both cases.
+
+#### `personAdd.tpl`
+
+{% highlight smarty %}
+{% include tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl %}
+{% endhighlight %}
+
+We will now only concentrate on the new parts compared to `personList.tpl`:
+
+1. We use the `$action` variable to distinguish between the languages items used for adding a person and for creating a person.
+1. Including the `formError` template automatically shows an error message if the validation failed.
+1. The `.success` element is shown after successful saving the data and, again, shows different a text depending on the executed action.
+1. The main part is the `form` element which has a common structure you will find in many forms in WoltLab Suite Core.
+ The notable parts here are:
+ - The `action` attribute of the `form` element is set depending on which controller will handle the request.
+ In the link for the edit controller, we can now simply pass the edited `Person` object directly as the `Person` class implements the `IRouteController` interface.
+ - The field that caused the validation error can be accessed via `$errorField`.
+ - The type of the validation error can be accessed via `$errorType`.
+ For an empty input field, we show the generic `wcf.global.form.error.empty` language item.
+ In all other cases, we use the error type to determine the object- and property-specific language item to show.
+ The approach used here allows plugins to easily add further validation error messages by simply using a different error type and providing the associated language item.
+ - Input fields can be grouped into different `.section` elements.
+ At the end of each `.section` element, there should be an template event whose name ends with `Fields`.
+ The first part of the event name should reflect the type of fields in the particular `.section` element.
+ Here, the input fields are just general “data” fields so that the event is called `dataFields`.
+ - After the last `.section` element, fire a `section` event so that plugins can add further sections.
+ - Lastly, the `.formSubmit` shows the submit button and `{csrfToken}` contains a CSRF token that is automatically validated after the form is submitted.
+
+### Person Edit Form
+
+As mentioned before, for the form to edit existing people, we only need a new controller as the template has already been implemented in a way that it handles both, adding and editing.
+
+#### `PersonEditForm`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php %}
+{% endhighlight %}
+
+In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
+
+After setting a different active menu item, we declare two new properties for the edited person:
+the id of the person passed in the URL is stored in `$personID` and based on this ID, a `Person` object is created that is stored in the `$person` property.
+
+Now let use go through the different methods in chronological order again:
+
+1. `readParameters()` reads the passed ID of the edited person and creates a `Person` object based on this ID.
+ If the ID is invalid, `$this->person->personID` is `null` and an `IllegalLinkException` is thrown.
+1. `readData()` only executes additional code in the case if `$_POST` is empty, thus only for the initial request before the form has been submitted.
+ The data properties of `PersonAddForm` are populated with the data of the edited person so that this data is shown in the form for the initial request.
+1. `save()` handles saving the changed data.
+
+ {% include callout.html content="Do not call `parent::save()` because that would cause `PersonAddForm::save()` to be executed and thus a new person would to be created! In order for the `save` event to be fired, call `AbstractForm::save()` instead!" type="warning" %}
+
+ The only differences compared to `PersonAddForm::save()` are that we pass the edited object to the `PersonAction` constructor, execute the `update` action instead of the `create` action and do not clear the input fields after saving the changes.
+1. In `assignVariables()`, we assign the edited `Person` object to the template, which is required to create the link in the form’s action property.
+ Furthermore, we assign the template variable `$action` `edit` as value.
+
+ {% include callout.html content="After calling `parent::assignVariables()`, the template variable `$action` actually has the value `add` so that here, we are overwriting this already assigned value." type="info" %}
+
+
+## Frontend
+
+For the front end, that means the part with which the visitors of a website interact, we want to implement a simple sortable page that lists the people.
+This page should also be directly linked in the main menu.
+
+### `page.xml`
+
+First, let us register the page with the system because every front end page or form needs to be explicitly registered using the [page package installation plugin](package_pip_page.html):
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-1/page.xml %}
+{% endhighlight %}
+
+For more information about what each of the elements means, please refer to the [page package installation plugin page](package_pip_page.html).
+
+### `menuItem.xml`
+
+Next, we register the menu item using the [menuItem package installation plugin](package_pip_menuItem.html):
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-1/menuItem.xml %}
+{% endhighlight %}
+
+Here, the import parts are that we register the menu item for the main menu `com.woltlab.wcf.MainMenu` and link the menu item with the page `com.woltlab.wcf.people.PersonList`, which we just registered.
+
+### People List
+
+As in the ACP, we need a controller and a template.
+You might notice that both the controller’s (unqualified) class name and the template name are the same for the ACP and the front end.
+This is no problem because the qualified names of the classes differ and the files are stored in different directories and because the templates are installed by different package installation plugins and are also stored in different directories.
+
+#### `PersonListPage`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php %}
+{% endhighlight %}
+
+This class is almost identical to the ACP version.
+In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page.
+Furthermore, `$neededPermissions` has not been set because in the front end, users do not need any special permission to access the page.
+In the front end, we explicitly set the `$defaultSortField` so that the people listed on the page are sorted by their last name (in ascending order) by default.
+
+#### `personList.tpl`
+
+{% highlight smarty %}
+{% include tutorial/tutorial-series/part-1/templates/personList.tpl %}
+{% endhighlight %}
+
+If you compare this template to the one used in the ACP, you will recognize similar elements like the `.paginationTop` element, the `p.info` element if no people exist, and the `.contentFooter` element.
+Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
+
+Now, let us take a closer look at the differences:
+
+- We do not explicitly create a `.contentHeader` element but simply assign the title to the `contentTitle` variable.
+ The value of the assignment is simply the title of the page and a badge showing the number of listed people.
+ The `header` template that we include later will handle correctly displaying the content header on its own based on the `$contentTitle` variable.
+- Next, we create additional element for the HTML document’s `<head>` element.
+ In this case, we define the [canonical link of the page](https://en.wikipedia.org/wiki/Canonical_link_element) and, because we are showing paginated content, add links to the previous and next page (if they exist).
+- We want the page to be sortable but as we will not be using a table for listing the people like in the ACP, we are not able to place links to sort the people into the table head.
+ Instead, usually a box is created in the sidebar on the right-hand side that contains `select` elements to determine sort field and sort order.
+- The main part of the page is the listing of the people.
+ We use a structure similar to the one used for displaying registered users.
+ Here, for each person, we simply display a FontAwesome icon representing a person and show the person’s full name relying on `Person::__toString()`.
+ Additionally, like in the user list, we provide the initially empty `ul.inlineList.commaSeparated` and `dl.plain.inlineDataList.small` elements that can be filled by plugins using the templates events.
+
+
+## `userGroupOption.xml`
+
+We have already used the `admin.content.canManagePeople` permissions several times, now we need to install it using the [userGroupOption package installation plugin](package_pip_user-group-option.html):
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-1/userGroupOption.xml %}
+{% endhighlight %}
+
+We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
+As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
+This permission is only relevant for registered users so that it should not be visible when editing the guest user group.
+This is achieved by setting `usersonly` to `1`.
+
+
+## `package.xml`
+
+Lastly, we need to create the `package.xml` file.
+For more information about this kind of file, please refer to [the `package.xml` page](package_package-xml.html).
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-1/package.xml %}
+{% endhighlight %}
+
+As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
+We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
+Additionally, we disallow installation of the package in the next major version `3.1` by excluding the `3.1.0 Alpha 1` version.
+This ensures that if changes from WoltLab Suite Core 3.0 to 3.1 require changing some parts of the package, it will not break the instance in which the package is installed.
+
+The most important part are to installation instructions.
+First, we install the ACP templates, files and templates, create the database table and import the language item.
+Afterwards, the ACP menu items and the permission are added.
+Now comes the part of the instructions where the order of the instructions is crucial:
+In `menuItem.xml`, we refer to the `com.woltlab.wcf.people.PersonList` page that is delivered by `page.xml`.
+As the menu item package installation plugin validates the given page and throws an exception if the page does not exist, we need to install the page before the menu item!
+
+---
+
+This concludes the first part of our tutorial series after which you now have a working simple package with which you can manage people in the ACP and show the visitors of your website a simple list of all created people in the front end.
+
+The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-1).
--- /dev/null
+---
+title: "Part 2: Event Listeners and Template Listeners"
+sidebar: sidebar
+permalink: tutorial_tutorial-series_part-2-event-listeners-and-template-listeners.html
+folder: tutorial/tutorial-series
+parent: tutorial_tutorial-series
+---
+
+In the [first part](tutorial_tutorial-series_part-1-base-structure.html) of this tutorial series, we have created the base structure of our people management package.
+In further parts, we will use the package of the first part as a basis to directly add new features.
+In order to explain how event listeners and template works, however, we will not directly adding a new feature to the package by altering it in this part, but we will assume that somebody else created the package and that we want to extend it the “correct” way by creating a plugin.
+
+The goal of the small plugin that will be created in this part is to add the birthday of the managed people.
+As in the first part, we will not bother with careful validation of the entered date but just make sure that it is a valid date.
+
+
+## Package Functionality
+
+The package should provide the following possibilities/functions:
+
+- List person’s birthday (if set) in people list in the ACP
+- Sort people list by birthday in the ACP
+- Add or remove birthday when adding or editing person
+- List person’s birthday (if set) in people list in the front end
+- Sort people list by birthday in the front end
+
+
+## Used Components
+
+We will use the following package installation plugins:
+
+- [acpTemplate package installation plugin](package_pip_acp-template.html),
+- [eventListener package installation plugin](package_pip_event-listener.html),
+- [file package installation plugin](package_pip_file.html),
+- [language package installation plugin](package_pip_language.html),
+- [sql package installation plugin](package_pip_sql.html),
+- [template package installation plugin](package_pip_template.html),
+- [templateListener package installation plugin](package_pip_template-listener.html).
+
+For more information about the event system, please refer to the [dedicated page on events](php_api_events.html).
+
+
+## Package Structure
+
+The package will have the following file structure:
+
+```
+├── acptemplates
+│ └── __personAddBirthday.tpl
+├── eventListener.xml
+├── files
+│ └── lib
+│ └── system
+│ └── event
+│ └── listener
+│ ├── BirthdayPersonAddFormListener.class.php
+│ └── BirthdaySortFieldPersonListPageListener.class.php
+├── install.sql
+├── language
+│ ├── de.xml
+│ └── en.xml
+├── package.xml
+├── templateListener.xml
+└── templates
+ ├── __personListBirthday.tpl
+ └── __personListBirthdaySortField.tpl
+```
+
+
+## Extending Person Model (`install.sql`)
+
+The existing model of a person only contains the person’s first name and their last name (in additional to the id used to identify created people).
+To add the birthday to the model, we need to create an additional database table column using the [sql package installation plugin](package_pip_sql.html):
+
+{% highlight sql %}
+{% include tutorial/tutorial-series/part-2/install.sql %}
+{% endhighlight %}
+
+If we have a [Person object](tutorial_tutorial-series_part-1-base-structure.html#person), this new property can be accessed the same way as the `personID` property, the `firstName` property, or the `lastName` property from the base package: `$person->birthday`.
+
+
+## Setting Birthday in ACP
+
+To set the birthday of a person, we need to extend the `personAdd` template to add an additional birthday field.
+This can be achieved using the `dataFields` template event at whose position we inject the following template code:
+
+{% highlight sql %}
+{% include tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl %}
+{% endhighlight %}
+
+which we store in a `__personAddBirthday.tpl` template file.
+The used language item `wcf.person.birthday` is actually the only new one for this package:
+
+{% highlight sql %}
+{% include tutorial/tutorial-series/part-2/language/de.xml %}
+{% endhighlight %}
+
+{% highlight sql %}
+{% include tutorial/tutorial-series/part-2/language/en.xml %}
+{% endhighlight %}
+
+The template listener needs to be registered using the [templateListener package installation plugin](package_pip_template-listener.html).
+The corresponding complete `templateListener.xml` file is included [below](#templatelistenerxml).
+
+The template code alone is not sufficient because the `birthday` field is, at the moment, neither read, nor processed, nor saved by any PHP code.
+This can be be achieved, however, by adding event listeners to `PersonAddForm` and `PersonEditForm` which allow us to execute further code at specific location of the program.
+Before we take a look at the event listener code, we need to identify exactly which additional steps we need to undertake:
+
+1. If a person is edited and the form has not been submitted, the existing birthday of that person needs to be read.
+1. If a person is added or edited and the form has been submitted, the new birthday value needs to be read.
+1. If a person is added or edited and the form has been submitted, the new birthday value needs to be validated.
+1. If a person is added or edited and the new birthday value has been successfully validated, the new birthday value needs to be saved.
+1. If a person is added and the new birthday value has been successfully saved, the internally stored birthday needs to be reset so that the birthday field is empty when the form is shown again.
+1. The internally stored birthday value needs to be assigned to the template.
+
+The following event listeners achieves these requirements:
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php %}
+{% endhighlight %}
+
+Some notes on the code:
+
+- We are inheriting from `AbstractEventListener`, instead of just implementing the `IParameterizedEventListener` interface.
+ The `execute()` method of `AbstractEventListener` contains a dispatcher that automatically calls methods called `on` followed by the event name with the first character uppercased, passing the event object and the `$parameters` array.
+ This simple pattern results in the event `foo` being forwarded to the method `onFoo($eventObj, $parameters)`.
+- The `birthday` column has a default value of `0000-00-00`, which we interpret as “birthday not set”.
+ To show an empty input field in this case, we empty the `birthday` property after reading such a value in `readData()`.
+- The validation of the date is, as mentioned before, very basic and just checks the form of the string and uses PHP’s [checkdate](https://secure.php.net/manual/en/function.checkdate.php) function to validate the components.
+- The `save` needs to make sure that the passed date is actually a valid date and set it to `0000-00-00` if no birthday is given.
+ To actually save the birthday in the database, we do not directly manipulate the database but can add an additional field to the data array passed to `PersonAction::create()` via `AbstractForm::$additionalFields`.
+ As the `save` event is the last event fired before the actual save process happens, this is the perfect event to set this array element.
+
+The event listeners are installed using the `eventListener.xml` file shown [below](#eventlistenerxml).
+
+
+## Adding Birthday Table Column in ACP
+
+To add a birthday column to the person list page in the ACP, we need three parts:
+
+1. an event listener that makes the `birthday` database table column a valid sort field,
+1. a template listener that adds the birthday column to the table’s head, and
+1. a template listener that adds the birthday column to the table’s rows.
+
+The first part is a very simple class:
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php %}
+{% endhighlight %}
+
+{% include callout.html content="We use `SortablePage` as a type hint instead of `wcf\acp\page\PersonListPage` because we will be using the same event listener class in the front end to also allow sorting that list by birthday." type="info" %}
+
+As the relevant template codes are only one line each, we will simply put them directly in the `templateListener.xml` file that will be shown [later on](#templatelistenerxml).
+The code for the table head is similar to the other `th` elements:
+
+```smarty
+<th class="columnDate columnBirthday{if $sortField == 'birthday'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=birthday&sortOrder={if $sortField == 'birthday' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.birthday{/lang}</a></th>
+```
+
+For the table body’s column, we need to make sure that the birthday is only show if it is actually set:
+
+```smarty
+<td class="columnDate columnBirthday">{if $person->birthday !== '0000-00-00'}{@$person->birthday|strtotime|date}{/if}</td>
+```
+
+
+## Adding Birthday in Front End
+
+In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person’s “statistics”.
+
+To add the birthday as a valid sort field, we use `BirthdaySortFieldPersonListPageListener` just as in the ACP.
+In the front end, we will now use a template (`__personListBirthdaySortField.tpl`) instead of a directly putting the template code in the `templateListener.xml` file:
+
+{% highlight smarty %}
+{% include tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl %}
+{% endhighlight %}
+
+{% include callout.html content="You might have noticed the two underscores at the beginning of the template file. For templates that are included via template listeners, this is the naming convention we use." type="info" %}
+
+Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable.
+
+To show the birthday, we use the following template code for the `personStatistics` template event, which again makes sure that the birthday is only shown if it is actually set:
+
+{% highlight smarty %}
+{% include tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl %}
+{% endhighlight %}
+
+
+## `templateListener.xml`
+
+The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-2/templateListener.xml %}
+{% endhighlight %}
+
+In cases where a template is used, we simply use the `include` syntax to load the template.
+
+
+## `eventListener.xml`
+
+There are two event listeners, `birthdaySortFieldAdminPersonList` and `birthdaySortFieldPersonList`, that make `birthday` a valid sort field in the ACP and the front end, respectively, and the rest takes care of setting the birthday.
+The event listener `birthdayPersonAddFormInherited` takes care of the events that are relevant for both adding and editing people, thus it listens to the `PersonAddForm` class but has `inherit` set to `1` so that it also listens to the events of the `PersonEditForm` class.
+In contrast, reading the existing birthday from a person is only relevant for editing so that the event listener `birthdayPersonEditForm` only listens to that class.
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-2/eventListener.xml %}
+{% endhighlight %}
+
+
+## `package.xml`
+
+The only relevant difference between the `package.xml` file of the base page from part 1 and the `package.xml` file of this package is that this package requires the base package `com.woltlab.wcf.people` (see `<requiredpackages>`):
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-2/package.xml %}
+{% endhighlight %}
+
+---
+
+This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people.
+
+The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-2).
--- /dev/null
+---
+title: "Tutorial Series Part 3: Person Page and Comments"
+sidebar: sidebar
+permalink: tutorial_tutorial-series_part-3-person-page-and-comments.html
+folder: tutorial/tutorial-series
+parent: tutorial_tutorial-series
+---
+
+In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details.
+To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite’s reusable comment functionality.
+
+
+## Package Functionality
+
+In addition to the existing functions from [part 1](tutorial_tutorial-series_part-1-base-structure.html), the package will provide the following possibilities/functions after this part of the tutorial:
+
+- Details page for each person linked in the front end person list
+- Comment on people on their respective page (can be disabled per person)
+- User online location for person details page with name and link to person details page
+- Create menu items linking to specific person details pages
+
+
+## Used Components
+
+In addition to the components used in [part 1](tutorial_tutorial-series_part-1-base-structure.html), we will use the [objectType package installation plugin](package_pip_object-type.html), use the [comment API](php_api_comments.html), create a [runtime cache](php_api_caches_runtime-caches.html), and create a page handler.
+
+
+## Package Structure
+
+The complete package will have the following file structure (including the files from [part 1](tutorial_tutorial-series_part-1-base-structure.html)):
+
+```
+├── acpMenu.xml
+├── acptemplates
+│ ├── personAdd.tpl
+│ └── personList.tpl
+├── files
+│ └── lib
+│ ├── acp
+│ │ ├── form
+│ │ │ ├── PersonAddForm.class.php
+│ │ │ └── PersonEditForm.class.php
+│ │ └── page
+│ │ └── PersonListPage.class.php
+│ ├── data
+│ │ └── person
+│ │ ├── Person.class.php
+│ │ ├── PersonAction.class.php
+│ │ ├── PersonEditor.class.php
+│ │ └── PersonList.class.php
+│ ├── page
+│ │ ├── PersonListPage.class.php
+│ │ └── PersonPage.class.php
+│ └── system
+│ ├── cache
+│ │ └── runtime
+│ │ └── PersonRuntimeCache.class.php
+│ ├── comment
+│ │ └── manager
+│ │ └── PersonCommentManager.class.php
+│ └── page
+│ └── handler
+│ └── PersonPageHandler.class.php
+├── install.sql
+├── language
+│ ├── de.xml
+│ └── en.xml
+├── menuItem.xml
+├── objectType.xml
+├── package.xml
+├── page.xml
+├── templates
+│ ├── person.tpl
+│ └── personList.tpl
+└── userGroupOption.xml
+```
+
+{% include callout.html content="We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new `Person::getLink()` method and new language items have been added. For all changes, please refer to the [source code on GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3)." type="warning" %}
+
+
+## Runtime Cache
+
+To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](php_api_caches_runtime-caches.html) for people:
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php %}
+{% endhighlight %}
+
+
+## Comments
+
+To allow users to comment on people, we need to tell the system that people support comments.
+This is done by registering a `com.woltlab.wcf.comment.commentableContent` object type whose processor implements [ICommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php):
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-3/objectType.xml %}
+{% endhighlight %}
+
+The `PersonCommentManager` class extended `ICommentManager`’s default implementation [AbstractCommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/AbstractCommentManager.class.php):
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php %}
+{% endhighlight %}
+
+- First, the system is told the names of the permissions via the `$permission*` properties.
+ More information about comment permissions can be found [here](php_api_comments.html#user-group-options).
+- The `getLink()` method returns the link to the person with the passed comment id.
+ As in `isAccessible()`, `PersonRuntimeCache` is used to potentially save database queries.
+- The `isAccessible()` method checks if the active user can access the relevant person.
+ As we do not have any special restrictions for accessing people, we only need to check if the person exists.
+- The `getTitle()` method returns the title used for comments and responses, which is just a generic language item in this case.
+- The `updateCounter()` updates the comments’ counter of the person.
+ We have added a new `comments` database table column to the `wcf1_person` database table in order to keep track on the number of comments.
+
+Additionally, we have added a new `enableComments` database table column to the `wcf1_person` database table whose value can be set when creating or editing a person in the ACP.
+With this option, comments on individual people can be disabled.
+
+{% include callout.html content="Liking comments is already built-in and only requires some extra code in the `PersonPage` class for showing the likes of pre-loaded comments." type="info" %}
+
+
+## Person Page
+
+### `PersonPage`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php %}
+{% endhighlight %}
+
+The `PersonPage` class is similar to the `PersonEditForm` in the ACP in that it reads the id of the requested person from the request data and validates the id in `readParameters()`.
+The rest of the code only handles fetching the list of comments on the requested person.
+In `readData()`, this list is fetched using `CommentHandler::getCommentList()` if comments are enabled for the person.
+The `assignVariables()` method assigns some additional template variables like `$commentCanAdd`, which is `1` if the active person can add comments and is `0` otherwise, `$lastCommentTime`, which contains the UNIX timestamp of the last comment, and `$likeData`, which contains data related to the likes for the disabled comments.
+
+### `person.tpl`
+
+{% highlight tpl %}
+{% include tutorial/tutorial-series/part-3/templates/person.tpl %}
+{% endhighlight %}
+
+For now, the `person` template is still very empty and only shows the comments in the content area.
+The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container `ul#personCommentList` element for the comments shown by `commentList` template.
+The `ul#personCommentList` elements has five additional `data-` attributes required by the JavaScript API for comments for loading more comments or creating new ones.
+The `commentListAddComment` template adds the WYSIWYG support.
+The attribute `wysiwygSelector` should be the id of the comment list `personCommentList` with an additional `AddComment` suffix.
+
+### `page.xml`
+
+{% highlight xml %}
+{% include tutorial/tutorial-series/part-3/page.xml %}
+{% endhighlight %}
+
+The `page.xml` file has been extended for the new person page with identifier `com.woltlab.wcf.people.Person`.
+Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are four differences:
+
+1. It has a `<handler>` element with a class name as value.
+ This aspect will be discussed in more detail in the next section.
+1. There are no `<content>` elements because, both, the title and the content of the page are dynamically generated in the template.
+1. The `<requireObjectID>` tells the system that this page requires an object id to properly work, in this case a valid person id.
+1. This page has a `<parent>` page, the person list page.
+ In general, the details page for any type of object that is listed on a different page has the list page as its parent.
+
+### `PersonPageHandler`
+
+{% highlight php %}
+{% include tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php %}
+{% endhighlight %}
+
+Like any page handler, the `PersonPageHandler` class has to implement the [IMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IMenuPageHandler.class.php) interface, which should be done by extending the [AbstractMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractMenuPageHandler.class.php) class.
+As we want administrators to link to specific people in menus, for example, we have to also implement the [ILookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/ILookupPageHandler.class.php) interface by extending the [AbstractLookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractLookupPageHandler.class.php) class.
+
+For the `ILookupPageHandler` interface, we need to implement three methods:
+
+1. `getLink($objectID)` returns the link to the person page with the given id.
+ In this case, we simply delegate this method call to the `Person` object returned by `PersonRuntimeCache::getObject()`.
+1. `isValid($objectID)` returns `true` if the person with the given id exists, otherwise `false`.
+ Here, we use `PersonRuntimeCache::getObject()` again and check if the return value is `null`, which is the case for non-existing people.
+1. `lookup($searchString)` is used when setting up an internal link and when searching for the linked person.
+ This method simply searches the first and last name of the people and returns an array with the person data.
+ While the `link`, the `objectID`, and the `title` element are self-explanatory, the `image` element can either contain an HTML `<img>` tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with `fa-`).
+
+Additionally, the class also implements [IOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IOnlineLocationPageHandler.class.php) which is used to determine the online location of users.
+To ensure upwards-compatibility if the `IOnlineLocationPageHandler` interface changes, the [TOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/TOnlineLocationPageHandler.class.php) trait is used.
+The `IOnlineLocationPageHandler` interface requires two methods to be implemented:
+
+1. `getOnlineLocation(Page $page, UserOnline $user)` returns the textual description of the online location.
+ The language item for the user online locations should use the pattern `wcf.page.onlineLocation.{page identifier}`.
+1. `prepareOnlineLocation(Page $page, UserOnline $user)` is called for each user online before the `getOnlineLocation()` calls.
+ In this case, calling `prepareOnlineLocation()` first enables us to add all relevant person ids to the person runtime cache so that for all `getOnlineLocation()` calls combined, only one database query is necessary to fetch all person objects.
+
+---
+
+This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person.
+
+The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3).
+
--- /dev/null
+---
+title: CSS
+sidebar: sidebar
+permalink: view_css.html
+folder: view
+---
+
+## SCSS and CSS
+
+SCSS is a scripting language that features a syntax similar to CSS and compiles into native CSS at runtime. It provides many great additions to CSS such as declaration nesting and variables, it is recommended to read the [official guide](http://sass-lang.com/guide) to learn more.
+
+You can create `.scss` files containing only pure CSS code and it will work just fine, you are at no point required to write actual SCSS code.
+
+### File Location
+
+Please place your style files in a subdirectory of the `style/` directory of the target application or the Core's style directory, for example `style/layout/pageHeader.scss`.
+
+### Variables
+
+You can access variables with `$myVariable`, variable interpolation (variables inside strings) is accomplished with `#{$myVariable}`.
+
+#### Linking images
+
+Images used within a style must be located in the style's image folder. To get the folder name within the CSS the SCSS variable `#{$style_image_path}` can be used. The value will contain a trailing slash.
+
+## Media Breakpoints
+
+Media breakpoints instruct the browser to apply different CSS depending on the viewport dimensions, e.g. serving a desktop PC a different view than when viewed on a smartphone.
+
+```scss
+/* red background color for desktop pc */
+@include screen-lg {
+ body {
+ background-color: red;
+ }
+}
+
+/* green background color on smartphones and tablets */
+@include screen-md-down {
+ body {
+ background-color: green;
+ }
+}
+```
+
+### Available Breakpoints
+
+{% include callout.html content="Some very large smartphones, for example the Apple iPhone 7 Plus, do match the media query for `Tablets (portrait)` when viewed in landscape mode." type="info" %}
+
+| Name | Devices | `@media` equivalent |
+|-------|-------|-------|
+| `screen-xs` | Smartphones only | `(max-width: 544px)` |
+| `screen-sm` | Tablets (portrait) | `(min-width: 545px) and (max-width: 768px)` |
+| `screen-sm-down` | Tablets (portrait) and smartphones | `(max-width: 768px)` |
+| `screen-sm-up` | Tablets and desktop PC | `(min-width: 545px)` |
+| `screen-sm-md` | Tablets only | `(min-width: 545px) and (max-width: 1024px)` |
+| `screen-md` | Tablets (landscape) | `(min-width: 769px) and (max-width: 1024px)` |
+| `screen-md-down` | Smartphones and Tablets | `(max-width: 1024px)` |
+| `screen-md-up` | Tablets (landscape) and desktop PC | `(min-width: 769px)` |
+| `screen-lg` | Desktop PC | `(min-width: 1025px)` |
--- /dev/null
+---
+title: Languages
+sidebar: sidebar
+permalink: view_languages.html
+folder: view
+---
+
+WoltLab Suite offers full i18n support with its integrated language system,
+including but not limited to dynamic phrases using template scripting and the
+built-in support for right-to-left languages.
+
+Phrases are deployed using the [language][package_pip_language] package
+installation plugin, please also read the [naming conventions for language items](view_languages_naming-conventions.html).
+
+## Special Phrases
+
+### `wcf.date.dateFormat`
+
+{% include callout.html content="Many characters in the format have a special meaning and will be replaced with date fragments. If you want to include a literal character, you'll have to use the backslash `\` as an escape sequence to indicate that the character should be output as-is rather than being replaced. For example, `Y-m-d` will be output as `2018-03-30`, but `\Y-m-d` will result in `Y-03-30`." type="warning" %}
+
+_Defaults to `M jS Y`._
+
+The date format without time using PHP's format characters for the
+[`date()`](https://secure.php.net/manual/en/function.date.php) function. This
+value is also used inside the JavaScript implementation, where the characters
+are mapped to an equivalent representation.
+
+### `wcf.date.timeFormat`
+
+_Defaults to `g:i a`._
+
+The date format that is used to represent a time, but not a date. Please see the
+explanation on `wcf.date.dateFormat` to learn more about the format characters.
+
+### `wcf.date.firstDayOfTheWeek`
+
+_Defaults to `0`._
+
+Sets the first day of the week:
+* `0` - Sunday
+* `1` - Monday
+
+### `wcf.global.pageDirection` - RTL support
+
+_Defaults to `ltr`._
+
+Changing this value to `rtl` will reverse the page direction and enable the
+right-to-left support for phrases. Additionally, a special version of the
+stylesheet is loaded that contains all necessary adjustments for the reverse
+direction.
+
+{% include links.html %}
--- /dev/null
+---
+title: Language Naming Conventions
+sidebar: sidebar
+permalink: view_languages_naming-conventions.html
+folder: view
+parent: view_languages
+---
+
+This page contains general rules for naming language items and for their values.
+API-specific rules are listed on the relevant API page:
+
+- [Comments](php_api_comments.html#language-items)
+
+
+## Forms
+
+### Fields
+
+If you have an application `foo` and a database object `foo\data\bar\Bar` with a property `baz` that can be set via a form field, the name of the corresponding language item has to be `foo.bar.baz`.
+If you want to add an additional description below the field, use the language item `foo.bar.baz.description`.
+
+### Error Texts
+
+If an error of type `{error type}` for the previously mentioned form field occurs during validation, you have to use the language item `foo.bar.baz.error.{error type}` for the language item describing the error.
+
+Exception to this rule:
+There are several general error messages like `wcf.global.form.error.empty` that have to be used for general errors like an empty field that may not be empty to avoid duplication of the same error message text over and over again in different language items.
+
+#### Naming Conventions
+
+- If the entered text does not conform to some special rules, i.e. if the text is invalid, use `invalid` as error type.
+- If the entered text is required to be unique but is already used for another object, use `notUnique` as error type.
+
+
+## Confirmation messages
+
+If the language item for an action is `foo.bar.action`, the language item for the confirmation message has to be `foo.bar.action.confirmMessage` instead of `foo.bar.action.sure` which is still used by some older language items.
+
+### Type-Specific Deletion Confirmation Message
+
+#### German
+
+```
+{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} wirklich löschen?
+```
+
+Example:
+
+```
+{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Icon wirklich löschen?
+```
+
+#### English
+
+```
+Do you really want delete the {element type}?
+```
+
+Example:
+
+```
+Do you really want delete the icon?
+```
+
+### Object-Specific Deletion Confirmation Message
+
+#### German
+
+```
+{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} <span class="confirmationObject">{object name}</span> wirklich löschen?
+```
+
+Example:
+
+```
+{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Artikel <span class="confirmationObject">{$article->getTitle()}</span> wirklich löschen?
+```
+
+#### English
+
+```
+Do you really want to delete the {element type} <span class="confirmationObject">{object name}</span>?
+```
+
+Example:
+
+```
+Do you really want to delete the article <span class="confirmationObject">{$article->getTitle()}</span>?
+```
+
+
+## User Group Options
+
+### Comments
+
+#### German
+
+| group type | action | example permission name | language item |
+| ---------- | ------ | ----------------------- | ------------- |
+| user | adding | `user.foo.canAddComment` | `Kann Kommentare erstellen` |
+| user | deleting | `user.foo.canDeleteComment` | `Kann eigene Kommentare löschen` |
+| user | editing | `user.foo.canEditComment` | `Kann eigene Kommentare bearbeiten` |
+| moderator | deleting | `mod.foo.canDeleteComment` | `Kann Kommentare löschen` |
+| moderator | editing | `mod.foo.canEditComment` | `Kann Kommentare bearbeiten` |
+| moderator | moderating | `mod.foo.canModerateComment` | `Kann Kommentare moderieren` |
+
+#### English
+
+| group type | action | example permission name | language item |
+| ---------- | ------ | ----------------------- | ------------- |
+| user | adding | `user.foo.canAddComment` | `Can create comments` |
+| user | deleting | `user.foo.canDeleteComment` | `Can delete their comments` |
+| user | editing | `user.foo.canEditComment` | `Can edit their comments` |
+| moderator | deleting | `mod.foo.canDeleteComment` | `Can delete comments` |
+| moderator | editing | `mod.foo.canEditComment` | `Can edit comments` |
+| moderator | moderating | `mod.foo.canModerateComment` | `Can moderate comments` |
--- /dev/null
+---
+title: Template Plugins
+sidebar: sidebar
+permalink: view_template-plugins.html
+folder: view
+parent: view_templates
+---
+
+## <span class="label label-info">5.3+</span> `anchor`
+
+The `anchor` template plugin creates `a` HTML elements.
+The easiest way to use the template plugin is to pass it an instance of `ITitledLinkObject`:
+
+```smarty
+{anchor object=$object}
+```
+
+generates the same output as
+
+```smarty
+<a href="{$object->getLink()}">{$object->getTitle()}</a>
+```
+
+Instead of an `object` parameter, a `link` and `content` parameter can be used:
+
+```smarty
+{anchor link=$linkObject content=$content}
+```
+
+where `$linkObject` implements `ILinkableObject` and `$content` is either an object implementing `ITitledObject` or having a `__toString()` method or `$content` is a string or a number.
+
+The last special attribute is `append` whose contents are appended to the `href` attribute of the generated anchor element.
+
+All of the other attributes matching `~^[a-z]+([A-z]+)+$~`, expect for `href` which is disallowed, are added as attributes to the anchor element.
+
+If an `object` attribute is present, the object also implements `IPopoverObject` and if the return value of `IPopoverObject::getPopoverLinkClass()` is included in the `class` attribute of the `anchor` tag, `data-object-id` is automatically added.
+This functionality makes it easy to generate links with popover support.
+Instead of
+
+```smarty
+<a href="{$entry->getLink()}" class="blogEntryLink" data-object-id="{@$entry->entryID}">{$entry->subject}</a>
+```
+
+using
+
+```smarty
+{anchor object=$entry class='blogEntryLink'}
+```
+
+is sufficient if `Entry::getPopoverLinkClass()` returns `blogEntryLink`.
+
+## <span class="label label-info">5.3+</span> `anchorAttributes`
+
+`anchorAttributes` compliments the `StringUtil::getAnchorTagAttributes(string, bool): string` method.
+It allows to easily generate the necessary attributes for an anchor tag based off the destination URL.
+
+```smarty
+<a href="https://www.example.com" {anchorAttributes url='https://www.example.com' appendHref=false appendClassname=true isUgc=true}>
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `url` | destination URL |
+| `appendHref` | whether the `href` attribute should be generated; `true` by default |
+| `isUgc` | whether the `rel="ugc"` attribute should be generated; `false` by default |
+| `appendClassname` | whether the `class="externalURL"` attribute should be generated; `true` by default |
+
+## `append`
+
+If a string should be appended to the value of a variable, `append` can be used:
+
+```smarty
+{assign var=templateVariable value='newValue'}
+
+{$templateVariable} {* prints 'newValue *}
+
+{append var=templateVariable value='2'}
+
+{$templateVariable} {* now prints 'newValue2 *}
+```
+
+If the variables does not exist yet, `append` creates a new one with the given value.
+If `append` is used on an array as the variable, the value is appended to all elements of the array.
+
+
+## `assign`
+
+New template variables can be declared and new values can be assigned to existing template variables using `assign`:
+
+```smarty
+{assign var=templateVariable value='newValue'}
+
+{$templateVariable} {* prints 'newValue *}
+```
+
+
+## `capture`
+
+In some situations, `assign` is not sufficient to assign values to variables in templates if the value is complex.
+Instead, `capture` can be used:
+
+```smarty
+{capture var=templateVariable}
+ {if $foo}
+ <p>{$bar}</p>
+ {else}
+ <small>{$baz}</small>
+ {/if}
+{/capture}
+```
+
+
+## `concat`
+
+`concat` is a modifier used to concatenate multiple strings:
+
+```smarty
+{assign var=foo value='foo'}
+
+{assign var=templateVariable value='bar'|concat:$foo}
+
+{$templateVariable} {* prints 'foobar *}
+```
+
+
+## `counter`
+
+`counter` can be used to generate and optionally print a counter:
+
+```smarty
+{counter name=fooCounter print=true} {* prints '1' *}
+
+{counter name=fooCounter print=true} {* prints '2' now *}
+
+{counter name=fooCounter} {* prints nothing, but counter value is '3' now internally *}
+
+{counter name=fooCounter print=true} {* prints '4' *}
+```
+
+Counter supports the following attributes:
+
+| Attribute | Description |
+|-----------|-------------|
+| `assign` | optional name of the template variable the current counter value is assigned to |
+| `direction` | counting direction, either `up` or `down`; `up` by default |
+| `name` | name of the counter, relevant if multiple counters are used simultaneously |
+| `print` | if `true`, the current counter value is printed; `false` by default |
+| `skip` | positive counting increment; `1` by default |
+| `start` | start counter value; `1` by default |
+
+
+## <span class="label label-info">5.4+</span> `csrfToken`
+
+`{csrfToken}` prints out the session's CSRF token (“Security Token”).
+
+```smarty
+<form action="{link controller="Foo"}{/link}" method="post">
+ {* snip *}
+
+ {csrfToken}
+</form>
+```
+
+The `{csrfToken}` template plugin supports a `type` parameter.
+Specifying this parameter might be required in rare situations.
+Please [check the implementation](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/template/plugin/CsrfTokenFunctionTemplatePlugin.class.php) for details.
+
+## `currency`
+
+`currency` is a modifier used to format currency values with two decimals using language dependent thousands separators and decimal point:
+
+```smarty
+{assign var=currencyValue value=12.345}
+
+{$currencyValue|currency} {* prints '12.34' *}
+```
+
+
+## `cycle`
+
+`cycle` can be used to cycle between different values:
+
+```smarty
+{cycle name=fooCycle values='bar,baz'} {* prints 'bar' *}
+
+{cycle name=fooCycle} {* prints 'baz' *}
+
+{cycle name=fooCycle advance=false} {* prints 'baz' again *}
+
+{cycle name=fooCycle} {* prints 'bar' *}
+```
+
+{% include callout.html content="The values attribute only has to be present for the first call. If `cycle` is used in a loop, the presence of the same values in consecutive calls has no effect. Only once the values change, the cycle is reset." type="info" %}
+
+| Attribute | Description |
+|-----------|-------------|
+| `advance` | if `true`, the current cycle value is advanced to the next value; `true` by default |
+| `assign` | optional name of the template variable the current cycle value is assigned to; if used, `print` is set to `false` |
+| `delimiter` | delimiter between the different cycle values; `,` by default |
+| `name` | name of the cycle, relevant if multiple cycles are used simultaneously |
+| `print` | if `true`, the current cycle value is printed, `false` by default |
+| `reset` | if `true`, the current cycle value is set to the first value, `false` by default |
+| `values` | string containing the different cycles values, also see `delimiter` |
+
+
+## `date`
+
+`date` generated a formatted date using `wcf\util\DateUtil::format()` with `DateUtil::DATE_FORMAT` internally.
+
+```smarty
+{$timestamp|date}
+```
+
+
+## <span class="label label-info">3.1+</span> `dateInterval`
+
+`dateInterval` calculates the difference between two unix timestamps and generated a textual date interval.
+
+```smarty
+{dateInterval start=$startTimestamp end=$endTimestamp full=true format='sentence'}
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `end` | end of the time interval; current timestamp by default (though either `start` or `end` has to be set) |
+| `format` | output format, either `default`, `sentence`, or `plain`; defaults to `default`, see `wcf\util\DateUtil::FORMAT_*` constants |
+| `full` | if `true`, full difference in minutes is shown; if `false`, only the longest time interval is shown; `false` by default |
+| `start` | start of the time interval; current timestamp by default (though either `start` or `end` has to be set) |
+
+
+## `encodeJS`
+
+`encodeJS` encodes a string to be used as a single-quoted string in JavaScript by replacing `\\` with `\\\\`, `'` with `\'`, linebreaks with `\n`, and `/` with `\/`.
+
+```smarty
+<script>
+ var foo = '{@$foo|encodeJS}';
+</script>
+```
+
+
+## `encodeJSON`
+
+`encodeJSON` encodes a JSON string to be used as a single-quoted string in JavaScript by replacing `\\` with `\\\\`, `'` with `'`, linebreaks with `\n`, and `/` with `\/`.
+Additionally, `htmlspecialchars` is applied to the string.
+
+```smarty
+'{@$foo|encodeJSON}'
+```
+
+
+## `escapeCDATA`
+
+`escapeCDATA` encodes a string to be used in a `CDATA` element by replacing `]]>` with `]]]]><![CDATA[>`.
+
+```smarty
+<![CDATA[{@$foo|encodeCDATA}]]>
+```
+
+
+## `event`
+
+`event` provides extension points in templates that [template listeners](package_pip_template-listener.html) can use.
+
+```smarty
+{event name='foo'}
+```
+
+
+## `fetch`
+
+`fetch` fetches the contents of a file using `file_get_contents`.
+
+```smarty
+{fetch file='foo.html'} {* prints the contents of `foo.html` *}
+
+{fetch file='bar.html' assign=bar} {* assigns the contents of `foo.html` to `$bar`; does not print the contents *}
+```
+
+
+## `filesizeBinary`
+
+`filesizeBinary` formats the filesize using binary filesize (in bytes).
+
+```smarty
+{$filesize|filesizeBinary}
+```
+
+
+## `filesize`
+
+`filesize` formats the filesize using filesize (in bytes).
+
+```smarty
+{$filesize|filesize}
+```
+
+
+## `hascontent`
+
+In many cases, conditional statements can be used to determine if a certain section of a template is shown:
+
+```smarty
+{if $foo === 'bar'}
+ only shown if $foo is bar
+{/if}
+```
+
+In some situations, however, such conditional statements are not sufficient.
+One prominent example is a template event:
+
+```smarty
+{if $foo === 'bar'}
+ <ul>
+ {if $foo === 'bar'}
+ <li>Bar</li>
+ {/if}
+
+ {event name='listItems'}
+ </li>
+{/if}
+```
+
+In this example, if `$foo !== 'bar'`, the list will not be shown, regardless of the additional template code provided by template listeners.
+In such a situation, `hascontent` has to be used:
+
+```smarty
+{hascontent}
+ <ul>
+ {content}
+ {if $foo === 'bar'}
+ <li>Bar</li>
+ {/if}
+
+ {event name='listItems'}
+ {/content}
+ </ul>
+{/hascontent}
+```
+
+If the part of the template wrapped in the `content` tags has any (trimmed) content, the part of the template wrapped by `hascontent` tags is shown (including the part wrapped by the `content` tags), otherwise nothing is shown.
+Thus, this construct avoids an empty list compared to the `if` solution above.
+
+Like `foreach`, `hascontent` also supports an `else` part:
+
+```smarty
+{hascontent}
+ <ul>
+ {content}
+ {* … *}
+ {/content}
+ </ul>
+{hascontentelse}
+ no list
+{/hascontent}
+```
+
+
+## `htmlCheckboxes`
+
+`htmlCheckboxes` generates a list of HTML checkboxes.
+
+```smarty
+{htmlCheckboxes name=foo options=$fooOptions selected=$currentFoo}
+
+{htmlCheckboxes name=bar output=$barLabels values=$barValues selected=$currentBar}
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| <span class="label label-info">5.2+</span> `disabled` | if `true`, all checkboxes are disabled |
+| `disableEncoding` | if `true`, the values are not passed through `wcf\util\StringUtil::encodeHTML()`; `false` by default |
+| `name` | `name` attribute of the `input` checkbox element |
+| `output` | array used as keys and values for `options` if present; not present by default |
+| `options` | array selectable options with the key used as `value` attribute and the value as the checkbox label |
+| `selected` | current selected value(s) |
+| `separator` | separator between the different checkboxes in the generated output; empty string by default |
+| `values` | array with values used in combination with `output`, where `output` is only used as keys for `options` |
+
+
+## `htmlOptions`
+
+`htmlOptions` generates an `select` HTML element.
+
+```smarty
+{htmlOptions name='foo' options=$options selected=$selected}
+
+<select name="bar">
+ <option value=""{if !$selected} selected{/if}>{lang}foo.bar.default{/lang}</option>
+ {htmlOptions options=$options selected=$selected} {* no `name` attribute *}
+</select>
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `disableEncoding` | if `true`, the values are not passed through `wcf\util\StringUtil::encodeHTML()`; `false` by default |
+| `object` | optional instance of `wcf\data\DatabaseObjectList` that provides the selectable options (overwrites `options` attribute internally) |
+| `name` | `name` attribute of the `select` element; if not present, only the <strong>contents</strong> of the `select` element are printed |
+| `output` | array used as keys and values for `options` if present; not present by default |
+| `values` | array with values used in combination with `output`, where `output` is only used as keys for `options` |
+| `options` | array selectable options with the key used as `value` attribute and the value as the option label; if a value is an array, an `optgroup` is generated with the array key as the `optgroup` label |
+| `selected` | current selected value(s) |
+
+All additional attributes are added as attributes of the `select` HTML element.
+
+
+## `implode`
+
+`implodes` transforms an array into a string and prints it.
+
+```smarty
+{implode from=$array key=key item=item glue=";"}{$key}: {$value}{/implode}
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `from` | array with the imploded values |
+| `glue` | separator between the different array values; `', '` by default |
+| `item` | template variable name where the current array value is stored during the iteration |
+| `key` | optional template variable name where the current array key is stored during the iteration |
+
+
+## <span class="label label-info">5.2+</span> `ipSearch`
+
+`ipSearch` generates a link to search for an IP address.
+
+```smarty
+{"127.0.0.1"|ipSearch}
+```
+
+
+## <span class="label label-info">3.0+</span> `js`
+
+`js` generates script tags based on whether `ENABLE_DEBUG_MODE` and `VISITOR_USE_TINY_BUILD` are enabled.
+
+```smarty
+{js application='wbb' file='WBB'} {* generates 'http://example.com/js/WBB.js' *}
+
+{js application='wcf' file='WCF.Like' bundle='WCF.Combined'}
+ {* generates 'http://example.com/wcf/js/WCF.Like.js' if ENABLE_DEBUG_MODE=1 *}
+ {* generates 'http://example.com/wcf/js/WCF.Combined.min.js' if ENABLE_DEBUG_MODE=0 *}
+
+{js application='wcf' lib='jquery'}
+ {* generates 'http://example.com/wcf/js/3rdParty/jquery.js' *}
+
+{js application='wcf' lib='jquery-ui' file='awesomeWidget'}
+ {* generates 'http://example.com/wcf/js/3rdParty/jquery-ui/awesomeWidget.js' *}
+
+{js application='wcf' file='WCF.Like' bundle='WCF.Combined' hasTiny=true}
+ {* generates 'http://example.com/wcf/js/WCF.Like.js' if ENABLE_DEBUG_MODE=1 *}
+ {* generates 'http://example.com/wcf/js/WCF.Combined.min.js' (ENABLE_DEBUG_MODE=0 *}
+ {* generates 'http://example.com/wcf/js/WCF.Combined.tiny.min.js' if ENABLE_DEBUG_MODE=0 and VISITOR_USE_TINY_BUILD=1 *}
+```
+
+
+## <span class="label label-info">5.3+</span> `jslang`
+
+`jslang` works like [`lang`](#lang) with the difference that the resulting string is automatically passed through [`encodeJS`](#encodejs).
+
+```smarty
+require(['Language', /* … */], function(Language, /* … */) {
+ Language.addObject({
+ 'app.foo.bar': '{jslang}app.foo.bar{/jslang}',
+ });
+
+ // …
+});
+```
+
+
+## `lang`
+
+`lang` replaces a language items with its value.
+
+```smarty
+{lang}foo.bar.baz{/lang}
+
+{lang __literal=true}foo.bar.baz{/lang}
+
+{lang foo='baz'}foo.bar.baz{/lang}
+
+{lang}foo.bar.baz.{$action}{/lang}
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `__encode` | if `true`, the output will be passed through `StringUtil::encodeHTML()` |
+| `__literal` | if `true`, template variables will not resolved but printed as they are in the language item; `false` by default |
+| `__optional` | if `true` and the language item does not exist, an empty string is printed; `false` by default |
+
+All additional attributes are available when parsing the language item.
+
+
+## `language`
+
+`language` replaces a language items with its value.
+If the template variable `__language` exists, this language object will be used instead of `WCF::getLanguage()`.
+This modifier is useful when assigning the value directly to a variable.
+
+```smarty
+{$languageItem|language}
+
+{assign var=foo value=$languageItem|language}
+```
+
+
+## `link`
+
+`link` generates internal links using `LinkHandler`.
+
+```smarty
+<a href="{link controller='FooList' application='bar'}param1=2¶m2=A{/link}">Foo</a>
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `application` | abbreviation of the application the controller belongs to; `wcf` by default |
+| `controller` | name of the controller; if not present, the landing page is linked in the frontend and the index page in the ACP |
+| `encode` | if `true`, the generated link is passed through `wcf\util\StringUtil::encodeHTML()`; `true` by default |
+| `isEmail` | sets `encode=false` and forces links to link to the frontend |
+
+Additional attributes are passed to `LinkHandler::getLink()`.
+
+
+## `newlineToBreak`
+
+`newlineToBreak` transforms newlines into HTML `<br>` elements after encoding the content via `wcf\util\StringUtil::encodeHTML()`.
+
+```smarty
+{$foo|newlineToBreak}
+```
+
+
+## <span class="label label-info">3.0+</span> `page`
+
+`page` generates an internal link to a CMS page.
+
+```smarty
+{page}com.woltlab.wcf.CookiePolicy{/page}
+
+{page pageID=1}{/page}
+
+{page language='de'}com.woltlab.wcf.CookiePolicy{/page}
+
+{page languageID=2}com.woltlab.wcf.CookiePolicy{/page}
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `pageID` | unique id of the page (cannot be used together with a page identifier as value) |
+| `languageID` | id of the page language (cannot be used together with `language`) |
+| `language` | language code of the page language (cannot be used together with `languageID`) |
+
+
+## `pages`
+
+`pages` generates a pagination.
+
+```smarty
+{pages controller='FooList' link="pageNo=%d" print=true assign=pagesLinks} {* prints pagination *}
+
+{@$pagesLinks} {* prints same pagination again *}
+```
+
+| Attribute | Description |
+|-----------|-------------|
+| `assign` | optional name of the template variable the pagination is assigned to |
+| `controller` | controller name of the generated links |
+| `link` | additional link parameter where `%d` will be replaced with the relevant page number |
+| `pages` | maximum number of of pages; by default, the template variable `$pages` is used |
+| `print` | if `false` and `assign=true`, the pagination is not printed |
+| `application`, `id`, `object`, `title` | additional parameters passed to `LinkHandler::getLink()` to generate page links |
+
+
+## `plainTime`
+
+`plainTime` formats a timestamp to include year, month, day, hour, and minutes.
+The exact formatting depends on the current language (via the language items `wcf.date.dateTimeFormat`, `wcf.date.dateFormat`, and `wcf.date.timeFormat`).
+
+```smarty
+{$timestamp|plainTime}
+```
+
+
+## <span class="label label-info">5.3+</span> `plural`
+
+`plural` allows to easily select the correct plural form of a phrase based on a given `value`.
+The pluralization logic follows the [Unicode Language Plural Rules](https://unicode-org.github.io/cldr-staging/charts/37/supplemental/language_plural_rules.html) for cardinal numbers.
+
+The `#` placeholder within the resulting phrase is replaced by the `value`.
+It is automatically formatted using `StringUtil::formatNumeric`.
+
+
+
+English:
+
+Note the use of `1` if the number (`#`) is not used within the phrase and the use of `one` otherwise.
+They are equivalent for English, but following this rule generalizes better to other languages, helping the translator.
+```smarty
+{assign var=numberOfWorlds value=2}
+<h1>Hello {plural value=$numberOfWorlds 1='World' other='Worlds'}!</h1>
+<p>There {plural value=$numberOfWorlds 1='is one world' other='are # worlds'}!</p>
+<p>There {plural value=$numberOfWorlds one='is # world' other='are # worlds'}!</p>
+```
+
+German:
+```smarty
+{assign var=numberOfWorlds value=2}
+<h1>Hallo {plural value=$numberOfWorlds 1='Welt' other='Welten'}!</h1>
+<p>Es gibt {plural value=$numberOfWorlds 1='eine Welt' other='# Welten'}!</p>
+<p>Es gibt {plural value=$numberOfWorlds one='# Welt' other='# Welten'}!</p>
+```
+
+Romanian:
+
+Note the additional use of `few` which is not required in English or German.
+```smarty
+{assign var=numberOfWorlds value=2}
+<h1>Salut {plural value=$numberOfWorlds 1='lume' other='lumi'}!</h1>
+<p>Există {plural value=$numberOfWorlds 1='o lume' few='# lumi' other='# de lumi'}!</p>
+<p>Există {plural value=$numberOfWorlds one='# lume' few='# lumi' other='# de lumi'}!</p>
+```
+
+Russian:
+
+Note the difference between `1` (exactly `1`) and `one` (ending in `1`, except ending in `11`).
+```smarty
+{assign var=numberOfWorlds value=2}
+<h1>Привет {plural value=$numberOfWorld 1='мир' other='миры'}!</h1>
+<p>Есть {plural value=$numberOfWorlds 1='мир' one='# мир' few='# мира' many='# миров' other='# миров'}!</p>
+```
+
+
+| Attribute | Description |
+|-----------|-------------|
+| value | The value that is used to select the proper phrase. |
+| other | The phrase that is used when no other selector matches. |
+| Any Category Name | The phrase that is used when `value` belongs to the named category. Available categories depend on the language. |
+| Any Integer | The phrase that is used when `value` is that exact integer. |
+
+## `prepend`
+
+If a string should be prepended to the value of a variable, `prepend` can be used:
+
+```smarty
+{assign var=templateVariable value='newValue'}
+
+{$templateVariable} {* prints 'newValue *}
+
+{prepend var=templateVariable value='2'}
+
+{$templateVariable} {* now prints '2newValue' *}
+```
+
+If the variables does not exist yet, `prepend` creates a new one with the given value.
+If `prepend` is used on an array as the variable, the value is prepended to all elements of the array.
+
+
+## `shortUnit`
+
+`shortUnit` shortens numbers larger than 1000 by using unit suffixes:
+
+```smarty
+{10000|shortUnit} {* prints 10k *}
+{5400000|shortUnit} {* prints 5.4M *}
+```
+
+
+## `smallpages`
+
+`smallpages` generates a smaller version of `pages` by using adding the `small` CSS class to the generated `<nav>` element and only showing 7 instead of 9 links.
+
+
+## `tableWordwrap`
+
+`tableWordwrap` inserts zero width spaces every 30 characters in words longer than 30 characters.
+
+```smarty
+{$foo|tableWordwrap}
+```
+
+
+## `time`
+
+`time` generates an HTML `time` elements based on a timestamp that shows a relative time or the absolute time if the timestamp more than six days ago.
+
+```smarty
+{$timestamp|time} {* prints a '<time>' element *}
+```
+
+
+## `truncate`
+
+`truncate` truncates a long string into a shorter one:
+
+```smarty
+{$foo|truncate:35}
+
+{$foo|truncate:35:'_':true}
+```
+
+
+| Parameter Number | Description |
+|-----------|-------------|
+| 0 | truncated string |
+| 1 | truncated length; `80` by default |
+| 2 | ellipsis symbol; `wcf\util\StringUtil::HELLIP` by default |
+| 3 | if `true`, words can be broken up in the middle; `false` by default |
+
+
+## <span class="label label-info">5.3+</span> `user`
+
+`user` generates links to user profiles.
+The mandatory `object` parameter requires an instances of `UserProfile`.
+The optional `type` parameter is responsible for what the generated link contains:
+
+- `type='default'` (also applies if no `type` is given) outputs the formatted username relying on the “User Marking” setting of the relevant user group.
+ Additionally, the user popover card will be shown when hovering over the generated link.
+- `type='plain'` outputs the username without additional formatting.
+- `type='avatar(\d+)'` outputs the user’s avatar in the specified size, i.e., `avatar48` outputs the avatar with a width and height of 48 pixels.
+
+The last special attribute is `append` whose contents are appended to the `href` attribute of the generated anchor element.
+
+All of the other attributes matching `~^[a-z]+([A-z]+)+$~`, except for `href` which may not be added, are added as attributes to the anchor element.
+
+Examples:
+
+```smarty
+{user object=$user}
+```
+
+generates
+
+```smarty
+<a href="{$user->getLink()}" data-object-id="{$user->userID}" class="userLink">{@$user->getFormattedUsername()}</a>
+```
+
+and
+
+```smarty
+{user object=$user type='avatar48' foo='bar'}
+```
+
+generates
+
+```smarty
+<a href="{$user->getLink()}" foo="bar">{@$object->getAvatar()->getImageTag(48)}</a>
+```
--- /dev/null
+---
+title: Templates
+sidebar: sidebar
+permalink: view_templates.html
+folder: view
+parent: view
+---
+
+Templates are responsible for the output a user sees when requesting a page (while the PHP code is responsible for providing the data that will be shown).
+Templates are text files with `.tpl` as the file extension.
+WoltLab Suite Core compiles the template files once into a PHP file that is executed when a user requests the page.
+In subsequent request, as the PHP file containing the compiled template already exists, compiling the template is not necessary anymore.
+
+
+## Template Types and Conventions
+
+WoltLab Suite Core supports two types of templates:
+frontend templates (or simply *templates*) and backend templates (*ACP templates*).
+Each type of template is only available in its respective domain, thus frontend templates cannot be included or used in the ACP and vice versa.
+
+For pages and forms, the name of the template matches the unqualified name of the PHP class except for the `Page` or `Form` suffix:
+
+- `RegisterForm.class.php` → `register.tpl`
+- `UserPage.class.php` → `user.tpl`
+
+If you follow this convention, WoltLab Suite Core will automatically determine the template name so that you do not have to explicitly set it.
+
+{% include callout.html content="For forms that handle creating and editing objects, in general, there are two form classes: `FooAddForm` and `FooEditForm`. WoltLab Suite Core, however, generally only uses one template `fooAdd.tpl` and the template variable `$action` to distinguish between creating a new object (`$action = 'add'`) and editing an existing object (`$action = 'edit'`) as the differences between templates for adding and editing an object are minimal." type="info" %}
+
+
+
+## Installing Templates
+
+Templates and ACP templates are installed by two different package installation plugins:
+the [template PIP](package_pip_template.html) and the [ACP template PIP](package_pip_acp-template.html).
+More information about installing templates can be found on those pages.
+
+
+## Base Templates
+
+### Frontend
+
+```smarty
+{include file='header'}
+
+{* content *}
+
+{include file='footer'}
+```
+
+### Backend
+
+```smarty
+{include file='header' pageTitle='foo.bar.baz'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">Title</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ {* your default content header navigation buttons *}
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{* content *}
+
+{include file='footer'}
+```
+
+`foo.bar.baz` is the language item that contains the title of the page.
+
+
+## Common Template Components
+
+### Forms
+
+{% include callout.html content="For new forms, use the new [form builder API](php_api_form_builder.html) introduced with WoltLab Suite 5.2." type="info" %}
+
+```smarty
+<form method="post" action="{link controller='FooBar'}{/link}">
+ <div class="section">
+ <dl{if $errorField == 'baz'} class="formError"{/if}>
+ <dt><label for="baz">{lang}foo.bar.baz{/lang}</label></dt>
+ <dd>
+ <input type="text" id="baz" name="baz" value="{$baz}" class="long" required autofocus>
+ {if $errorField == 'baz'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}foo.bar.baz.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl>
+ <dt><label for="bar">{lang}foo.bar.bar{/lang}</label></dt>
+ <dd>
+ <textarea name="bar" id="bar" cols="40" rows="10">{$bar}</textarea>
+ {if $errorField == 'bar'}
+ <small class="innerError">{lang}foo.bar.bar.error.{@$errorType}{/lang}</small>
+ {/if}
+ </dd>
+ </dl>
+
+ {* other fields *}
+
+ {event name='dataFields'}
+ </div>
+
+ {* other sections *}
+
+ {event name='sections'}
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ {csrfToken}
+ </div>
+</form>
+```
+
+### Tab Menus
+
+```smarty
+<div class="section tabMenuContainer">
+ <nav class="tabMenu">
+ <ul>
+ <li><a href="{@$__wcf->getAnchor('tab1')}">Tab 1</a></li>
+ <li><a href="{@$__wcf->getAnchor('tab2')}">Tab 2</a></li>
+
+ {event name='tabMenuTabs'}
+ </ul>
+ </nav>
+
+ <div id="tab1" class="tabMenuContent">
+ <div class="section">
+ {* contents of first tab *}
+ </div>
+ </div>
+
+ <div id="tab2" class="tabMenuContainer tabMenuContent">
+ <nav class="menu">
+ <ul>
+ <li><a href="{@$__wcf->getAnchor('tab2A')}">Tab 2A</a></li>
+ <li><a href="{@$__wcf->getAnchor('tab2B')}">Tab 2B</a></li>
+
+ {event name='tabMenuTab2Subtabs'}
+ </ul>
+ </nav>
+
+ <div id="tab2A" class="tabMenuContent">
+ <div class="section">
+ {* contents of first subtab for second tab *}
+ </div>
+ </div>
+
+ <div id="tab2B" class="tabMenuContent">
+ <div class="section">
+ {* contents of second subtab for second tab *}
+ </div>
+ </div>
+
+ {event name='tabMenuTab2Contents'}
+ </div>
+
+ {event name='tabMenuContents'}
+</div>
+```
+
+
+## Template Scripting
+
+### Template Variables
+
+Template variables can be assigned via `WCF::getTPL()->assign('foo', 'bar')` and accessed in templates via `$foo`:
+
+- `{$foo}` will result in the contents of `$foo` to be passed to `StringUtil::encodeHTML()` before being printed.
+- `{#$foo}` will result in the contents of `$foo` to be passed to `StringUtil::formatNumeric()` before being printed.
+ Thus, this method is relevant when printing numbers and having them formatted correctly according the the user’s language.
+- `{@$foo}` will result in the contents of `$foo` to be printed directly.
+ In general, this method should not be used for user-generated input.
+
+Multiple template variables can be assigned by passing an array:
+
+```php
+WCF::getTPL()->assign([
+ 'foo' => 'bar',
+ 'baz' => false
+]);
+```
+
+#### Modifiers
+
+If you want to call a function on a variable, you can use the modifier syntax:
+`{@$foo|trim}`, for example, results in the trimmed contents of `$foo` to be printed.
+
+#### System Template Variable
+
+The template variable `$tpl` is automatically assigned and is an array containing different data:
+
+- `$tpl[get]` contains `$_GET`.
+- `$tpl[post]` contains `$_POST`.
+- `$tpl[cookie]` contains `$_COOKIE`.
+- `$tpl[server]` contains `$_SERVER`.
+- `$tpl[env]` contains `$_ENV`.
+- `$tpl[now]` contains `TIME_NOW` (current timestamp).
+
+Furthermore, the following template variables are also automatically assigned:
+
+- `$__wcf` contains the `WCF` object (or `WCFACP` object in the backend).
+
+### Comments
+
+Comments are wrapped in `{*` and `*}` and can span multiple lines:
+
+```smarty
+{* some
+ comment *}
+```
+
+{% include callout.html content="The template compiler discards the comments, so that they not included in the compiled template." type="info" %}
+
+### Conditions
+
+Conditions follow a similar syntax to PHP code:
+
+```smarty
+{if $foo === 'bar'}
+ foo is bar
+{elseif $foo === 'baz'}
+ foo is baz
+{else}
+ foo is neither bar nor baz
+{/if}
+```
+
+The supported operators in conditions are `===`, `!==`, `==`, `!=`, `<=`, `<`, `>=`, `>`, `||`, `&&`, `!`, and `=`.
+
+More examples:
+
+````smarty
+{if $bar|isset}…{/if}
+
+{if $bar|count > 3 && $bar|count < 100}…{/if}
+````
+
+### Foreach Loops
+
+Foreach loops allow to iterate over arrays or iterable objects:
+
+```smarty
+<ul>
+ {foreach from=$array key=key item=value}
+ <li>{$key}: {$value}</li>
+ {/foreach}
+</ul>
+```
+
+While the `from` attribute containing the iterated structure and the `item` attribute containg the current value are mandatory, the `key` attribute is optional.
+If the foreach loop has a name assigned to it via the `name` attribute, the `$tpl` template variable provides additional data about the loop:
+
+```smarty
+<ul>
+ {foreach from=$array key=key item=value name=foo}
+ {if $tpl[foreach][foo][first]}
+ something special for the first iteration
+ {elseif $tpl[foreach][foo][last]}
+ something special for the last iteration
+ {/if}
+
+ <li>iteration {#$tpl[foreach][foo][iteration]+1} out of {#$tpl[foreach][foo][total]} {$key}: {$value}</li>
+ {/foreach}
+</ul>
+```
+
+In contrast to PHP’s foreach loop, templates also support `foreachelse`:
+
+```smarty
+{foreach from=$array item=value}
+ …
+{foreachelse}
+ there is nothing to iterate over
+{/foreach}
+```
+
+### Including Other Templates
+
+To include template named `foo` from the same domain (frontend/backend), you can use
+
+```smarty
+{include file='foo'}
+```
+
+If the template belongs to an application, you have to specify that application using the `application` attribute:
+
+```smarty
+{include file='foo' application='app'}
+```
+
+Additional template variables can be passed to the included template as additional attributes:
+
+```smarty
+{include file='foo' application='app' var1='foo1' var2='foo2'}
+```
+
+### Template Plugins
+
+An overview of all available template plugins can be found [here](view_template-plugins.html).
+++ /dev/null
----
-title: WoltLab Suite 5.3 Documentation
-sidebar: sidebar
-permalink: index.html
-toc: false
----
-
-## Introduction
-
-This documentation explains the basic API functionality and the creation of own packages. It is expected that you are somewhat experienced with [PHP](https://en.wikipedia.org/wiki/PHP), [Object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) and [MySQL](https://en.wikipedia.org/wiki/MySQL).
-
-Head over to the [quick start tutorial][getting-started_quick-start] to learn more.
-
-## About WoltLab Suite 5.3
-
-The [WoltLab Suite Core](https://github.com/WoltLab/WCF) as well as most of the other packages are available on [github.com/WoltLab/](https://github.com/WoltLab) and are licensed under the terms of the [GNU Lesser General Public License 2.1](https://github.com/WoltLab/WCF/blob/master/LICENSE).
-
-You can edit this documentation by visiting the edit link on each page, it is also available on [GitHub](https://github.com/WoltLab/woltlab.github.io).
-
-{% include links.html %}
+++ /dev/null
----
-title: Creating a simple package
-sidebar: sidebar
-permalink: getting-started_quick-start.html
-folder: getting-started
----
-
-## Setup and Requirements
-
-This guide will help you to create a simple package that provides a simple test
-page. It is nothing too fancy, but you can use it as the foundation for your
-next project.
-
-There are some requirements you should met before starting:
-
-- Text editor with syntax highlighting for PHP, [Notepad++](https://notepad-plus-plus.org/) is a solid pick
- - `*.php` and `*.tpl` should be encoded with ANSI/ASCII
- - `*.xml` are always encoded with UTF-8, but omit the BOM (byte-order-mark)
- - Use tabs instead of spaces to indent lines
- - It is recommended to set the tab width to `8` spaces, this is used in the entire software and will ease reading the source files
-- An active installation of WoltLab Suite 3
-- An application to create `*.tar` archives, e.g. [7-Zip](http://www.7-zip.org/) on Windows
-
-## The package.xml File
-
-We want to create a simple page that will display the sentence "Hello World" embedded
-into the application frame. Create an empty directory in the workspace of your choice
-to start with.
-
-Create a new file called `package.xml` and insert the code below:
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<package xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/package.xsd" name="com.example.test">
- <packageinformation>
- <!-- com.example.test -->
- <packagename>Simple Package</packagename>
- <packagedescription>A simple package to demonstrate the package system of WoltLab Suite Core</packagedescription>
- <version>1.0.0</version>
- <date>2019-04-28</date>
- </packageinformation>
- <authorinformation>
- <author>Your Name</author>
- <authorurl>http://www.example.com</authorurl>
- </authorinformation>
- <excludedpackages>
- <excludedpackage version="6.0.0 Alpha 1">com.woltlab.wcf</excludedpackage>
- </excludedpackages>
- <instructions type="install">
- <instruction type="file" />
- <instruction type="template" />
- <instruction type="page" />
- </instructions>
-</package>
-```
-
-There is an [entire chapter][package_package-xml] on the package system that explains what the code above
-does and how you can adjust it to fit your needs. For now we'll keep it as it is.
-
-## The PHP Class
-
-The next step is to create the PHP class which will serve our page:
-
-1. Create the directory `files` in the same directory where `package.xml` is located
-2. Open `files` and create the directory `lib`
-3. Open `lib` and create the directory `page`
-4. Within the directory `page`, please create the file `TestPage.class.php`
-
-Copy and paste the following code into the `TestPage.class.php`:
-
-```php
-<?php
-namespace wcf\page;
-use wcf\system\WCF;
-
-/**
- * A simple test page for demonstration purposes.
- *
- * @author YOUR NAME
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- */
-class TestPage extends AbstractPage {
- /**
- * @var string
- */
- protected $greet = '';
-
- /**
- * @inheritDoc
- */
- public function readParameters() {
- parent::readParameters();
-
- if (isset($_GET['greet'])) $this->greet = $_GET['greet'];
- }
-
- /**
- * @inheritDoc
- */
- public function readData() {
- parent::readData();
-
- if (empty($this->greet)) {
- $this->greet = 'World';
- }
- }
-
- /**
- * @inheritDoc
- */
- public function assignVariables() {
- parent::assignVariables();
-
- WCF::getTPL()->assign([
- 'greet' => $this->greet
- ]);
- }
-}
-
-```
-
-The class inherits from [wcf\page\AbstractPage](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/page/AbstractPage.class.php), the default implementation of pages without form controls. It
-defines quite a few methods that will be automatically invoked in a specific order, for example `readParameters()` before `readData()` and finally `assignVariables()` to pass arbitrary values to the template.
-
-The property `$greet` is defined as `World`, but can optionally be populated through a GET variable (`index.php?test/&greet=You` would output `Hello You!`). This extra code illustrates the separation of data
-processing that takes place within all sort of pages, where all user-supplied data is read from within a single method. It helps organizing the code, but most of all it enforces a clean class logic that does not
-start reading user input at random places, including the risk to only escape the input of variable `$_GET['foo']` 4 out of 5 times.
-
-Reading and processing the data is only half the story, now we need a template to display the actual content for our page. You don't need to specify it yourself, it will be automatically guessed based on your
-namespace and class name, you can [read more about it later](#appendixTemplateGuessing).
-
-Last but not least, you must not include the closing PHP tag `?>` at the end, it can cause PHP to break on whitespaces and is not required at all.
-
-## The Template
-
-Navigate back to the root directory of your package until you see both the `files` directory and the `package.xml`. Now create a directory called `templates`, open it and create the file `test.tpl`.
-
-```smarty
-{include file='header'}
-
-<div class="section">
- Hello {$greet}!
-</div>
-
-{include file='footer'}
-```
-
-Templates are a mixture of HTML and Smarty-like template scripting to overcome the static nature of raw HTML. The above code will display the phrase `Hello World!` in the application frame, just as any other
-page would render. The included templates `header` and `footer` are responsible for the majority of the overall page functionality, but offer a whole lot of customization abilities to influence their behavior and appearance.
-
-## The Page Definition
-
-The package now contains the PHP class and the matching template, but it is still missing the page definition. Please create the file `page.xml` in your project's root directory, thus on the same level as the `package.xml`.
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/page.xsd">
- <import>
- <page identifier="com.example.test.Test">
- <controller>wcf\page\TestPage</controller>
- <name language="en">Test Page</name>
- <pageType>system</pageType>
- </page>
- </import>
-</data>
-```
-
-You can provide a lot more data for a page, including logical nesting and dedicated handler classes for display in menus.
-
-## Building the Package
-
-If you have followed the above guidelines carefully, your package directory should now look like this:
-
-```
-├── files
-│ └── lib
-│ ├── page
-│ │ ├── TestPage.class.php
-├── package.xml
-├── page.xml
-├── templates
-│ └── test.tpl
-```
-
-Both files and templates are archive-based package components, that deploy their payload using tar archives rather than adding the raw files to the package file. Please create the archive `files.tar` and add the contents of the `files/*` directory, but not the directory `files/` itself. Repeat the same process for the `templates` directory, but this time with the file name `templates.tar`. Place both files in the root of your project.
-
-Last but not least, create the package archive `com.example.test.tar` and add all the files listed below.
-
-- `files.tar`
-- `package.xml`
-- `page.xml`
-- `templates.tar`
-
-The archive's filename can be anything you want, all though it is the general convention to use the package name itself for easier recognition.
-
-## Installation
-
-Open the Administration Control Panel and navigate to `Configuration > Packages > Install Package`, click on `Upload Package` and select the file `com.example.test.tar` from your disk. Follow the on-screen instructions until it has been successfully installed.
-
-Open a new browser tab and navigate to your newly created page. If WoltLab Suite is installed at `https://example.com/wsc/`, then the URL should read `https://example.com/wsc/index.php?test/`.
-
-Congratulations, you have just created your first package!
-
-## Developer Tools
-
-{% include callout.html content="This feature is available with WoltLab Suite 3.1 or newer only." type="warning" %}
-
-The developer tools provide an interface to synchronize the data of an installed package with a bare repository on the local disk. You can re-import most PIPs at any time and have the changes applied without crafting a manual update. This process simulates a regular package update with a single PIP only, and resets the cache after the import has been completed.
-
-### Registering a Project
-
-Projects require the absolute path to the package directory, that is, the directory where it can find the `package.xml`. It is not required to install an package to register it as a project, but you have to install it in order to work with it. It does not install the package by itself!
-
-There is a special button on the project list that allows for a mass-import of projects based on a search path. Each direct child directory of the provided path will be tested and projects created this way will use the identifier extracted from the `package.xml`.
-
-### Synchronizing
-
-The install instructions in the `package.xml` are ignored when offering the PIP imports, the detection works entirely based on the default filename for each PIP. On top of that, only PIPs that implement the interface `wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin` are valid for import, as it indicates that importing the PIP multiple times will have no side-effects and that the result is deterministic regardless of the number of times it has been imported.
-
-Some built-in PIPs, such as `sql` or `script`, do not qualify for this step and remain unavailable at all times. However, you can still craft and perform an actual package update to have these PIPs executed.
-
-## Appendix
-
-### Template Guessing {#appendixTemplateGuessing}
-
-The class name including the namespace is used to automatically determine the path to the template and its name. The example above used the page class name `wcf\page\TestPage` that is then split into four distinct parts:
-
-1. `wcf`, the internal abbreviation of WoltLab Suite Core (previously known as WoltLab Community Framework)
-2. `\page\` (ignored)
-3. `Test`, the actual name that is used for both the template and the URL
-4. `Page` (page type, ignored)
-
-The fragments `1.` and `3.` from above are used to construct the path to the template: `<installDirOfWSC>/templates/test.tpl` (the first letter of `Test` is being converted to lower-case).
-
-{% include links.html %}
+++ /dev/null
----
-title: Code Snippets - JavaScript API
-sidebar: sidebar
-permalink: javascript_code-snippets.html
-folder: javascript
----
-
-This is a list of code snippets that do not fit into any of the other articles
-and merely describe how to achieve something very specific, rather than explaining
-the inner workings of a function.
-
-## ImageViewer
-
-The ImageViewer is available on all frontend pages by default, you can easily
-add images to the viewer by wrapping the thumbnails with a link with the CSS
-class `jsImageViewer` that points to the full version.
-
-```html
-<a href="http://example.com/full.jpg" class="jsImageViewer">
- <img src="http://example.com/thumbnail.jpg">
-</a>
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: General JavaScript Usage
-sidebar: sidebar
-permalink: javascript_general-usage.html
-folder: javascript
----
-
-## The History of the Legacy API
-
-The WoltLab Suite 3.0 [introduced a new API][javascript_new-api_writing-a-module] based on AMD-Modules
-with ES5-JavaScript that was designed with high performance and visible dependencies
-in mind. This was a fundamental change in comparison to [the legacy API][javascript_legacy-api]
-that was build many years before while jQuery was still a thing and we had to deal
-with ancient browsers such as Internet Explorer 9 that felt short in both CSS and
-JavaScript capabilities.
-
-Fast forward a few years, the old API is still around and most important, it is
-actively being used by some components that have not been rewritten yet. This
-has been done to preserve the backwards-compatibility and to avoid the
-significant amount of work that it requires to rewrite a component. The components
-invoked on page initialization have all been rewritten to use the modern API, but
-some deferred objects that are invoked later during the page runtime may still
-use the old API.
-
-However, the legacy API is deprecated and you should not rely on it for new
-components at all. It slowly but steadily gets replaced up until a point where its
-last bits are finally removed from the code base.
-
-## Embedding JavaScript inside Templates
-
-The `<script>`-tags are extracted and moved during template processing, eventually
-placing them at the very end of the body element while preserving their order of
-appearance.
-
-This behavior is controlled through the `data-relocate="true"` attribute on the `<script>`
-which is mandatory for almost all scripts, mostly because their dependencies (such
-as jQuery) are moved to the bottom anyway.
-
-```html
-<script data-relocate="true">
- $(function() {
- // Code that uses jQuery (Legacy API)
- });
-</script>
-
-<!-- or -->
-
-<script data-relocate="true">
- require(["Some", "Dependencies"], function(Some, Dependencies) {
- // Modern API
- });
-</script>
-```
-
-## Including External JavaScript Files
-
-The AMD-Modules used in the new API are automatically recognized and lazy-loaded
-on demand, so unless you have a rather large and pre-compiled code-base, there
-is nothing else to worry about.
-
-### Debug-Variants and Cache-Buster
-
-Your JavaScript files may change over time and you would want the users' browsers
-to always load and use the latest version of your files. This can be achieved by
-appending the special `LAST_UPDATE_TIME` constant to your file path. It contains
-the unix timestamp of the last time any package was installed, updated or removed
-and thus avoid outdated caches by relying on a unique value, without invalidating
-the cache more often that it needs to be.
-
-```html
-<script data-relocate="true" src="{@$__wcf->getPath('app')}js/App.js?t={@LAST_UPDATE_TIME}"></script>
-```
-
-For small scripts you can simply serve the full, non-minified version to the user
-at all times, the differences in size and execution speed are insignificant and
-are very unlikely to offer any benefits. They might even yield a worse performance,
-because you'll have to include them statically in the template, even if the code
-is never called.
-
-However, if you're including a minified build in your app or plugin, you should
-include a switch to load the uncompressed version in the debug mode, while serving
-the minified and optimized file to the average visitor. You should use the
-`ENABLE_DEBUG_MODE` constant to decide which version should be loaded.
-
-```html
-<script data-relocate="true" src="{@$__wcf->getPath('app')}js/App{if !ENABLE_DEBUG_MODE}.min{/if}.js?t={@LAST_UPDATE_TIME}"></script>
-```
-
-### The Accelerated Guest View ("Tiny Builds")
-
-{% include callout.html content="You can learn more on the [Accelerated Guest View][migration_wsc-30_javascript] in the migration docs." type="info" %}
-
-The "Accelerated Guest View" was introduced in WoltLab Suite 3.1 and aims to
-decrease page size and to improve responsiveness by enabling a read-only mode
-for visitors. If you are providing a separate compiled build for this mode, you'll
-need to include yet another switch to serve the right version to the visitor.
-
-```html
-<script data-relocate="true" src="{@$__wcf->getPath('app')}js/App{if !ENABLE_DEBUG_MODE}{if VISITOR_USE_TINY_BUILD}.tiny{/if}.min{/if}.js?t={@LAST_UPDATE_TIME}"></script>
-```
-
-### The `{js}` Template Plugin
-
-The `{js}` template plugin exists solely to provide a much easier and less error-prone
-method to include external JavaScript files.
-
-```html
-{js application='app' file='App' hasTiny=true}
-```
-
-The `hasTiny` attribute is optional, you can set it to `false` or just omit it
-entirely if you do not provide a tiny build for your file.
-
-{% include links.html %}
+++ /dev/null
----
-title: JavaScript Helper Functions
-sidebar: sidebar
-permalink: javascript_helper-functions.html
-folder: javascript
----
-
-## Introduction
-
-Since version 3.0, WoltLab Suite ships with a set of global helper functions that are
-exposed on the `window`-object and thus are available regardless of the context.
-They are meant to reduce code repetition and to increase readability by moving
-potentially relevant parts to the front of an instruction.
-
-## Elements
-
-### `elCreate(tagName: string): Element`
-
-Creates a new element with the provided tag name.
-
-```js
-var element = elCreate("div");
-// equals
-var element = document.createElement("div");
-```
-
-### `elRemove(element: Element)`
-
-Removes an element from its parent without returning it. This function will throw
-an error if the `element` doesn't have a parent node.
-
-```js
-elRemove(element);
-// equals
-element.parentNode.removeChild(element);
-```
-
-### `elShow(element: Element)`
-
-Attempts to show an element by removing the `display` CSS-property, usually used
-in conjunction with the `elHide()` function.
-
-```js
-elShow(element);
-// equals
-element.style.removeProperty("display");
-```
-
-### `elHide(element: Element)`
-
-Attempts to hide an element by setting the `display` CSS-property to `none`, this
-is intended to be used with `elShow()` that relies on this behavior.
-
-```js
-elHide(element);
-// equals
-element.style.setProperty("display", "none", "");
-```
-
-### `elToggle(element: Element)`
-
-Attempts to toggle the visibility of an element by examining the value of the
-`display` CSS-property and calls either `elShow()` or `elHide()`.
-
-## Attributes
-
-### `elAttr(element: Element, attribute: string, value?: string): string`
-
-Sets or reads an attribute value, value are implicitly casted into strings and
-reading non-existing attributes will always yield an empty string. If you want
-to test for attribute existence, you'll have to fall-back to the native
-[`Element.hasAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)
-method.
-
-You should read and set native attributes directly, such as `img.src` rather
-than `img.getAttribute("src");`.
-
-```js
-var value = elAttr(element, "some-attribute");
-// equals
-var value = element.getAttribute("some-attribute");
-
-elAttr(element, "some-attribute", "some value");
-// equals
-element.setAttribute("some-attribute", "some value");
-```
-
-### `elAttrBool(element: Element, attribute: string): boolean`
-
-Reads an attribute and converts it value into a boolean value, the strings `"1"`
-and `"true"` will evaluate to `true`. All other values, including a missing attribute,
-will return `false`.
-
-```js
-if (elAttrBool(element, "some-attribute")) {
- // attribute is true-ish
-}
-```
-
-### `elData(element: Element, attribute: string, value?: string): string`
-
-Short-hand function to read or set HTML5 `data-*`-attributes, it essentially
-prepends the `data-` prefix before forwarding the call to `elAttr()`.
-
-```js
-var value = elData(element, "some-attribute");
-// equals
-var value = elAttr(element, "data-some-attribute");
-
-elData(element, "some-attribute", "some value");
-// equals
-elAttr(element, "data-some-attribute", "some value");
-```
-
-### `elDataBool(element: Element, attribute: string): boolean`
-
-Short-hand function to convert a HTML5 `data-*`-attribute into a boolean value. It
-prepends the `data-` prefix before forwarding the call to `elAttrBool()`.
-
-```js
-if (elDataBool(element, "some-attribute")) {
- // attribute is true-ish
-}
-// equals
-if (elAttrBool(element, "data-some-attribute")) {
- // attribute is true-ish
-}
-```
-
-## Selecting Elements
-
-{% include callout.html content="Unlike libraries like jQuery, these functions will return `null` if an element is not found. You are responsible to validate if the element exist and to branch accordingly, invoking methods on the return value without checking for `null` will yield an error." type="warning" %}
-
-### `elById(id: string): Element | null`
-
-Selects an element by its `id`-attribute value.
-
-```js
-var element = elById("my-awesome-element");
-// equals
-var element = document.getElementById("my-awesome-element");
-```
-
-### `elBySel(selector: string, context?: Element): Element | null`
-
-{% include callout.html content="The underlying `querySelector()`-method works on the entire DOM hierarchy and can yield results outside of your context element! Please read and understand the MDN article on [`Element.querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector#The_entire_hierarchy_counts) to learn more about this." type="danger" %}
-
-Select a single element based on a CSS selector, optionally limiting the results
-to be a direct or indirect children of the context element.
-
-```js
-var element = elBySel(".some-element");
-// equals
-var element = document.querySelector(".some-element");
-
-// limiting the scope to a context element:
-var element = elBySel(".some-element", context);
-// equals
-var element = context.querySelector(".some-element");
-```
-
-### `elBySelAll(selector: string, context?: Element, callback: (element: Element) => void): NodeList`
-
-{% include callout.html content="The underlying `querySelector()`-method works on the entire DOM hierarchy and can yield results outside of your context element! Please read and understand the MDN article on [`Element.querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector#The_entire_hierarchy_counts) to learn more about this." type="danger" %}
-
-Finds and returns a `NodeList` containing all elements that match the provided
-CSS selector. Although `NodeList` is an array-like structure, it is not possible
-to iterate over it using array functions, including `.forEach()` which is not
-available in Internet Explorer 11.
-
-```js
-var elements = elBySelAll(".some-element");
-// equals
-var elements = document.querySelectorAll(".some-element");
-
-// limiting the scope to a context element:
-var elements = elBySelAll(".some-element", context);
-// equals
-var elements = context.querySelectorAll(".some-element");
-```
-
-#### Callback to Iterate Over Elements
-
-`elBySelAll()` supports an optional third parameter that expects a callback function
-that is invoked for every element in the list.
-
-```js
-// set the 2nd parameter to `undefined` or `null` to query the whole document
-elBySelAll(".some-element", undefined, function(element) {
- // is called for each element
-});
-
-// limiting the scope to a context element:
-elBySelAll(".some-element", context, function(element) {
- // is called for each element
-});
-```
-
-### `elClosest(element: Element, selector: string): Element | null`
-
-Returns the first `Element` that matches the provided CSS selector, this will
-return the provided element itself if it matches the selector.
-
-```js
-var element = elClosest(context, ".some-element");
-// equals
-var element = context.closest(".some-element");
-```
-
-#### Text Nodes
-
-If the provided context is a `Text`-node, the function will move the context to
-the parent element before applying the CSS selector. If the `Text` has no parent,
-`null` is returned without evaluating the selector.
-
-### `elByClass(className: string, context?: Element): NodeList`
-
-Returns a live `NodeList` containing all elements that match the provided CSS
-class now _and_ in the future! The collection is automatically updated whenever
-an element with that class is added or removed from the DOM, it will also include
-elements that get dynamically assigned or removed this CSS class.
-
-You absolutely need to understand that this collection is dynamic, that means that
-elements can and will be added and removed from the collection _even while_ you
-iterate over it. There are only very few cases where you would need such a collection,
-almost always `elBySelAll()` is what you're looking for.
-
-```js
-// no leading dot!
-var elements = elByClass("some-element");
-// equals
-var elements = document.getElementsByClassName("some-element");
-
-// limiting the scope to a context element:
-var elements = elByClass("some-element", context);
-// equals
-var elements = context.getElementsByClassName(".some-element");
-```
-
-### `elByTag(tagName: string, context?: Element): NodeList`
-
-Returns a live `NodeList` containing all elements with the provided tag name now
-_and_ in the future! Please read the remarks on `elByClass()` above to understand
-the implications of this.
-
-```js
-var elements = elByTag("div");
-// equals
-var elements = document.getElementsByTagName("div");
-
-// limiting the scope to a context element:
-var elements = elByTag("div", context);
-// equals
-var elements = context.getElementsByTagName("div");
-```
-
-## Utility Functions
-
-### `elInnerError(element: Element, errorMessage?: string, isHtml?: boolean): Element | null``
-
-Unified function to display and remove inline error messages for input elements,
-please read the [section in the migration docs](migration_wsc-30_javascript.html#helper-function-for-inline-error-messages)
-to learn more about this function.
-
-## String Extensions
-
-### `hashCode(): string`
-
-Computes a numeric hash value of a string similar to Java's `String.hashCode()` method.
-
-```js
-console.log("Hello World".hashCode());
-// outputs: -862545276
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Legacy JavaScript API
-sidebar: sidebar
-permalink: javascript_legacy-api.html
-folder: javascript
----
-
-## Introduction
-
-The legacy JavaScript API is the original code that was part of the 2.x series
-of WoltLab Suite, formerly known as WoltLab Community Framework. It has been
-superseded for the most part by the [ES5/AMD-modules API][javascript_new-api_writing-a-module]
-introduced with WoltLab Suite 3.0.
-
-Some parts still exist to this day for backwards-compatibility and because some
-less important components have not been rewritten yet. The old API is still
-supported, but marked as deprecated and will continue to be replaced parts by
-part in future releases, up until their entire removal, including jQuery support.
-
-This guide does not provide any explanation on the usage of those legacy components,
-but instead serves as a cheat sheet to convert code to use the new API.
-
-## Classes
-
-### Singletons
-
-Singleton instances are designed to provide a unique "instance" of an object
-regardless of when its first instance was created. Due to the lack of a `class`
-construct in ES5, they are represented by mere objects that act as an instance.
-
-```js
-// App.js
-window.App = {};
-App.Foo = {
- bar: function() {}
-};
-
-// --- NEW API ---
-
-// App/Foo.js
-define([], function() {
- "use strict";
-
- return {
- bar: function() {}
- };
-});
-```
-
-### Regular Classes
-
-```js
-// App.js
-window.App = {};
-App.Foo = Class.extend({
- bar: function() {}
-});
-
-// --- NEW API ---
-
-// App/Foo.js
-define([], function() {
- "use strict";
-
- function Foo() {};
- Foo.prototype = {
- bar: function() {}
- };
-
- return Foo;
-});
-```
-
-#### Inheritance
-
-```js
-// App.js
-window.App = {};
-App.Foo = Class.extend({
- bar: function() {}
-});
-App.Baz = App.Foo.extend({
- makeSnafucated: function() {}
-});
-
-// --- NEW API ---
-
-// App/Foo.js
-define([], function() {
- "use strict";
-
- function Foo() {};
- Foo.prototype = {
- bar: function() {}
- };
-
- return Foo;
-});
-
-// App/Baz.js
-define(["Core", "./Foo"], function(Core, Foo) {
- "use strict";
-
- function Baz() {};
- Core.inherit(Baz, Foo, {
- makeSnafucated: function() {}
- });
-
- return Baz;
-});
-```
-
-## Ajax Requests
-
-```js
-// App.js
-App.Foo = Class.extend({
- _proxy: null,
-
- init: function() {
- this._proxy = new WCF.Action.Proxy({
- success: $.proxy(this._success, this)
- });
- },
-
- bar: function() {
- this._proxy.setOption("data", {
- actionName: "baz",
- className: "app\\foo\\FooAction",
- objectIDs: [1, 2, 3],
- parameters: {
- foo: "bar",
- baz: true
- }
- });
- this._proxy.sendRequest();
- },
-
- _success: function(data) {
- // ajax request result
- }
-});
-
-// --- NEW API ---
-
-// App/Foo.js
-define(["Ajax"], function(Ajax) {
- "use strict";
-
- function Foo() {}
- Foo.prototype = {
- bar: function() {
- Ajax.api(this, {
- objectIDs: [1, 2, 3],
- parameters: {
- foo: "bar",
- baz: true
- }
- });
- },
-
- // magic method!
- _ajaxSuccess: function(data) {
- // ajax request result
- },
-
- // magic method!
- _ajaxSetup: function() {
- return {
- actionName: "baz",
- className: "app\\foo\\FooAction"
- }
- }
- }
-
- return Foo;
-});
-```
-
-## Phrases
-
-```html
-<script data-relocate="true">
-$(function() {
- WCF.Language.addObject({
- 'app.foo.bar': '{lang}app.foo.bar{/lang}'
- });
-
- console.log(WCF.Language.get("app.foo.bar"));
-});
-</script>
-
-<!-- NEW API -->
-
-<script data-relocate="true">
-require(["Language"], function(Language) {
- Language.addObject({
- 'app.foo.bar': '{jslang}app.foo.bar{/jslang}'
- });
-
- console.log(Language.get("app.foo.bar"));
-});
-</script>
-```
-
-## Event-Listener
-
-```html
-<script data-relocate="true">
-$(function() {
- WCF.System.Event.addListener("app.foo.bar", "makeSnafucated", function(data) {
- console.log("Event was invoked.");
- });
-
- WCF.System.Event.fireEvent("app.foo.bar", "makeSnafucated", { some: "data" });
-});
-</script>
-
-<!-- NEW API -->
-
-<script data-relocate="true">
-require(["EventHandler"], function(EventHandler) {
- EventHandler.add("app.foo.bar", "makeSnafucated", function(data) {
- console.log("Event was invoked");
- });
-
- EventHandler.fire("app.foo.bar", "makeSnafucated", { some: "data" });
-});
-</script>
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Ajax Requests - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_ajax.html
-folder: javascript
----
-
-## Ajax inside Modules
-
-The Ajax component was designed to be used from inside modules where an object
-reference is used to delegate request callbacks. This is acomplished through
-a set of magic methods that are automatically called when the request is created
-or its state has changed.
-
-### `_ajaxSetup()`
-
-The lazy initialization is performed upon the first invocation from the callee,
-using the magic `_ajaxSetup()` method to retrieve the basic configuration for
-this and any future requests.
-
-The data returned by `_ajaxSetup()` is cached and the data will be used to
-pre-populate the request data before sending it. The callee can overwrite any of
-these properties. It is intended to reduce the overhead when issuing request
-when these requests share the same properties, such as accessing the same endpoint.
-
-```js
-// App/Foo.js
-define(["Ajax"], function(Ajax) {
- "use strict";
-
- function Foo() {};
- Foo.prototype = {
- one: function() {
- // this will issue an ajax request with the parameter `value` set to `1`
- Ajax.api(this);
- },
-
- two: function() {
- // this request is almost identical to the one issued with `.one()`, but
- // the value is now set to `2` for this invocation only.
- Ajax.api(this, {
- parameters: {
- value: 2
- }
- });
- },
-
- _ajaxSetup: function() {
- return {
- data: {
- actionName: "makeSnafucated",
- className: "app\\data\\foo\\FooAction",
- parameters: {
- value: 1
- }
- }
- }
- }
- };
-
- return Foo;
-});
-```
-
-### Request Settings
-
-The object returned by the aforementioned `_ajaxSetup()` callback can contain these
-values:
-
-#### `data`
-
-_Defaults to `{}`._
-
-A plain JavaScript object that contains the request data that represents the form
-data of the request. The `parameters` key is recognized by the PHP Ajax API and
-becomes accessible through `$this->parameters`.
-
-#### `contentType`
-
-_Defaults to `application/x-www-form-urlencoded; charset=UTF-8`._
-
-The request content type, sets the `Content-Type` HTTP header if it is not empty.
-
-#### `responseType`
-
-_Defaults to `application/json`._
-
-The server must respond with the `Content-Type` HTTP header set to this value,
-otherwise the request will be treated as failed. Requests for `application/json`
-will have the return body attempted to be evaluated as JSON.
-
-Other content types will only be validated based on the HTTP header, but no
-additional transformation is performed. For example, setting the `responseType`
-to `application/xml` will check the HTTP header, but will not transform the
-`data` parameter, you'll still receive a string in `_ajaxSuccess`!
-
-#### `type`
-
-_Defaults to `POST`._
-
-The HTTP Verb used for this request.
-
-#### `url`
-
-_Defaults to an empty string._
-
-Manual override for the request endpoint, it will be automatically set to the
-Core API endpoint if left empty. If the Core API endpoint is used, the options
-`includeRequestedWith` and `withCredentials` will be force-set to true.
-
-#### `withCredentials`
-
-{% include callout.html content="Enabling this parameter for any domain other than the current will trigger a CORS preflight request." type="warning" %}
-
-_Defaults to `false`._
-
-Include cookies with this requested, is always true when `url` is (implicitly)
-set to the Core API endpoint.
-
-#### `autoAbort`
-
-_Defaults to `false`._
-
-When set to `true`, any pending responses to earlier requests will be silently
-discarded when issuing a new request. This only makes sense if the new request
-is meant to completely replace the result of the previous one, regardless of its
-reponse body.
-
-Typical use-cases include input field with suggestions, where possible values
-are requested from the server, but the input changed faster than the server was
-able to reply. In this particular case the client is not interested in the result
-for an earlier value, auto-aborting these requests avoids implementing this logic
-in the requesting code.
-
-#### `ignoreError`
-
-_Defaults to `false`._
-
-Any failing request will invoke the `failure`-callback to check if an error
-message should be displayed. Enabling this option will suppress the general
-error overlay that reports a failed request.
-
-You can achieve the same result by returning `false` in the `failure`-callback.
-
-#### `silent`
-
-_Defaults to `false`._
-
-Enabling this option will suppress the loading indicator overlay for this request,
-other non-"silent" requests will still trigger the loading indicator.
-
-#### `includeRequestedWith`
-
-{% include callout.html content="Enabling this parameter for any domain other than the current will trigger a CORS preflight request." type="warning" %}
-
-_Defaults to `true`._
-
-Sets the custom HTTP header `X-Requested-With: XMLHttpRequest` for the request,
-it is automatically set to `true` when `url` is pointing at the WSC API endpoint.
-
-#### `failure`
-
-_Defaults to `null`._
-
-Optional callback function that will be invoked for requests that have failed
-for one of these reasons:
- 1. The request timed out.
- 2. The HTTP status is not `2xx` or `304`.
- 3. A `responseType` was set, but the response HTTP header `Content-Type` did not match the expected value.
- 4. The `responseType` was set to `application/json`, but the response body was not valid JSON.
-
-The callback function receives the parameter `xhr` (the `XMLHttpRequest` object)
-and `options` (deep clone of the request parameters). If the callback returns
-`false`, the general error overlay for failed requests will be suppressed.
-
-There will be no error overlay if `ignoreError` is set to `true` or if the
-request failed while attempting to evaluate the response body as JSON.
-
-#### `finalize`
-
-_Defaults to `null`._
-
-Optional callback function that will be invoked once the request has completed,
-regardless if it succeeded or failed. The only parameter it receives is
-`options` (the request parameters object), but it does not receive the request's
-`XMLHttpRequest`.
-
-#### `success`
-
-_Defaults to `null`._
-
-This semi-optional callback function will always be set to `_ajaxSuccess()` when
-invoking `Ajax.api()`. It receives four parameters:
- 1. `data` - The request's response body as a string, or a JavaScript object if
- `contentType` was set to `application/json`.
- 2. `responseText` - The unmodified response body, it equals the value for `data`
- for non-JSON requests.
- 3. `xhr` - The underlying `XMLHttpRequest` object.
- 4. `requestData` - The request parameters that were supplied when the request
- was issued.
-
-### `_ajaxSuccess()`
-
-This callback method is automatically called for successful AJAX requests, it
-receives four parameters, with the first one containing either the response body
-as a string, or a JavaScript object for JSON requests.
-
-### `_ajaxFailure()`
-
-Optional callback function that is invoked for failed requests, it will be
-automatically called if the callee implements it, otherwise the global error
-handler will be executed.
-
-## Single Requests Without a Module
-
-The `Ajax.api()` method expects an object that is used to extract the request
-configuration as well as providing the callback functions when the request state
-changes.
-
-You can issue a simple Ajax request without object binding through `Ajax.apiOnce()`
-that will destroy the instance after the request was finalized. This method is
-significantly more expensive for repeated requests and does not offer deriving
-modules from altering the behavior. It is strongly recommended to always use
-`Ajax.api()` for requests to the WSC API endpoint.
-
-```html
-<script data-relocate="true">
- require(["Ajax"], function(Ajax) {
- Ajax.apiOnce({
- data: {
- actionName: "makeSnafucated",
- className: "app\\data\\foo\\FooAction",
- parameters: {
- value: 3
- }
- },
- success: function(data) {
- elBySel(".some-element").textContent = data.bar;
- }
- })
- });
-</script>
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Browser and Screen Sizes - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_browser.html
-folder: javascript
----
-
-## `Ui/Screen`
-
-CSS offers powerful media queries that alter the layout depending on the screen
-sizes, including but not limited to changes between landscape and portrait mode
-on mobile devices.
-
-The `Ui/Screen` module exposes a consistent interface to execute JavaScript code
-based on the same media queries that are available in the CSS code already. It
-features support for unmatching and executing code when a rule matches for the
-first time during the page lifecycle.
-
-### Supported Aliases
-
-You can pass in custom media queries, but it is strongly recommended to use the
-built-in media queries that match the same dimensions as your CSS.
-
-| Alias | Media Query |
-|---|---|
-| `screen-xs` | `(max-width: 544px)` |
-| `screen-sm` | `(min-width: 545px) and (max-width: 768px)` |
-| `screen-sm-down` | `(max-width: 768px)` |
-| `screen-sm-up` | `(min-width: 545px)` |
-| `screen-sm-md` | `(min-width: 545px) and (max-width: 1024px)` |
-| `screen-md` | `(min-width: 769px) and (max-width: 1024px)` |
-| `screen-md-down` | `(max-width: 1024px)` |
-| `screen-md-up` | `(min-width: 769px)` |
-| `screen-lg` | `(min-width: 1025px)`
-
-### `on(query: string, callbacks: Object): string`
-
-Registers a set of callback functions for the provided media query, the possible
-keys are `match`, `unmatch` and `setup`. The method returns a randomly generated
-UUIDv4 that is used to identify these callbacks and allows them to be removed
-via `.remove()`.
-
-### `remove(query: string, uuid: string)`
-
-Removes all callbacks for a media query that match the UUIDv4 that was previously
-obtained from the call to `.on()`.
-
-### `is(query: string): boolean`
-
-Tests if the provided media query currently matches and returns true on match.
-
-### `scrollDisable()`
-
-Temporarily prevents the page from being scrolled, until `.scrollEnable()` is
-called.
-
-### `scrollEnable()`
-
-Enables page scrolling again, unless another pending action has also prevented
-the page scrolling.
-
-## `Environment`
-
-{% include callout.html content="The `Environment` module uses a mixture of feature detection and user agent sniffing to determine the browser and platform. In general, its results have proven to be very accurate, but it should be taken with a grain of salt regardless. Especially the browser checks are designed to be your last resort, please use feature detection instead whenever it is possible!" type="warning" %}
-
-Sometimes it may be necessary to alter the behavior of your code depending on
-the browser platform (e. g. mobile devices) or based on a specific browser in
-order to work-around some quirks.
-
-### `browser(): string`
-
-Attempts to detect browsers based on their technology and supported CSS vendor
-prefixes, and although somewhat reliable for major browsers, it is highly
-recommended to use feature detection instead.
-
-Possible values:
- - `chrome` (includes Opera 15+ and Vivaldi)
- - `firefox`
- - `safari`
- - `microsoft` (Internet Explorer and Edge)
- - `other` (default)
-
-### `platform(): string`
-
-Attempts to detect the browser platform using user agent sniffing.
-
-Possible values:
- - `ios`
- - `android`
- - `windows` (IE Mobile)
- - `mobile` (generic mobile device)
- - `desktop` (default)
-
-{% include links.html %}
+++ /dev/null
----
-title: Core Modules and Functions - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_core.html
-folder: javascript
----
-
-A brief overview of common methods that may be useful when writing any module.
-
-## `Core`
-
-### `clone(object: Object): Object`
-
-Creates a deep-clone of the provided object by value, removing any references on
-the original element, including arrays. However, this does not clone references
-to non-plain objects, these instances will be copied by reference.
-
-```js
-require(["Core"], function(Core) {
- var obj1 = { a: 1 };
- var obj2 = Core.clone(obj1);
-
- console.log(obj1 === obj2); // output: false
- console.log(obj2.hasOwnProperty("a") && obj2.a === 1); // output: true
-});
-```
-
-### `extend(base: Object, ...merge: Object[]): Object`
-
-Accepts an infinite amount of plain objects as parameters, values will be copied
-from the 2nd...nth object into the first object. The first parameter will be
-cloned and the resulting object is returned.
-
-```js
-require(["Core"], function(Core) {
- var obj1 = { a: 2 };
- var obj2 = { a: 1, b: 2 };
- var obj = Core.extend({
- b: 1
- }, obj1, obj2);
-
- console.log(obj.b === 2); // output: true
- console.log(obj.hasOwnProperty("a") && obj.a === 2); // output: false
-});
-```
-
-### `inherit(base: Object, target: Object, merge?: Object)`
-
-Derives the second object's prototype from the first object, afterwards the
-derived class will pass the `instanceof` check against the original class.
-
-```js
-// App.js
-window.App = {};
-App.Foo = Class.extend({
- bar: function() {}
-});
-App.Baz = App.Foo.extend({
- makeSnafucated: function() {}
-});
-
-// --- NEW API ---
-
-// App/Foo.js
-define([], function() {
- "use strict";
-
- function Foo() {};
- Foo.prototype = {
- bar: function() {}
- };
-
- return Foo;
-});
-
-// App/Baz.js
-define(["Core", "./Foo"], function(Core, Foo) {
- "use strict";
-
- function Baz() {};
- Core.inherit(Baz, Foo, {
- makeSnafucated: function() {}
- });
-
- return Baz;
-});
-```
-
-### `isPlainObject(object: Object): boolean`
-
-Verifies if an object is a plain JavaScript object and not an object instance.
-
-```js
-require(["Core"], function(Core) {
- function Foo() {}
- Foo.prototype = {
- hello: "world";
- };
-
- var obj1 = { hello: "world" };
- var obj2 = new Foo();
-
- console.log(Core.isPlainObject(obj1)); // output: true
- console.log(obj1.hello === obj2.hello); // output: true
- console.log(Core.isPlainObject(obj2)); // output: false
-});
-```
-
-### `triggerEvent(element: Element, eventName: string)`
-
-Creates and dispatches a synthetic JavaScript event on an element.
-
-```js
-require(["Core"], function(Core) {
- var element = elBySel(".some-element");
- Core.triggerEvent(element, "click");
-});
-```
-
-## `Language`
-
-### `add(key: string, value: string)`
-
-Registers a new phrase.
-
-```html
-<script data-relocate="true">
- require(["Language"], function(Language) {
- Language.add('app.foo.bar', '{jslang}app.foo.bar{/jslang}');
- });
-</script>
-```
-
-### `addObject(object: Object)`
-
-Registers a list of phrases using a plain object.
-
-```html
-<script data-relocate="true">
- require(["Language"], function(Language) {
- Language.addObject({
- 'app.foo.bar': '{jslang}app.foo.bar{/jslang}'
- });
- });
-</script>
-```
-
-### `get(key: string, parameters?: Object): string`
-
-Retrieves a phrase by its key, optionally supporting basic template scripting
-with dynamic variables passed using the `parameters` object.
-
-```js
-require(["Language"], function(Language) {
- var title = Language.get("app.foo.title");
- var content = Language.get("app.foo.content", {
- some: "value"
- });
-});
-```
-
-## `StringUtil`
-
-### `escapeHTML(str: string): string`
-
-Escapes special HTML characters by converting them into an HTML entity.
-
-| Character | Replacement |
-|---|---|
-| `&` | `&` |
-| `"` | `"` |
-| `<` | `<` |
-| `>` | `>` |
-
-### `escapeRegExp(str: string): string`
-
-Escapes a list of characters that have a special meaning in regular expressions
-and could alter the behavior when embedded into regular expressions.
-
-### `lcfirst(str: string): string`
-
-Makes a string's first character lowercase.
-
-### `ucfirst(str: string): string`
-
-Makes a string's first character uppercase.
-
-### `unescapeHTML(str: string): string`
-
-Converts some HTML entities into their original character. This is the reverse
-function of `escapeHTML()`.
-
-{% include links.html %}
+++ /dev/null
----
-title: Data Structures - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_data-structures.html
-folder: javascript
----
-
-## Introduction
-
-JavaScript offers only limited types of collections to hold and iterate over
-data. Despite the ongoing efforts in ES6 and newer, these new data structures
-and access methods, such as `for … of`, are not available in the still supported
-Internet Explorer 11.
-
-## `Dictionary`
-
-Represents a simple key-value map, but unlike the use of plain objects, will
-always to guarantee to iterate over directly set values only.
-
-_In supported browsers this will use a native `Map` internally, otherwise a plain object._
-
-### `set(key: string, value: any)`
-
-Adds or updates an item using the provided key. Numeric keys will be converted
-into strings.
-
-### `delete(key: string)`
-
-Removes an item from the collection.
-
-### `has(key: string): boolean`
-
-Returns true if the key is contained in the collection.
-
-### `get(key: string): any`
-
-Returns the value for the provided key, or `undefined` if the key was not found.
-Use `.has()` to check for key existence.
-
-### `forEach(callback: (value: any, key: string) => void)`
-
-Iterates over all items in the collection in an arbitrary order and invokes the
-supplied callback with the value and the key.
-
-### `size: number`
-
-This read-only property counts the number of items in the collection.
-
-## `List`
-
-Represents a list of unique values.
-
-_In supported browsers this will use a native `Set` internally, otherwise an array._
-
-### `add(value: any)`
-
-Adds a value to the list. If the value is already part of the list, this method
-will silently abort.
-
-### `clear()`
-
-Resets the collection.
-
-### `delete(value: any): boolean`
-
-Attempts to remove a value from the list, it returns true if the value has been
-part of the list.
-
-### `forEach(callback: (value: any) => void)`
-
-Iterates over all values in the list in an arbitrary order and invokes the
-supplied callback for each value.
-
-### `has(value: any): boolean`
-
-Returns true if the provided value is part of this list.
-
-### `size: number`
-
-This read-only property counts the number of items in the list.
-
-## `ObjectMap`
-
-{% include callout.html content="This class uses a `WeakMap` internally, the keys are only weakly referenced and do not prevent garbage collection." type="info" %}
-
-Represents a collection where any kind of objects, such as class instances or
-DOM elements, can be used as key. These keys are weakly referenced and will not
-prevent garbage collection from happening, but this also means that it is not
-possible to enumerate or iterate over the stored keys and values.
-
-This class is especially useful when you want to store additional data for
-objects that may get disposed on runtime, such as DOM elements. Using any regular
-data collections will cause the object to be referenced indefinitely, preventing
-the garbage collection from removing orphaned objects.
-
-### `set(key: Object, value: Object)`
-
-Adds the key with the provided value to the map, if the key was already part
-of the collection, its value is overwritten.
-
-### `delete(key: Object)`
-
-Attempts to remove a key from the collection. The method will abort silently if
-the key is not part of the collection.
-
-### `has(key: Object): boolean`
-
-Returns true if there is a value for the provided key in this collection.
-
-### `get(key: Object): Object | undefined`
-
-Retrieves the value of the provided key, or `undefined` if the key was not found.
-
-{% include links.html %}
+++ /dev/null
----
-title: Dialogs - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_dialogs.html
-folder: javascript
----
-
-## Introduction
-
-Dialogs are full screen overlays that cover the currently visible window area
-using a semi-opague backdrop and a prominently placed dialog window in the
-foreground. They shift the attention away from the original content towards the
-dialog and usually contain additional details and/or dedicated form inputs.
-
-## `_dialogSetup()`
-
-The lazy initialization is performed upon the first invocation from the callee,
-using the magic `_dialogSetup()` method to retrieve the basic configuration for
-the dialog construction and any event callbacks.
-
-```js
-// App/Foo.js
-define(["Ui/Dialog"], function(UiDialog) {
- "use strict";
-
- function Foo() {};
- Foo.prototype = {
- bar: function() {
- // this will open the dialog constructed by _dialogSetup
- UiDialog.open(this);
- },
-
- _dialogSetup: function() {
- return {
- id: "myDialog",
- source: "<p>Hello World!</p>",
- options: {
- onClose: function() {
- // the fancy dialog was closed!
- }
- }
- }
- }
- };
-
- return Foo;
-});
-```
-
-### `id: string`
-
-The `id` is used to identify a dialog on runtime, but is also part of the first-
-time setup when the dialog has not been opened before. If `source` is `undefined`,
-the module attempts to construct the dialog using an element with the same id.
-
-### `source: any`
-
-There are six different types of value that `source` does allow and each of them
-changes how the initial dialog is constructed:
-
-1. `undefined`
- The dialog exists already and the value of `id` should be used to identify the
- element.
-2. `null`
- The HTML is provided using the second argument of `.open()`.
-3. `() => void`
- If the `source` is a function, it is executed and is expected to start the
- dialog initialization itself.
-4. `Object`
- Plain objects are interpreted as parameters for an Ajax request, in particular
- `source.data` will be used to issue the request. It is possible to specify the
- key `source.after` as a callback `(content: Element, responseData: Object) => void`
- that is executed after the dialog was opened.
-5. `string`
- The string is expected to be plain HTML that should be used to construct the
- dialog.
-6. `DocumentFragment`
- A new container `<div>` with the provided `id` is created and the contents of
- the `DocumentFragment` is appended to it. This container is then used for the
- dialog.
-
-### `options: Object`
-
-All configuration options and callbacks are handled through this object.
-
-#### `options.backdropCloseOnClick: boolean`
-
-_Defaults to `true`._
-
-Clicks on the dialog backdrop will close the top-most dialog. This option will
-be force-disabled if the option `closeable` is set to `false`.
-
-#### `options.closable: boolean`
-
-_Defaults to `true`._
-
-Enables the close button in the dialog title, when disabled the dialog can be
-closed through the `.close()` API call only.
-
-#### `options.closeButtonLabel: string`
-
-_Defaults to `Language.get("wcf.global.button.close")`._
-
-The phrase that is displayed in the tooltip for the close button.
-
-#### `options.closeConfirmMessage: string`
-
-_Defaults to `""`._
-
-Shows a [confirmation dialog][javascript_new-api_ui] using the configured message
-before closing the dialog. The dialog will not be closed if the dialog is
-rejected by the user.
-
-#### `options.title: string`
-
-_Defaults to `""`._
-
-The phrase that is displayed in the dialog title.
-
-#### `options.onBeforeClose: (id: string) => void`
-
-_Defaults to `null`._
-
-The callback is executed when the user clicks on the close button or, if enabled,
-on the backdrop. The callback is responsible to close the dialog by itself, the
-default close behavior is automatically prevented.
-
-#### `options.onClose: (id: string) => void`
-
-_Defaults to `null`._
-
-The callback is notified once the dialog is about to be closed, but is still
-visible at this point. It is not possible to abort the close operation at this
-point.
-
-#### `options.onShow: (content: Element) => void`
-
-_Defaults to `null`._
-
-Receives the dialog content element as its only argument, allowing the callback
-to modify the DOM or to register event listeners before the dialog is presented
-to the user. The dialog is already visible at call time, but the dialog has not
-been finalized yet.
-
-## `setTitle(id: string | Object, title: string)`
-
-Sets the title of a dialog.
-
-## `setCallback(id: string | Object, key: string, value: (data: any) => void | null)`
-
-Sets a callback function after the dialog initialization, the special value
-`null` will remove a previously set callback. Valid values for `key` are
-`onBeforeClose`, `onClose` and `onShow`.
-
-## `rebuild(id: string | Object)`
-
-Rebuilds a dialog by performing various calculations on the maximum dialog
-height in regards to the overflow handling and adjustments for embedded forms.
-This method is automatically invoked whenever a dialog is shown, after invoking
-the `options.onShow` callback.
-
-## `close(id: string | Object)`
-
-Closes an open dialog, this will neither trigger a confirmation dialog, nor does
-it invoke the `options.onBeforeClose` callback. The `options.onClose` callback
-will always be invoked, but it cannot abort the close operation.
-
-## `getDialog(id: string | Object): Object`
-
-{% include callout.html content="This method returns an internal data object by reference, any modifications made do have an effect on the dialogs behavior and in particular no validation is performed on the modification. It is strongly recommended to use the `.set*()` methods only." type="warning" %}
-
-Returns the internal dialog data that is attached to a dialog. The most important
-key is `.content` which holds a reference to the dialog's inner content element.
-
-## `isOpen(id: string | Object): boolean`
-
-Returns true if the dialog exists and is open.
-
-{% include links.html %}
+++ /dev/null
----
-title: Working with the DOM - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_dom.html
-folder: javascript
----
-
-## Helper Functions
-
-There is large set of [helper functions][javascript_helper-functions] that assist
-you when working with the DOM tree and its elements. These functions are globally
-available and do not require explicit module imports.
-
-## `Dom/Util`
-
-### `createFragmentFromHtml(html: string): DocumentFragment`
-
-Parses a HTML string and creates a `DocumentFragment` object that holds the
-resulting nodes.
-
-### `identify(element: Element): string`
-
-Retrieves the unique identifier (`id`) of an element. If it does not currently
-have an id assigned, a generic identifier is used instead.
-
-### `outerHeight(element: Element, styles?: CSSStyleDeclaration): number`
-
-Computes the outer height of an element using the element's `offsetHeight` and
-the sum of the rounded down values for `margin-top` and `margin-bottom`.
-
-### `outerWidth(element: Element, styles?: CSSStyleDeclaration): number`
-
-Computes the outer width of an element using the element's `offsetWidth` and
-the sum of the rounded down values for `margin-left` and `margin-right`.
-
-### `outerDimensions(element: Element): { height: number, width: number }`
-
-Computes the outer dimensions of an element including its margins.
-
-### `offset(element: Element): { top: number, left: number }`
-
-Computes the element's offset relative to the top left corner of the document.
-
-### `setInnerHtml(element: Element, innerHtml: string)`
-
-Sets the inner HTML of an element via `element.innerHTML = innerHtml`. Browsers
-do not evaluate any embedded `<script>` tags, therefore this method extracts each
-of them, creates new `<script>` tags and inserts them in their original order of
-appearance.
-
-### `contains(element: Element, child: Element): boolean`
-
-Evaluates if `element` is a direct or indirect parent element of `child`.
-
-### `unwrapChildNodes(element: Element)`
-
-Moves all child nodes out of `element` while maintaining their order, then removes
-`element` from the document.
-
-## `Dom/ChangeListener`
-
-This class is used to observe specific changes to the DOM, for example after an
-Ajax request has completed. For performance reasons this is a manually-invoked
-listener that does not rely on a `MutationObserver`.
-
-```js
-require(["Dom/ChangeListener"], function(DomChangeListener) {
- DomChangeListener.add("App/Foo", function() {
- // the DOM may have been altered significantly
- });
-
- // propagate changes to the DOM
- DomChangeListener.trigger();
-});
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Event Handling - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_events.html
-folder: javascript
----
-
-## `EventKey`
-
-This class offers a set of static methods that can be used to determine if some
-common keys are being pressed. Internally it compares either the `.key` property
-if it is supported or the value of `.which`.
-
-```js
-require(["EventKey"], function(EventKey) {
- elBySel(".some-input").addEventListener("keydown", function(event) {
- if (EventKey.Enter(event)) {
- // the `Enter` key was pressed
- }
- });
-});
-```
-
-### `ArrowDown(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `↓` key.
-
-### `ArrowLeft(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `←` key.
-
-### `ArrowRight(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `→` key.
-
-### `ArrowUp(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `↑` key.
-
-### `Comma(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `,` key.
-
-### `Enter(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `↲` key.
-
-### `Escape(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `Esc` key.
-
-### `Tab(event: KeyboardEvent): boolean`
-
-Returns true if the user has pressed the `↹` key.
-
-## `EventHandler`
-
-A synchronous event system based on string identifiers rather than DOM elements,
-similar to the PHP event system in WoltLab Suite. Any components can listen to
-events or trigger events itself at any time.
-
-### Identifiying Events with the Developer Tools
-
-The Developer Tools in WoltLab Suite 3.1 offer an easy option to identify existing
-events that are fired while code is being executed. You can enable this watch
-mode through your browser's console using `Devtools.toggleEventLogging()`:
-
-```
-> Devtools.toggleEventLogging();
-< Event logging enabled
-< [Devtools.EventLogging] Firing event: bar @ com.example.app.foo
-< [Devtools.EventLogging] Firing event: baz @ com.example.app.foo
-```
-
-### `add(identifier: string, action: string, callback: (data: Object) => void): string`
-
-Adding an event listeners returns a randomly generated UUIDv4 that is used to
-identify the listener. This UUID is required to remove a specific listener through
-the `remove()` method.
-
-### `fire(identifier: string, action: string, data?: Object)`
-
-Triggers an event using an optional `data` object that is passed to each listener
-by reference.
-
-### `remove(identifier: string, action: string, uuid: string)`
-
-Removes a previously registered event listener using the UUID returned by `add()`.
-
-### `removeAll(identifier: string, action: string)`
-
-Removes all event listeners registered for the provided `identifier` and `action`.
-
-### `removeAllBySuffix(identifier: string, suffix: string)`
-
-Removes all event listeners for an `identifier` whose action ends with the value
-of `suffix`.
-
-{% include links.html %}
+++ /dev/null
----
-title: User Interface - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_ui.html
-folder: javascript
----
-
-## `Ui/Alignment`
-
-Calculates the alignment of one element relative to another element, with support
-for boundary constraints, alignment restrictions and additional pointer elements.
-
-### `set(element: Element, referenceElement: Element, options: Object)`
-
-Calculates and sets the alignment of the element `element`.
-
-#### `verticalOffset: number`
-
-_Defaults to `0`._
-
-Creates a gap between the element and the reference element, in pixels.
-
-#### `pointer: boolean`
-
-_Defaults to `false`._
-
-Sets the position of the pointer element, requires an existing child of the
-element with the CSS class `.elementPointer`.
-
-#### `pointerOffset: number`
-
-_Defaults to `4`._
-
-The margin from the left/right edge of the element and is used to avoid the
-arrow from being placed right at the edge.
-
-Does not apply when aligning the element to the reference elemnent's center.
-
-#### `pointerClassNames: string[]`
-
-_Defaults to `[]`._
-
-If your element uses CSS-only pointers, such as using the `::before` or `::after`
-pseudo selectors, you can specifiy two separate CSS class names that control the
-alignment:
-
-- `pointerClassNames[0]` is applied to the element when the pointer is displayed
- at the bottom.
-- `pointerClassNames[1]` is used to align the pointer to the right side of the
- element.
-
-#### `refDimensionsElement: Element`
-
-_Defaults to `null`._
-
-An alternative element that will be used to determine the position and dimensions
-of the reference element. This can be useful if you reference element is contained
-in a wrapper element with alternating dimensions.
-
-#### `horizontal: string`
-
-{% include callout.html content="This value is automatically flipped for RTL (right-to-left) languages, `left` is changed into `right` and vice versa." type="info" %}
-
-_Defaults to `"left"`._
-
-Sets the prefered alignment, accepts either `left` or `right`. The value `left`
-instructs the module to align the element with the left boundary of the reference
-element.
-
-The `horizontal` alignment is used as the default and a flip only occurs, if there
-is not enough space in the desired direction. If the element exceeds the boundaries
-in both directions, the value of `horizontal` is used.
-
-#### `vertical: string`
-
-_Defaults to `"bottom"`._
-
-Sets the prefered alignment, accepts either `bottom` or `top`. The value `bottom`
-instructs the module to align the element below the reference element.
-
-The `vertical` alignment is used as the default and a flip only occurs, if there
-is not enough space in the desired direction. If the element exceeds the boundaries
-in both directions, the value of `vertical` is used.
-
-#### `allowFlip: string`
-
-{% include callout.html content="The value for `horizontal` is automatically flipped for RTL (right-to-left) languages, `left` is changed into `right` and vice versa. This setting only controls the behavior when violating space constraints, therefore the aforementioned transformation is always applied." type="info" %}
-
-_Defaults to `"both"`._
-
-Restricts the automatic alignment flipping if the element exceeds the window
-boundaries in the instructed direction.
-
-- `both` - No restrictions.
-- `horizontal` - Element can be aligned with the left _or_ the right boundary of
- the reference element, but the vertical position is fixed.
-- `vertical` - Element can be aligned below _or_ above the reference element,
- but the vertical position is fixed.
-- `none` - No flipping can occur, the element will be aligned regardless of
- any space constraints.
-
-## `Ui/CloseOverlay`
-
-Register elements that should be closed when the user clicks anywhere else, such
-as drop-down menus or tooltips.
-
-```js
-require(["Ui/CloseOverlay"], function(UiCloseOverlay) {
- UiCloseOverlay.add("App/Foo", function() {
- // invoked, close something
- });
-});
-```
-
-### `add(identifier: string, callback: () => void)`
-
-Adds a callback that will be invoked when the user clicks anywhere else.
-
-## `Ui/Confirmation`
-
-Prompt the user to make a decision before carrying out an action, such as a safety
-warning before permanently deleting content.
-
-```js
-require(["Ui/Confirmation"], function(UiConfirmation) {
- UiConfirmation.show({
- confirm: function() {
- // the user has confirmed the dialog
- },
- message: "Do you really want to continue?"
- });
-});
-```
-
-### `show(options: Object)`
-
-Displays a dialog overlay with actions buttons to confirm or reject the dialog.
-
-#### `cancel: (parameters: Object) => void`
-
-_Defaults to `null`._
-
-Callback that is invoked when the dialog was rejected.
-
-#### `confirm: (parameters: Object) => void`
-
-_Defaults to `null`._
-
-Callback that is invoked when the user has confirmed the dialog.
-
-#### `message: string`
-
-_Defaults to '""'._
-
-Text that is displayed in the content area of the dialog, optionally this can
-be HTML, but this requires `messageIsHtml` to be enabled.
-
-#### `messageIsHtml`
-
-_Defaults to `false`._
-
-The `message` option is interpreted as text-only, setting this option to `true`
-will cause the `message` to be evaluated as HTML.
-
-#### `parameters: Object`
-
-Optional list of parameter options that will be passed to the `cancel()` and
-`confirm()` callbacks.
-
-#### `template: string`
-
-An optional HTML template that will be inserted into the dialog content area,
-but after the `message` section.
-
-## `Ui/Notification`
-
-Displays a simple notification at the very top of the window, such as a success
-message for Ajax based actions.
-
-```js
-require(["Ui/Notification"], function(UiNotification) {
- UiNotification.show(
- "Your changes have been saved.",
- function() {
- // this callback will be invoked after 2 seconds
- },
- "success"
- );
-});
-```
-
-### `show(message: string, callback?: () => void, cssClassName?: string)`
-
-Shows the notification and executes the callback after 2 seconds.
-
-{% include links.html %}
+++ /dev/null
----
-title: Writing a Module - JavaScript API
-sidebar: sidebar
-permalink: javascript_new-api_writing-a-module.html
-folder: javascript
----
-
-## Introduction
-
-The new JavaScript-API was introduced with WoltLab Suite 3.0 and was a major
-change in all regards. The previously used API heavily relied on larger JavaScript
-files that contained a lot of different components with hidden dependencies
-and suffered from extensive jQuery usage for historic reasons.
-
-Eventually a new API was designed that solves the issues with the legacy API
-by following a few basic principles:
- 1. Vanilla ES5-JavaScript.
- It allows us to achieve the best performance across all platforms, there is
- simply no reason to use jQuery today and the performance penalty on mobile
- devices is a real issue.
- 2. Strict usage of modules.
- Each component is placed in an own file and all dependencies are explicitly
- declared and injected at the top.Eventually we settled with AMD-style modules
- using require.js which offers both lazy loading and "ahead of time"-compilatio
- with `r.js`.
- 3. No jQuery-based components on page init.
- Nothing is more annoying than loading a page and then wait for JavaScript to
- modify the page before it becomes usable, forcing the user to sit and wait.
- Heavily optimized vanilla JavaScript components offered the speed we wanted.
- 4. Limited backwards-compatibility.
- The new API should make it easy to update existing components by providing
- similar interfaces, while still allowing legacy code to run side-by-side for
- best compatibility and to avoid rewritting everything from the start.
-
-## Defining a Module
-
-The default location for modules is `js/` in the Core's app dir,
-but every app and plugin can register their own lookup path by providing the path
-using a [template-listener][package_pip_template-listener] on `requirePaths@headIncludeJavaScript`.
-
-For this example we'll assume the file is placed at `js/WoltLabSuite/Core/Ui/Foo.js`,
-the module name is therefore `WoltLabSuite/Core/Ui/Foo`, it is automatically
-derived from the file path and name.
-
-For further instructions on how to define and require modules head over to the [RequireJS API](http://requirejs.org/docs/api.html).
-
-```js
-define(["Ajax", "WoltLabSuite/Core/Ui/Bar"], function(Ajax, UiBar) {
- "use strict";
-
- function Foo() { this.init(); }
- Foo.prototype = {
- init: function() {
- elBySel(".myButton").addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
- },
-
- _click: function(event) {
- event.preventDefault();
-
- if (UiBar.isSnafucated()) {
- Ajax.api(this);
- }
- },
-
- _ajaxSuccess: function(data) {
- console.log("Received response", data);
- },
-
- _ajaxSetup: function() {
- return {
- data: {
- actionName: "makeSnafucated",
- className: "wcf\\data\\foo\\FooAction"
- }
- };
- }
- }
-
- return Foo;
-});
-```
-
-## Loading a Module
-
-Modules can then be loaded through their derived name:
-
-```html
-<script data-relocate="true">
- require(["WoltLabSuite/Core/Ui/Foo"], function(UiFoo) {
- new UiFoo();
- });
-</script>
-```
-
-### Module Aliases
-
-Some common modules have short-hand aliases that can be used to include them
-without writing out their full name. You can still use their original path, but
-it is strongly recommended to use the aliases for consistency.
-
-| Alias | Full Path |
-|---|---|
-| [Ajax][javascript_new-api_ajax] | WoltLabSuite/Core/Ajax |
-| AjaxJsonp | WoltLabSuite/Core/Ajax/Jsonp |
-| AjaxRequest | WoltLabSuite/Core/Ajax/Request |
-| CallbackList | WoltLabSuite/Core/CallbackList |
-| ColorUtil | WoltLabSuite/Core/ColorUtil |
-| [Core][javascript_new-api_core] | WoltLabSuite/Core/Core |
-| DateUtil | WoltLabSuite/Core/Date/Util |
-| Devtools | WoltLabSuite/Core/Devtools |
-| [Dictionary][javascript_new-api_data-structures] | WoltLabSuite/Core/Dictionary |
-| [Dom/ChangeListener][javascript_new-api_dom] | WoltLabSuite/Core/Dom/Change/Listener |
-| Dom/Traverse | WoltLabSuite/Core/Dom/Traverse |
-| [Dom/Util][javascript_new-api_dom] | WoltLabSuite/Core/Dom/Util |
-| [Environment][javascript_new-api_browser] | WoltLabSuite/Core/Environment |
-| [EventHandler][javascript_new-api_events] | WoltLabSuite/Core/Event/Handler |
-| [EventKey][javascript_new-api_events] | WoltLabSuite/Core/Event/Key |
-| [Language][javascript_new-api_core] | WoltLabSuite/Core/Language |
-| [List][javascript_new-api_data-structures] | WoltLabSuite/Core/List |
-| [ObjectMap][javascript_new-api_data-structures] | WoltLabSuite/Core/ObjectMap |
-| Permission | WoltLabSuite/Core/Permission |
-| [StringUtil][javascript_new-api_core] | WoltLabSuite/Core/StringUtil |
-| [Ui/Alignment][javascript_new-api_ui] | WoltLabSuite/Core/Ui/Alignment |
-| [Ui/CloseOverlay][javascript_new-api_ui] | WoltLabSuite/Core/Ui/CloseOverlay |
-| [Ui/Confirmation][javascript_new-api_ui] | WoltLabSuite/Core/Ui/Confirmation |
-| [Ui/Dialog][javascript_new-api_dialogs] | WoltLabSuite/Core/Ui/Dialog |
-| [Ui/Notification][javascript_new-api_ui] | WoltLabSuite/Core/Ui/Notification |
-| Ui/ReusableDropdown | WoltLabSuite/Core/Ui/Dropdown/Reusable |
-| [Ui/Screen][javascript_new-api_browser] | WoltLabSuite/Core/Ui/Screen |
-| Ui/Scroll | WoltLabSuite/Core/Ui/Scroll |
-| Ui/SimpleDropdown | WoltLabSuite/Core/Ui/Dropdown/Simple |
-| Ui/TabMenu | WoltLabSuite/Core/Ui/TabMenu |
-| Upload | WoltLabSuite/Core/Upload |
-| User | WoltLabSuite/Core/User |
-
-{% include links.html %}
+++ /dev/null
----
-title: WCF 2.1.x - CSS
-sidebar: sidebar
-permalink: migration_wcf-21_css.html
-folder: migration/wcf-21
----
-
-The LESS compiler has been in use since WoltLab Community Framework 2.0, but was replaced with a SCSS compiler in WoltLab Suite 3.0. This change was motivated by SCSS becoming the de facto standard for CSS pre-processing and some really annoying shortcomings in the old LESS compiler.
-
-The entire CSS has been rewritten from scratch, please read the [docs on CSS][view_css] to learn what has changed.
-
-{% include links.html %}
+++ /dev/null
----
-title: WCF 2.1.x - Package Components
-sidebar: sidebar
-permalink: migration_wcf-21_package.html
-folder: migration/wcf-21
----
-
-## package.xml
-
-### Short Instructions
-
-Instructions can now omit the filename, causing them to use the default filename if defined by the package installation plugin (in short: `PIP`). Unless overridden it will default to the PIP's class name with the first letter being lower-cased, e.g. `EventListenerPackageInstallationPlugin` implies the filename `eventListener.xml`. The file is always assumed to be in the archive's root, files located in subdirectories need to be explicitly stated, just as it worked before.
-
-Every PIP can define a custom filename if the default value cannot be properly derived. For example the `ACPMenu`-pip would default to `aCPMenu.xml`, requiring the class to explicitly override the default filename with `acpMenu.xml` for readability.
-
-### Example
-
-```xml
-<instructions type="install">
- <!-- assumes `eventListener.xml` -->
- <instruction type="eventListener" />
- <!-- assumes `install.sql` -->
- <instruction type="sql" />
- <!-- assumes `language/*.xml` -->
- <instruction type="language" />
-
- <!-- exceptions -->
-
- <!-- assumes `files.tar` -->
- <instruction type="file" />
- <!-- no default value, requires relative path -->
- <instruction type="script">acp/install_com.woltlab.wcf_3.0.php</instruction>
-</instructions>
-```
-
-### Exceptions
-
-{% include callout.html content="These exceptions represent the built-in PIPs only, 3rd party plugins and apps may define their own exceptions." type="info" %}
-
-| PIP | Default Value |
-|-------|-------|
-| `acpTemplate` | `acptemplates.tar` |
-| `file` | `files.tar` |
-| `language` | `language/*.xml` |
-| `script` | (No default value) |
-| `sql` | `install.sql` |
-| `template` | `templates.tar` |
-
-## acpMenu.xml
-
-### Renamed Categories
-
-The following categories have been renamed, menu items need to be adjusted to reflect the new names:
-
-| Old Value | New Value |
-|-------|-------|
-| `wcf.acp.menu.link.system` | `wcf.acp.menu.link.configuration` |
-| `wcf.acp.menu.link.display` | `wcf.acp.menu.link.customization` |
-| `wcf.acp.menu.link.community` | `wcf.acp.menu.link.application` |
-
-### Submenu Items
-
-Menu items can now offer additional actions to be accessed from within the menu using an icon-based navigation. This step avoids filling the menu with dozens of `Add …` links, shifting the focus on to actual items. Adding more than one action is not recommended and you should at maximum specify two actions per item.
-
-### Example
-
-```xml
-<!-- category -->
-<acpmenuitem name="wcf.acp.menu.link.group">
- <parent>wcf.acp.menu.link.user</parent>
- <showorder>2</showorder>
-</acpmenuitem>
-
-<!-- menu item -->
-<acpmenuitem name="wcf.acp.menu.link.group.list">
- <controller>wcf\acp\page\UserGroupListPage</controller>
- <parent>wcf.acp.menu.link.group</parent>
- <permissions>admin.user.canEditGroup,admin.user.canDeleteGroup</permissions>
-</acpmenuitem>
-<!-- menu item action -->
-<acpmenuitem name="wcf.acp.menu.link.group.add">
- <controller>wcf\acp\form\UserGroupAddForm</controller>
- <!-- actions are defined by menu items of menu items -->
- <parent>wcf.acp.menu.link.group.list</parent>
- <permissions>admin.user.canAddGroup</permissions>
- <!-- required FontAwesome icon name used for display -->
- <icon>fa-plus</icon>
-</acpmenuitem>
-```
-
-### Common Icon Names
-
-You should use the same icon names for the (logically) same task, unifying the meaning of items and making the actions predictable.
-
-| Meaning | Icon Name | Result |
-|-------|-------|-------|
-| Add or create | `fa-plus` | <i class="fa fa-plus"></i> |
-| Search | `fa-search` | <i class="fa fa-search"></i> |
-| Upload | `fa-upload` | <i class="fa fa-upload"></i> |
-
-## box.xml
-
-The [box][package_pip_box] PIP has been added.
-
-## cronjob.xml
-
-{% include callout.html content="Legacy cronjobs are assigned a non-deterministic generic name, the only way to assign them a name is removing them and then adding them again." type="warning" %}
-
-Cronjobs can now be assigned a name using the name attribute as in `<cronjob name="com.woltlab.wcf.refreshPackageUpdates">`, it will be used to identify cronjobs during an update or delete.
-
-## eventListener.xml
-
-{% include callout.html content="Legacy event listeners are assigned a non-deterministic generic name, the only way to assign them a name is removing them and then adding them again." type="warning" %}
-
-Event listeners can now be assigned a name using the name attribute as in `<eventlistener name="sessionPageAccessLog">`, it will be used to identify event listeners during an update or delete.
-
-## menu.xml
-
-The [menu][package_pip_menu] PIP has been added.
-
-## menuItem.xml
-
-The [menuItem][package_pip_menu-item] PIP has been added.
-
-## objectType.xml
-
-The definition `com.woltlab.wcf.user.dashboardContainer` has been removed, it was previously used to register pages that qualify for dashboard boxes. Since WoltLab Suite 3.0, all pages registered through the `page.xml` are valid containers and therefore there is no need for this definition anymore.
-
-The definitions `com.woltlab.wcf.page` and `com.woltlab.wcf.user.online.location` have been superseded by the `page.xml`, they're no longer supported.
-
-## option.xml
-
-The `module.display` category has been renamed into `module.customization`.
-
-## page.xml
-
-The [page][package_pip_page] PIP has been added.
-
-## pageMenu.xml
-
-The `pageMenu.xml` has been superseded by the `page.xml` and is no longer available.
-
-{% include links.html %}
+++ /dev/null
----
-title: WCF 2.1.x - PHP
-sidebar: sidebar
-permalink: migration_wcf-21_php.html
-folder: migration/wcf-21
----
-
-## Message Processing
-
-WoltLab Suite 3.0 finally made the transition from raw bbcode to bbcode-flavored HTML, with many new features related to message processing being added. This change impacts both message validation and storing, requiring slightly different APIs to get the job done.
-
-### Input Processing for Storage
-
-The returned HTML is an intermediate representation with a maximum of meta data embedded into it, designed to be stored in the database. Some bbcodes are replaced during this process, for example `[b]…[/b]` becomes `<strong>…</strong>`, while others are converted into a metacode tag for later processing.
-
-```php
-<?php
-$processor = new \wcf\system\html\input\HtmlInputProcessor();
-$processor->process($message, $messageObjectType, $messageObjectID);
-$html = $processor->getHtml();
-```
-
-The `$messageObjectID` can be zero if the element did not exist before, but it should be non-zero when saving an edited message.
-
-### Embedded Objects
-
-Embedded objects need to be registered after saving the message, but once again you can use the processor instance to do the job.
-
-```php
-<?php
-$processor = new \wcf\system\html\input\HtmlInputProcessor();
-$processor->process($message, $messageObjectType, $messageObjectID);
-$html = $processor->getHtml();
-
-// at this point the message is saved to database and the created object
-// `$example` is a `DatabaseObject` with the id column `$exampleID`
-
-$processor->setObjectID($example->exampleID);
-if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($processor)) {
- // there is at least one embedded object, this is also the point at which you
- // would set `hasEmbeddedObjects` to true (if implemented by your type)
- (new \wcf\data\example\ExampleEditor($example))->update(['hasEmbeddedObjects' => 1]);
-}
-```
-
-### Rendering the Message
-
-The output processor will parse the intermediate HTML and finalize the output for display. This step is highly dynamic and allows for bbcode evaluation and contextual output based on the viewer's permissions.
-
-```php
-<?php
-$processor = new \wcf\system\html\output\HtmlOutputProcessor();
-$processor->process($html, $messageObjectType, $messageObjectID);
-$renderedHtml = $processor->getHtml();
-```
-
-#### Simplified Output
-
-At some point there can be the need of a simplified output HTML that includes only basic HTML formatting and reduces more sophisticated bbcodes into a simpler representation.
-
-```php
-<?php
-$processor = new \wcf\system\html\output\HtmlOutputProcessor();
-$processor->setOutputType('text/simplified-html');
-$processor->process(…);
-```
-
-#### Plaintext Output
-
-The `text/plain` output type will strip down the simplified HTML into pure text, suitable for text-only output such as the plaintext representation of an email.
-
-```php
-<?php
-$processor = new \wcf\system\html\output\HtmlOutputProcessor();
-$processor->setOutputType('text/plain');
-$processor->process(…);
-```
-
-### Rebuilding Data
-
-#### Converting from BBCode
-
-{% include callout.html content="Enabling message conversion for HTML messages is undefined and yields unexpected results." type="warning" %}
-
-Legacy message that still use raw bbcodes must be converted to be properly parsed by the html processors. This process is enabled by setting the fourth parameter of `process()` to `true`.
-
-```php
-<?php
-$processor = new \wcf\system\html\input\HtmlInputProcessor();
-$processor->process($html, $messageObjectType, $messageObjectID, true);
-$renderedHtml = $processor->getHtml();
-```
-
-#### Extracting Embedded Objects
-
-The `process()` method of the input processor is quite expensive, as it runs through the full message validation including the invocation of HTMLPurifier. This is perfectly fine when dealing with single messages, but when you're handling messages in bulk to extract their embedded objects, you're better of with `processEmbeddedContent()`. This method deconstructs the message, but skips all validation and expects the input to be perfectly valid, that is the output of a previous run of `process()` saved to storage.
-
-```php
-<?php
-$processor = new \wcf\system\html\input\HtmlInputProcessor();
-$processor->processEmbeddedContent($html, $messageObjectType, $messageObjectID);
-
-// invoke `MessageEmbeddedObjectManager::registerObjects` here
-```
-
-## Breadcrumbs / Page Location
-
-{% include callout.html content="Breadcrumbs used to be added left to right, but parent locations are added from the bottom to the top, starting with the first ancestor and going upwards. In most cases you simply need to reverse the order." type="warning" %}
-
-Breadcrumbs used to be a lose collection of arbitrary links, but are now represented by actual page objects and the control has shifted over to the `PageLocationManager`.
-
-```php
-<?php
-// before
-\wcf\system\WCF::getBreadcrumbs()->add(new \wcf\system\breadcrumb\Breadcrumb('title', 'link'));
-
-// after
-\wcf\system\page\PageLocationManager::getInstance()->addParentLocation($pageIdentifier, $pageObjectID, $object);
-```
-
-## Pages and Forms
-
-The property `$activeMenuItem` has been deprecated for the front end and is no longer evaluated at runtime. Recognition of the active item is entirely based around the invoked controller class name and its definition in the page table. You need to properly [register your pages](package_pip_page.html) for this feature to work.
-
-## Search
-
-### ISearchableObjectType
-
-Added the `setLocation()` method that is used to set the current page location based on the search result.
-
-### SearchIndexManager
-
-The methods `SearchIndexManager::add()` and `SearchIndexManager::update()` have been deprecated and forward their call to the new method `SearchIndexManager::set()`.
-
-{% include links.html %}
+++ /dev/null
----
-title: WCF 2.1.x - Templates
-sidebar: sidebar
-permalink: migration_wcf-21_templates.html
-folder: migration/wcf-21
----
-
-## Page Layout
-
-The template structure has been overhauled and it is no longer required nor recommended to include internal templates, such as `documentHeader`, `headInclude` or `userNotice`. Instead use a simple `{include file='header'}` that now takes care of of the entire application frame.
-
-* Templates must not include a trailing `</body></html>` after including the `footer` template.
-* The `documentHeader`, `headInclude` and `userNotice` template should no longer be included manually, the same goes with the `<body>` element, please use `{include file='header'}` instead.
-* The `sidebarOrientation` variable for the `header` template has been removed and no longer works.
-* `header.boxHeadline` has been unified and now reads `header.contentHeader`
-
-Please see the full example at the end of this page for more information.
-
-## Sidebars
-
-Sidebars are now dynamically populated by the box system, this requires a small change to unify the markup. Additionally the usage of `<fieldset>` has been deprecated due to browser inconsistencies and bugs and should be replaced with `section.box`.
-
-Previous markup used in WoltLab Community Framework 2.1 and earlier:
-
-```html
-<fieldset>
- <legend><!-- Title --></legend>
-
- <div>
- <!-- Content -->
- </div>
-</fieldset>
-```
-
-The new markup since WoltLab Suite 3.0:
-
-```html
-<section class="box">
- <h2 class="boxTitle"><!-- Title --></h2>
-
- <div class="boxContent">
- <!-- Content -->
- </div>
-</section>
-```
-
-## Forms
-
-The input tag for session ids `SID_INPUT_TAG` has been deprecated and no longer yields any content, it can be safely removed. In previous versions forms have been wrapped in `<div class="container containerPadding marginTop">…</div>` which no longer has any effect and should be removed.
-
-If you're using the preview feature for WYSIWYG-powered input fields, you need to alter the preview button include instruction:
-
-```smarty
-{include file='messageFormPreviewButton' previewMessageObjectType='com.example.foo.bar' previewMessageObjectID=0}
-```
-
-*The message object id should be non-zero when editing.*
-
-## Icons
-
-The old `.icon-<iconName>` classes have been removed, you are required to use the official `.fa-<iconName>` class names from FontAwesome. This does not affect the generic classes `.icon` (indicates an icon) and `.icon<size>` (e.g. `.icon16` that sets the dimensions), these are still required and have not been deprecated.
-
-Before:
-
-```html
-<span class="icon icon16 icon-list">
-```
-
-Now:
-
-```html
-<span class="icon icon16 fa-list">
-```
-
-### Changed Icon Names
-
-Quite a few icon names have been renamed, the official wiki lists the [new icon names](https://github.com/FortAwesome/Font-Awesome/wiki/Upgrading-from-3.2.1-to-4) in FontAwesome 4.
-
-## Changed Classes
-
-* `.dataList` has been replaced and should now read `<ol class="inlineList commaSeparated">` (same applies to `<ul>`)
-* `.framedIconList` has been changed into `.userAvatarList`
-
-## Removed Elements and Classes
-
-* `<nav class="jsClipboardEditor">` and `<div class="jsClipboardContainer">` have been replaced with a floating button.
-* The anchors `a.toTopLink` have been replaced with a floating button.
-* Avatars should no longer receive the class `framed`
-* The `dl.condensed` class, as seen in the editor tab menu, is no longer required.
-* Anything related to `sidebarCollapsed` has been removed as sidebars are no longer collapsible.
-
-## Simple Example
-
-The code below includes only the absolute minimum required to display a page, the content title is already included in the output.
-
-```smarty
-{include file='header'}
-
-<div class="section">
- Hello World!
-</div>
-
-{include file='footer'}
-```
-
-## Full Example
-
-```smarty
-{*
- The page title is automatically set using the page definition, avoid setting it if you can!
- If you really need to modify the title, you can still reference the original title with:
- {$__wcf->getActivePage()->getTitle()}
-*}
-{capture assign='pageTitle'}Custom Page Title{/capture}
-
-{*
- NOTICE: The content header goes here, see the section after this to learn more.
-*}
-
-{* you must not use `headContent` for JavaScript *}
-{capture assign='headContent'}
- <link rel="alternate" type="application/rss+xml" title="{lang}wcf.global.button.rss{/lang}" href="…">
-{/capture}
-
-{* optional, content will be added to the top of the left sidebar *}
-{capture assign='sidebarLeft'}
- …
-
- {event name='boxes'}
-{/capture}
-
-{* optional, content will be added to the top of the right sidebar *}
-{capture assign='sidebarRight'}
- …
-
- {event name='boxes'}
-{/capture}
-
-{capture assign='headerNavigation'}
- <li><a href="#" title="Custom Button" class="jsTooltip"><span class="icon icon16 fa-check"></span> <span class="invisible">Custom Button</span></a></li>
-{/capture}
-
-{include file='header'}
-
-{hascontent}
- <div class="paginationTop">
- {content}
- {pages …}
- {/content}
- </div>
-{/hascontent}
-
-{* the actual content *}
-<div class="section">
- …
-</div>
-
-<footer class="contentFooter">
- {* skip this if you're not using any pagination *}
- {hascontent}
- <div class="paginationBottom">
- {content}{@$pagesLinks}{/content}
- </div>
- {/hascontent}
-
- <nav class="contentFooterNavigation">
- <ul>
- <li><a href="…" class="button"><span class="icon icon16 fa-plus"></span> <span>Custom Button</span></a></li>
- {event name='contentFooterNavigation'}
- </ul>
- </nav>
-</footer>
-
-<script data-relocate="true">
- /* any JavaScript code you need */
-</script>
-
-{* do not include `</body></html>` here, the footer template is the last bit of code! *}
-{include file='footer'}
-```
-
-### Content Header
-
-There are two different methods to set the content header, one sets only the actual values, but leaves the outer HTML untouched, that is generated by the `header` template. This is the recommended approach and you should avoid using the alternative method whenever possible.
-
-#### Recommended Approach
-
-```smarty
-{* This is automatically set using the page data and should not be set manually! *}
-{capture assign='contentTitle'}Custom Content Title{/capture}
-
-{capture assign='contentDescription'}Optional description that is displayed right after the title.{/capture}
-
-{capture assign='contentHeaderNavigation'}List of navigation buttons displayed right next to the title.{/capture}
-```
-
-#### Alternative
-
-```smarty
-{capture assign='contentHeader'}
- <header class="contentHeader">
- <div class="contentHeaderTitle">
- <h1 class="contentTitle">Custom Content Title</h1>
- <p class="contentHeaderDescription">Custom Content Description</p>
- </div>
-
- <nav class="contentHeaderNavigation">
- <ul>
- <li><a href="{link controller='CustomController'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>Custom Button</span></a></li>
- {event name='contentHeaderNavigation'}
- </ul>
- </nav>
- </header>
-{/capture}
-```
+++ /dev/null
----
-title: Migrating from WSC 3.0 - CSS
-sidebar: sidebar
-permalink: migration_wsc-30_css.html
-folder: migration/wsc-30
----
-
-## New Style Variables
-
-{% include callout.html content="The new style variables are only applied to styles that have the compatibility set to WSC 3.1" type="info" %}
-
-### wcfContentContainer
-
-The page content is encapsulated in a new container that wraps around the inner content, but excludes the sidebars, header and page navigation elements.
-
- * `$wcfContentContainerBackground` - background color
- * `$wcfContentContainerBorder` - border color
-
-### wcfEditorButton
-
-These variables control the appearance of the editor toolbar and its buttons.
-
- * `$wcfEditorButtonBackground` - button and toolbar background color
- * `$wcfEditorButtonBackgroundActive` - active button background color
- * `$wcfEditorButtonText` - text color for available buttons
- * `$wcfEditorButtonTextActive` - text color for active buttons
- * `$wcfEditorButtonTextDisabled` - text color for disabled buttons
-
-## Color Variables in `alert.scss`
-
-The color values for `<small class="innerError">` used to be hardcoded values, but have now been changed to use the values for error messages (`wcfStatusError*`) instead.
-
-{% include links.html %}
+++ /dev/null
----
-title: Migrating from WSC 3.0 - JavaScript
-sidebar: sidebar
-permalink: migration_wsc-30_javascript.html
-folder: migration/wsc-30
----
-
-## Accelerated Guest View / Tiny Builds
-
-The new tiny builds are highly optimized variants of existing JavaScript files and modules, aiming for significant performance improvements for guests and search engines alike. This is accomplished by heavily restricting page interaction to read-only actions whenever possible, which in return removes the need to provide certain JavaScript modules in general.
-
-For example, disallowing guests to write any formatted messages will in return remove the need to provide the WYSIWYG editor at all. But it doesn't stop there, there are a lot of other modules that provide additional features for the editor, and by excluding the editor, we can also exclude these modules too.
-
-Long story short, the tiny mode guarantees that certain actions will never be carried out by guests or search engines, therefore some modules are not going to be needed by them ever.
-
-### Code Templates for Tiny Builds
-
-The following examples assume that you use the virtual constant `COMPILER_TARGET_DEFAULT` as a switch for the optimized code path. This is also the constant used by the official [build scripts for JavaScript files](https://github.com/WoltLab/WCF/tree/master/extra).
-
-We recommend that you provide a mock implementation for existing code to ensure 3rd party compatibility. It is enough to provide a bare object or class that exposes the original properties using the same primitive data types. This is intended to provide a soft-fail for implementations that are not aware of the tiny mode yet, but is not required for classes that did not exist until now.
-
-#### Legacy JavaScript
-
-```js
-if (COMPILER_TARGET_DEFAULT) {
- WCF.Example.Foo = {
- makeSnafucated: function() {
- return "Hello World";
- }
- };
-
- WCF.Example.Bar = Class.extend({
- foobar: "baz",
-
- foo: function($bar) {
- return $bar + this.foobar;
- }
- });
-}
-else {
- WCF.Example.Foo = {
- makeSnafucated: function() {}
- };
-
- WCF.Example.Bar = Class.extend({
- foobar: "",
- foo: function() {}
- });
-}
-```
-
-#### require.js Modules
-
-```js
-define(["some", "fancy", "dependencies"], function(Some, Fancy, Dependencies) {
- "use strict";
-
- if (!COMPILER_TARGET_DEFAULT) {
- var Fake = function() {};
- Fake.prototype = {
- init: function() {},
- makeSnafucated: function() {}
- };
- return Fake;
- }
-
- function MyAwesomeClass(niceArgument) { this.init(niceArgument); }
- MyAwesomeClass.prototype = {
- init: function(niceArgument) {
- if (niceArgument) {
- this.makeSnafucated();
- }
- },
-
- makeSnafucated: function() {
- console.log("Hello World");
- }
- }
-
- return MyAwesomeClass;
-});
-```
-
-### Including tinified builds through `{js}`
-
-The `{js}` template-plugin has been updated to include support for tiny builds controlled through the optional flag `hasTiny=true`:
-
-```
-{js application='wcf' file='WCF.Example' hasTiny=true}
-```
-
-This line generates a different output depending on the debug mode and the user login-state.
-
-## Real Error Messages for AJAX Responses
-
-The `errorMessage` property in the returned response object for failed AJAX requests contained an exception-specific but still highly generic error message. This issue has been around for quite a long time and countless of implementations are relying on this false behavior, eventually forcing us to leave the value unchanged.
-
-This problem is solved by adding the new property `realErrorMessage` that exposes the message exactly as it was provided and now matches the value that would be displayed to users in traditional forms.
-
-### Example Code
-
-```js
-define(['Ajax'], function(Ajax) {
- return {
- // ...
- _ajaxFailure: function(responseData, responseText, xhr, requestData) {
- console.log(responseData.realErrorMessage);
- }
- // ...
- };
-});
-```
-
-## Simplified Form Submit in Dialogs
-
-Forms embedded in dialogs often do not contain the HTML `<form>`-element and instead rely on JavaScript click- and key-handlers to emulate a `<form>`-like submit behavior. This has spawned a great amount of nearly identical implementations that all aim to handle the form submit through the `Enter`-key, still leaving some dialogs behind.
-
-WoltLab Suite 3.1 offers automatic form submit that is enabled through a set of specific conditions and data attributes:
-
- 1. There must be a submit button that matches the selector `.formSubmit > input[type="submit"], .formSubmit > button[data-type="submit"]`.
- 2. The dialog object provided to `UiDialog.open()` implements the method `_dialogSubmit()`.
- 3. Input fields require the attribute `data-dialog-submit-on-enter="true"` to be set, the `type` must be one of `number`, `password`, `search`, `tel`, `text` or `url`.
-
-Clicking on the submit button or pressing the `Enter`-key in any watched input field will start the submit process. This is done automatically and does not require a manual interaction in your code, therefore you should not bind any click listeners on the submit button yourself.
-
-Any input field with the `required` attribute set will be validated to contain a non-empty string after processing the value with `String.prototype.trim()`. An empty field will abort the submit process and display a visible error message next to the offending field.
-
-## Helper Function for Inline Error Messages
-
-Displaying inline error messages on-the-fly required quite a few DOM operations that were quite simple but also super repetitive and thus error-prone when incorrectly copied over. The global helper function `elInnerError()` was added to provide a simple and consistent behavior of inline error messages.
-
-You can display an error message by invoking `elInnerError(elementRef, "Your Error Message")`, it will insert a new `<small class="innerError">` and sets the given message. If there is already an inner error present, then the message will be replaced instead.
-
-Hiding messages is done by setting the 2nd parameter to `false` or an empty string:
-
- * `elInnerError(elementRef, false)`
- * `elInnerError(elementRef, '')`
-
-The special values `null` and `undefined` are supported too, but their usage is discouraged, because they make it harder to understand the intention by reading the code:
-
- * `elInnerError(elementRef, null)`
- * `elInnerError(elementRef)`
-
-### Example Code
-
-```js
-require(['Language'], function(Language)) {
- var input = elBySel('input[type="text"]');
- if (input.value.trim() === '') {
- // displays a new inline error or replaces the message if there is one already
- elInnerError(input, Language.get('wcf.global.form.error.empty'));
- }
- else {
- // removes the inline error if it exists
- elInnerError(input, false);
- }
-
- // the above condition is equivalent to this:
- elInnerError(input, (input.value.trim() === '' ? Language.get('wcf.global.form.error.empty') : false));
-}
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Migrating from WSC 3.0 - Package Components
-sidebar: sidebar
-permalink: migration_wsc-30_package.html
-folder: migration/wsc-30
----
-
-## Cronjob Scheduler uses Server Timezone
-
-The execution time of cronjobs was previously calculated based on the coordinated universal time (UTC). This was changed in WoltLab Suite 3.1 to use the server timezone or, to be precise, the default timezone set in the administration control panel.
-
-## Exclude Pages from becoming a Landing Page
-
-Some pages do not qualify as landing page, because they're designed around specific expectations that aren't matched in all cases. Examples include the user control panel and its sub-pages that cannot be accessed by guests and will therefore break the landing page for those. While it is somewhat to be expected from control panel pages, there are enough pages that fall under the same restrictions, but aren't easily recognized as such by an administrator.
-
-You can exclude these pages by adding `<excludeFromLandingPage>1</excludeFromLandingPage>` (case-sensitive) to the relevant pages in your `page.xml`.
-
-### Example Code
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
- <import>
- <page identifier="com.example.foo.Bar">
- <!-- ... -->
- <excludeFromLandingPage>1</excludeFromLandingPage>
- <!-- ... -->
- </page>
- </import>
-</data>
-```
-
-## New Package Installation Plugin for Media Providers
-
-Please refer to the documentation of the [`mediaProvider.xml`][package_pip_media-provider] to learn more.
-
-## Limited Forward-Compatibility for Plugins
-
-Please refer to the documentation of the [`<compatibility>`](package_package-xml.html#compatibility) tag in the `package.xml`.
-
-{% include links.html %}
+++ /dev/null
----
-title: Migrating from WSC 3.0 - PHP
-sidebar: sidebar
-permalink: migration_wsc-30_php.html
-folder: migration/wsc-30
----
-
-## Approval-System for Comments
-
-Comments can now be set to require approval by a moderator before being published. This feature is disabled by default if you do not provide a permission in the manager class, enabling it requires a new permission that has to be provided in a special property of your manage implementation.
-
-```php
-<?php
-class ExampleCommentManager extends AbstractCommentManager {
- protected $permissionAddWithoutModeration = 'foo.bar.example.canAddCommentWithoutModeration';
-}
-```
-
-## Raw HTML in User Activity Events
-
-User activity events were previously encapsulated inside `<div class="htmlContent">…</div>`, with impacts on native elements such as lists. You can now disable the class usage by defining your event as raw HTML:
-
-```php
-<?php
-class ExampleUserActivityEvent {
- // enables raw HTML for output, defaults to `false`
- protected $isRawHtml = true;
-}
-```
-
-## Permission to View Likes of an Object
-
-Being able to view the like summary of an object was restricted to users that were able to like the object itself. This creates situations where the object type in general is likable, but the particular object cannot be liked by the current users, while also denying them to view the like summary (but it gets partly exposed through the footer note/summary!).
-
-Implement the interface `\wcf\data\like\IRestrictedLikeObjectTypeProvider` in your object provider to add support for this new permission check.
-
-```php
-<?php
-class LikeableExampleProvider extends ExampleProvider implements IRestrictedLikeObjectTypeProvider, IViewableLikeProvider {
- public function canViewLikes(ILikeObject $object) {
- // perform your permission checks here
- return true;
- }
-}
-```
-
-## Developer Tools: Sync Feature
-
-The synchronization feature of the newly added developer tools works by invoking a package installation plugin (PIP) outside of a regular installation, while simulating the basic environment that is already exposed by the API.
-
-However, not all PIPs qualify for this kind of execution, especially because it could be invoked multiple times in a row by the user. This is solved by requiring a special marking for PIPs that have no side-effects (= idempotent) when invoked any amount of times with the same arguments.
-
-There's another feature that allows all matching PIPs to be executed in a row using a single button click. In order to solve dependencies on other PIPs, any implementing PIP must also provide the method `getSyncDependencies()` that returns the dependent PIPs in an arbitrary order.
-
-```php
-<?php
-class ExamplePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
- public static function getSyncDependencies() {
- // provide a list of dependent PIPs in arbitrary order
- return [];
- }
-}
-```
-
-## Media Providers
-
-Media providers were added through regular SQL queries in earlier versions, but this is neither convenient, nor did it offer a reliable method to update an existing provider. WoltLab Suite 3.1 adds a new `mediaProvider`-PIP that also offers a `className` parameter to off-load the result evaluation and HTML generation.
-
-### Example Implementation
-
-#### mediaProvider.xml
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/mediaProvider.xsd">
- <import>
- <provider name="example">
- <title>Example Provider</title>
- <regex><![CDATA[https?://example.com/watch?v=(?P<ID>[a-zA-Z0-9])]]></regex>
- <className><![CDATA[wcf\system\bbcode\media\provider\ExampleBBCodeMediaProvider]]></className>
- </provider>
- </import>
-</data>
-```
-
-#### PHP Callback
-
-The full match is provided for `$url`, while any capture groups from the regular expression are assigned to `$matches`.
-
-```php
-<?php
-class ExampleBBCodeMediaProvider implements IBBCodeMediaProvider {
- public function parse($url, array $matches = []) {
- return "final HTML output";
- }
-}
-```
-
-## Re-Evaluate HTML Messages
-
-{% include callout.html content="You need to manually set the disallowed bbcodes in order to avoid unintentional bbcode evaluation. Please see [this commit](https://github.com/WoltLab/WCF/commit/7e058783da1378dda5393a9bb4df9cfe94e5b394) for a reference implementation inside worker processes." type="warning" %}
-
-The HtmlInputProcessor only supported two ways to handle an existing HTML message:
-
- 1. Load the string through `process()` and run it through the validation and sanitation process, both of them are rather expensive operations and do not qualify for rebuild data workers.
- 2. Detect embedded content using `processEmbeddedContent()` which bypasses most tasks that are carried out by `process()` which aren't required, but does not allow a modification of the message.
-
-The newly added method `reprocess($message, $objectType, $objectID)` solves this short-coming by offering a full bbcode and text re-evaluation while bypassing any input filters, assuming that the input HTML was already filtered previously.
-
-### Example Usage
-
-```php
-<?php
-// rebuild data workers tend to contain code similar to this:
-foreach ($this->objectList as $message) {
- // ...
- if (!$message->enableHtml) {
- // ...
- }
- else {
- // OLD:
- $this->getHtmlInputProcessor()->processEmbeddedContent($message->message, 'com.example.foo.message', $message->messageID);
-
- // REPLACE WITH:
- $this->getHtmlInputProcessor()->reprocess($message->message, 'com.example.foo.message', $message->messageID);
- $data['message'] = $this->getHtmlInputProcessor()->getHtml();
- }
- // ...
-}
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Migrating from WSC 3.0 - Templates
-sidebar: sidebar
-permalink: migration_wsc-30_templates.html
-folder: migration/wsc-30
----
-
-## Comment-System Overhaul
-
-{% include callout.html content="Unfortunately, there has been a breaking change related to the creation of comments. You need to apply the changes below before being able to create new comments." type="danger" %}
-
-### Adding Comments
-
-Existing implementations need to include a new template right before including the generic `commentList` template.
-
-```html
-<ul id="exampleCommentList" class="commentList containerList" data-...>
- {include file='commentListAddComment' wysiwygSelector='exampleCommentListAddComment'}
- {include file='commentList'}
-</ul>
-```
-
-## Redesigned ACP User List
-
-Custom interaction buttons were previously added through the template event `rowButtons` and were merely a link-like element with an icon inside. This is still valid and supported for backwards-compatibility, but it is recommend to adapt to the new drop-down-style options using the new template event `dropdownItems`.
-
-```html
-<!-- button for usage with the `rowButtons` event -->
-<span class="icon icon16 fa-list jsTooltip" title="Button Title"></span>
-
-<!-- new drop-down item for the `dropdownItems` event -->
-<li><a href="#" class="jsMyButton">Button Title</a></li>
-```
-
-## Sidebar Toogle-Buttons on Mobile Device
-
-{% include callout.html content="You cannot override the button label for sidebars containing navigation menus." type="info" %}
-
-The page sidebars are automatically collapsed and presented as one or, when both sidebar are present, two condensed buttons. They use generic sidebar-related labels when open or closed, with the exception of embedded menus which will change the button label to read "Show/Hide Navigation".
-
-You can provide a custom label before including the sidebars by assigning the new labels to a few special variables:
-
-```html
-{assign var='__sidebarLeftShow' value='Show Left Sidebar'}
-{assign var='__sidebarLeftHide' value='Hide Left Sidebar'}
-{assign var='__sidebarRightShow' value='Show Right Sidebar'}
-{assign var='__sidebarRightHide' value='Hide Right Sidebar'}
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Migrating from WSC 3.1 - Form Builder
-sidebar: sidebar
-permalink: migration_wsc-31_form-builder.html
-folder: migration/wsc-31
-parent: migration_wsc-31_php
----
-
-## Example: Two Text Form Fields
-
-As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the [first part of the tutorial series](tutorial_tutorial-series_part-1-base-structure.html) will be updated to the new form builder API.
-This form is the perfect first examples as it is very simple with only two text fields whose only restriction is that they have to be filled out and that their values may not be longer than 255 characters each.
-
-As a reminder, here are the two relevant PHP files and the relevant template file:
-
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonAddForm_old.class.php %}
-{% endhighlight %}
-
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonEditForm_old.class.php %}
-{% endhighlight %}
-
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/personAdd_old.tpl %}
-{% endhighlight %}
-
-Updating the template is easy as the complete form is replace by a single line of code:
-
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/personAdd_new.tpl %}
-{% endhighlight %}
-
-`PersonEditForm` also becomes much simpler:
-only the edited `Person` object must be read:
-
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonEditForm_new.class.php %}
-{% endhighlight %}
-
-Most of the work is done in `PersonAddForm`:
-
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonAddForm_new.class.php %}
-{% endhighlight %}
-
-But, as you can see, the number of lines almost decreased by half.
-All changes are due to extending `AbstractFormBuilderForm`:
-
-- `$formAction` is added and set to `create` as the form is used to create a new person.
- In the edit form, `$formAction` has not to be set explicitly as it is done automatically if a `$formObject` is set.
-- `$objectActionClass` is set to `PersonAction::class` and is the class name of the used `AbstractForm::$objectAction` object to create and update the `Person` object.
-- `AbstractFormBuilderForm::createForm()` is overridden and the form contents are added:
- a form container representing the `div.section` element from the old version and the two form fields with the same ids and labels as before.
- The contents of the old `validate()` method is put into two method calls:
- `required()` to ensure that the form is filled out and `maximumLength(255)` to ensure that the names are not longer than 255 characters.
+++ /dev/null
----
-title: Migrating from WSC 3.1 - Like System
-sidebar: sidebar
-permalink: migration_wsc-31_like.html
-folder: migration/wsc-31
-parent: migration_wsc-31_php
----
-
-## Introduction
-
-With version 5.2 of WoltLab Suite Core the like system was completely replaced by the new reactions system. This makes it necessary to make some adjustments to existing code so that your plugin integrates completely into the new system. However, we have kept these adjustments as small as possible so that it is possible to use the reaction system with slight restrictions even without adjustments.
-
-## Limitations if no adjustments are made to the existing code
-
-If no adjustments are made to the existing code, the following functions are not available:
-* Notifications about reactions/likes
-* Recent Activity Events for reactions/likes
-
-## Migration
-### Notifications
-#### Mark notification as compatible
-Since there are no more likes with the new version, it makes no sense to send notifications about it. Instead of notifications about likes, notifications about reactions are now sent. However, this only changes the notification text and not the notification itself. To update the notification, we first add the interface `\wcf\data\reaction\object\IReactionObject` to the `\wcf\data\like\object\ILikeObject` object (e.g. in WoltLab Suite Forum we added the interface to the class `\wbb\data\post\LikeablePost`). After that the object is marked as "compatible with WoltLab Suite Core 5.2" and notifications about reactions are sent again.
-
-#### Language Variables
-Next, to display all reactions for the current notification in the notification text, we include the trait `\wcf\system\user\notification\event\TReactionUserNotificationEvent` in the user notification event class (typically named like `*LikeUserNotificationEvent`). These trait provides a new function that reads out and groups the reactions. The result of this function must now only be passed to the language variable. The name "reactions" is typically used as the variable name for the language variable.
-
-As a final step, we only need to change the language variables themselves. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.
-
-##### English
-
-`{prefix}.like.title`
-```
-Reaction to a {objectName}
-```
-
-`{prefix}.like.title.stacked`
-
-```
-{#$count} users reacted to your {objectName}
-```
-
-`{prefix}.like.message`
-```
-{@$author->getAnchorTag()} reacted to your {objectName} ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
-```
-
-`{prefix}.like.message.stacked`
-
-```
-{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} and {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} and {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} and {#$others} others{/if} reacted to your {objectName} ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
-```
-
-`wcf.user.notification.{objectTypeName}.like.notification.like`
-```
-Notify me when someone reacted to my {objectName}
-```
-
-##### German
-
-`{prefix}.like.title`
-```
-Reaktion auf einen {objectName}
-```
-
-`{prefix}.like.title.stacked`
-
-```
-{#$count} Benutzern haben auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert
-```
-
-`{prefix}.like.message`
-```
-{@$author->getAnchorTag()} hat auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
-```
-
-`{prefix}.like.message.stacked`
-
-```
-{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} und {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} und {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} und {#$others} weitere{/if} haben auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}).
-```
-
-`wcf.user.notification.{object_type_name}.like.notification.like`
-```
-Jemandem hat auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert
-```
-
-### Recent Activity
-
-To adjust entries in the Recent Activity, only three small steps are necessary. First we pass the concrete reaction to the language variable, so that we can use the reaction object there. To do this, we add the following variable to the text of the `\wcf\system\user\activity\event\IUserActivityEvent` object: `$event->reactionType`. Typically we name the variable `reactionType`. In the second step, we mark the event as compatible. Therefore we set the parameter `supportsReactions` in the [`objectType.xml`](package_pip_object-type) to `1`. So for example the entry looks like this:
-
-```xml
-<type>
- <name>com.woltlab.example.likeableObject.recentActivityEvent</name>
- <definitionname>com.woltlab.wcf.user.recentActivityEvent</definitionname>
- <classname>wcf\system\user\activity\event\LikeableObjectUserActivityEvent</classname>
- <supportsReactions>1</supportsReactions>
-</type>
-```
-
-Finally we modify our language variable. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.
-
-#### English
-`wcf.user.recentActivity.{object_type_name}.recentActivityEvent`
-```
-Reaction ({objectName})
-```
-
-_Your language variable for the recent activity text_
-```
-Reacted with <span title="{$reactionType->getTitle()}" class="jsTooltip">{@$reactionType->renderIcon()}</span> to the {objectName}.
-```
-
-#### German
-`wcf.user.recentActivity.{objectTypeName}.recentActivityEvent`
-```
-Reaktion ({objectName})
-```
-
-_Your language variable for the recent activity text_
-```
-Hat mit <span title="{$reactionType->getTitle()}" class="jsTooltip">{@$reactionType->renderIcon()}</span> auf {objectName} reagiert.
-```
-
-### Comments
-If comments send notifications, they must also be updated. The language variables are changed in the same way as described in the section [Notifications / Language](migration_wsc-31_like.html#Language-Variables). After that comment must be marked as compatible. Therefore we set the parameter `supportsReactions` in the [`objectType.xml`](package_pip_object-type) to `1`. So for example the entry looks like this:
-
-```xml
-<type>
- <name>com.woltlab.wcf.objectComment.response.like.notification</name>
- <definitionname>com.woltlab.wcf.notification.objectType</definitionname>
- <classname>wcf\system\user\notification\object\type\LikeUserNotificationObjectType</classname>
- <category>com.woltlab.example</category>
- <supportsReactions>1</supportsReactions>
-</type>
-```
-
-## Forward Compatibility
-
-So that these changes also work in older versions of WoltLab Suite Core, the used classes and traits were backported with WoltLab Suite Core 3.0.22 and WoltLab Suite Core 3.1.10.
+++ /dev/null
----
-title: Migrating from WSC 3.1 - PHP
-sidebar: sidebar
-permalink: migration_wsc-31_php.html
-folder: migration/wsc-31
----
-
-## Form Builder
-
-WoltLab Suite Core 5.2 introduces a new, simpler and quicker way of creating forms:
-[form builder](php_api_form_builder.html).
-You can find examples of how to migrate existing forms to form builder [here](migration_wsc-31_form-builder.html).
-
-In the near future, to ensure backwards compatibility within WoltLab packages, we will only use form builder for new forms or for major rewrites of existing forms that would break backwards compatibility anyway.
-
-## Like System
-WoltLab Suite Core 5.2 replaced the like system with the reaction system. You can find the migration guide [here](migration_wsc-31_like.html).
-
-## User Content Providers
-
-User content providers help the WoltLab Suite to find user generated content. They provide a class with which you can find content from a particular user and delete objects.
-
-
-### PHP Class
-
-First, we create the PHP class that provides our interface to provide the data. The class must implement interface `wcf\system\user\content\provider\IUserContentProvider` in any case. Mostly we process data which is based on [`wcf\data\DatabaseObject`](php_database-objects.html). In this case, the WoltLab Suite provides an abstract class `wcf\system\user\content\provider\AbstractDatabaseUserContentProvider` that can be used to automatically generates the standardized classes to generate the list and deletes objects via the DatabaseObjectAction. For example, if we would create a content provider for comments, the class would look like this:
-
-```php
-<?php
-namespace wcf\system\user\content\provider;
-use wcf\data\comment\Comment;
-
-/**
- * User content provider for comments.
- *
- * @author Joshua Ruesweg
- * @copyright 2001-2018 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\System\User\Content\Provider
- * @since 5.2
- */
-class CommentUserContentProvider extends AbstractDatabaseUserContentProvider {
- /**
- * @inheritdoc
- */
- public static function getDatabaseObjectClass() {
- return Comment::class;
- }
-}
-```
-
-### Object Type
-
-Now the appropriate object type must be created for the class. This object type must be from the definition `com.woltlab.wcf.content.userContentProvider` and include the previous created class as FQN in the parameter `classname`. Also the following parameters can be used in the object type:
-
-#### `nicevalue`
-
-<span class="label label-info">Optional</span>
-
-The nice value is used to determine the order in which the remove content worker are execute the provider. Content provider with lower nice values are executed first.
-
-#### `hidden`
-
-<span class="label label-info">Optional</span>
-
-Specifies whether or not this content provider can be actively selected in the Content Remove Worker. If it cannot be selected, it will not be executed automatically!
-
-#### `requiredobjecttype`
-
-<span class="label label-info">Optional</span>
-
-The specified list of comma-separated object types are automatically removed during content removal when this object type is being removed. Heads up: The order of removal is undefined by default, specify a `nicevalue` if the order is important.
-
-
-
-## PHP Database API
-
-WoltLab Suite 5.2 introduces a new way to update the database scheme:
-[database PHP API](package_database-php-api.html).
\ No newline at end of file
+++ /dev/null
----
-title: Migrating from WSC 5.2 - Third Party Libraries
-sidebar: sidebar
-permalink: migration_wsc-52_libraries.html
-folder: migration/wsc-52
----
-
-## SCSS Compiler
-
-WoltLab Suite Core 5.3 upgrades the bundled SCSS compiler from `leafo/scssphp` 0.7.x to `scssphp/scssphp` 1.1.x.
-With the updated composer package name the SCSS compiler also received updated namespaces.
-WoltLab Suite Core adds a compatibility layer that maps the old namespace to the new namespace.
-The classes themselves appear to be drop-in compatible.
-Exceptions cannot be mapped using this compatibility layer, any `catch` blocks catching a specific Exception within the `Leafo` namespace will need to be adjusted.
-
-More details can be found in the [Pull Request WoltLab/WCF#3415](https://github.com/WoltLab/WCF/pull/3415).
-
-## Guzzle
-
-WoltLab Suite Core 5.3 ships with a bundled version of [Guzzle 6](http://docs.guzzlephp.org/en/6.5/).
-Going forward using Guzzle is the recommended way to perform HTTP requests.
-The `\wcf\util\HTTPRequest` class should no longer be used and transparently uses Guzzle under the hood.
-
-Use `\wcf\system\io\HttpFactory` to retrieve a correctly configured `GuzzleHttp\ClientInterface`.
-
-Please note that it is recommended to explicitely specify a `sink` when making requests, due to a PHP / Guzzle bug.
-Have a [look at the implementation in WoltLab/WCF](https://github.com/WoltLab/WCF/blob/ce163806c468763f6e3b04e4bf7318c6f8035737/wcfsetup/install/files/lib/util/HTTPRequest.class.php#L194-L195) for an example.
+++ /dev/null
----
-title: Migrating from WSC 5.2 - PHP
-sidebar: sidebar
-permalink: migration_wsc-52_php.html
-folder: migration/wsc-52
----
-
-## Comments
-
-The [`ICommentManager::isContentAuthor(Comment|CommentResponse): bool`](https://github.com/WoltLab/WCF/blob/aa96d34130d58c150a35ebd8936f09c830ccd685/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php#L151-L158) method was added.
-A default implementation that always returns `false` is available when inheriting from `AbstractCommentManager`.
-
-It is strongly recommended to implement `isContentAuthor` within your custom comment manager.
-An example implementation [can be found in `ArticleCommentManager`](https://github.com/WoltLab/WCF/blob/aa96d34130d58c150a35ebd8936f09c830ccd685/wcfsetup/install/files/lib/system/comment/manager/ArticleCommentManager.class.php#L213-L219).
-
-## Event Listeners
-
-The [`AbstractEventListener`](https://github.com/WoltLab/WCF/blob/75631516d45f9355f6c73d6375bf804d2abd587e/wcfsetup/install/files/lib/system/event/listener/AbstractEventListener.class.php) class was added.
-`AbstractEventListener` contains an implementation of `execute()` that will dispatch the event handling to dedicated methods based on the `$eventName` and, in case of the event object being an `AbstractDatabaseObjectAction`, the action name.
-
-Find the details of the dispatch behavior within the class comment of `AbstractEventListener`.
-
-## Email Activation
-
-Starting with WoltLab Suite 5.3 the user activation status is independent of the email activation status.
-A user can be activated even though their email address has not been confirmed, preventing emails being sent to these users.
-Going forward the new `User::isEmailConfirmed()` method should be used to check whether sending automated emails to this user is acceptable.
-If you need to check the user's activation status you should use the new method `User::pendingActivation()` instead of relying on `activationCode`.
-To check, which type of activation is missing, you can use the new methods `User::requiresEmailActivation()` and `User::requiresAdminActivation()`.
-
-## `*AddForm`
-
-WoltLab Suite 5.3 provides a new framework to allow the administrator to easily edit newly created objects by adding an edit link to the success message.
-To support this edit link two small changes are required within your `*AddForm`.
-
-1. Update the template.
-
- Replace:
- ```smarty
- {include file='formError'}
-
- {if $success|isset}
- <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
- {/if}
- ```
-
- With:
- ```smarty
- {include file='formNotice'}
- ```
-
-2. Expose `objectEditLink` to the template.
-
- Example (`$object` being the newly created object):
- ```php
- WCF::getTPL()->assign([
- 'success' => true,
- 'objectEditLink' => LinkHandler::getInstance()->getControllerLink(ObjectEditForm::class, ['id' => $object->objectID]),
- ]);
- ```
-
-## User Generated Links
-
-It is [recommended by search engines](https://support.google.com/webmasters/answer/96569) to mark up links within user generated content using the `rel="ugc"` attribute to indicate that they might be less trustworthy or spammy.
-
-WoltLab Suite 5.3 will automatically sets that attribute on external links during message output processing.
-Set the new `HtmlOutputProcessor::$enableUgc` property to `false` if the type of message is not user-generated content, but restricted to a set of trustworthy users.
-An example of such a type of message would be official news articles.
-
-If you manually generate links based off user input you need to specify the attribute yourself.
-The `$isUgc` attribute was added to [`StringUtil::getAnchorTag(string, string, bool, bool): string`](https://github.com/WoltLab/WCF/blob/af245d7b9bdb411a344f79c0a038350c1f103e70/wcfsetup/install/files/lib/util/StringUtil.class.php#L664-L673), allowing you to easily generate a correct anchor tag.
-
-If you need to specify additional HTML attributes for the anchor tag you can use the new [`StringUtil::getAnchorTagAttributes(string, bool): string`](https://github.com/WoltLab/WCF/blob/af245d7b9bdb411a344f79c0a038350c1f103e70/wcfsetup/install/files/lib/util/StringUtil.class.php#L691-L699) method to generate the anchor attributes that are dependent on the target URL.
-Specifically the attributes returned are the `class="externalURL"` attribute, the `rel="…"` attribute and the `target="…"` attribute.
-
-Within the template the [`{anchorAttributes}`](view_template-plugins.html#53-anchorattributes) template plugin is newly available.
-
-## Resource Management When Scaling Images
-
-It was discovered that the code holds references to scaled image resources for an unnecessarily long time, taking up memory.
-This becomes especially apparent when multiple images are scaled within a loop, reusing the same variable name for consecutive images.
-Unless the destination variable is explicitely cleared before processing the next image up to two images will be stored in memory concurrently.
-This possibly causes the request to exceed the memory limit or ImageMagick's internal resource limits, even if sufficient resources would have been available to scale the current image.
-
-Starting with WoltLab Suite 5.3 it is recommended to clear image handles as early as possible.
-The usual pattern of creating a thumbnail for an existing image would then look like this:
-
-```php
-<?php
-foreach ([ 200, 500 ] as $size) {
- $adapter = ImageHandler::getInstance()->getAdapter();
- $adapter->loadFile($src);
- $thumbnail = $adapter->createThumbnail(
- $size,
- $size,
- true
- );
- $adapter->writeImage($thumbnail, $destination);
- // New: Clear thumbnail as soon as possible to free up the memory.
- $thumbnail = null;
-}
-```
-
-Refer to [WoltLab/WCF#3505](https://github.com/WoltLab/WCF/pull/3505) for additional details.
-
-## Toggle for Accelerated Mobile Pages (AMP)
-
-Controllers delivering AMP versions of pages have to check for the new option `MODULE_AMP` and the templates of the non-AMP versions have to also check if the option is enabled before outputting the `<link rel="amphtml" />` element.
+++ /dev/null
----
-title: Migrating from WSC 5.2 - Templates and Languages
-sidebar: sidebar
-permalink: migration_wsc-52_templates.html
-folder: migration/wsc-52
----
-
-## `{jslang}`
-
-Starting with WoltLab Suite 5.3 the `{jslang}` template plugin is available.
-`{jslang}` works like `{lang}`, with the difference that the result is automatically encoded for use within a single quoted JavaScript string.
-
-Before:
-
-```smarty
-<script>
-require(['Language', /* … */], function(Language, /* … */) {
- Language.addObject({
- 'app.foo.bar': '{lang}app.foo.bar{/lang}',
- });
-
- // …
-});
-</script>
-```
-
-After:
-
-```smarty
-<script>
-require(['Language', /* … */], function(Language, /* … */) {
- Language.addObject({
- 'app.foo.bar': '{jslang}app.foo.bar{/jslang}',
- });
-
- // …
-});
-</script>
-```
-
-## Template Plugins
-
-The [`{anchor}`](view_template-plugins.html#53-anchor), [`{plural}`](view_template-plugins.html#53-plural), and [`{user}`](view_template-plugins.html#53-user) template plugins have been added.
-
-## Notification Language Items
-
-In addition to using the new template plugins mentioned above, language items for notifications have been further simplified.
-
-As the whole notification is clickable now, all `a` elements have been replaced with `strong` elements in notification messages.
-
-The template code to output reactions has been simplified by introducing helper methods:
-
-```smarty
-{* old *}
-{implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}×{#$count}{/implode}
-{* new *}
-{@$__wcf->getReactionHandler()->renderInlineList($reactions)}
-
-{* old *}
-<span title="{$like->getReactionType()->getTitle()}" class="jsTooltip">{@$like->getReactionType()->renderIcon()}</span>
-{* new *}
-{@$like->render()}
-```
-
-Similarly, showing labels is now also easier due to the new `render` method:
-
-```smarty
-{* old *}
-<span class="label badge{if $label->getClassNames()} {$label->getClassNames()}{/if}">{$label->getTitle()}</span>
-{* new *}
-{@$label->render()}
-```
-
-The commonly used template code
-
-```smarty
-{if $count < 4}{@$authors[0]->getAnchorTag()}{if $count != 1}{if $count == 2 && !$guestTimesTriggered} and {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3}{if !$guestTimesTriggered} and {else}, {/if} {@$authors[2]->getAnchorTag()}{/if}{/if}{if $guestTimesTriggered} and {if $guestTimesTriggered == 1}a guest{else}guests{/if}{/if}{else}{@$authors[0]->getAnchorTag()}{if $guestTimesTriggered},{else} and{/if} {#$others} other users {if $guestTimesTriggered}and {if $guestTimesTriggered == 1}a guest{else}guests{/if}{/if}{/if}
-```
-
-in stacked notification messages can be replaced with a new language item:
-
-```smarty
-{@'wcf.user.notification.stacked.authorList'|language}
-```
-
-## Popovers
-
-Popovers provide additional information of the linked object when a user hovers over a link.
-We unified the approach for such links:
-
-1. The relevant DBO class implements `wcf\data\IPopoverObject`.
-2. The relevant DBO action class implements `wcf\data\IPopoverAction` and the `getPopover()` method returns an array with popover content.
-3. Globally available, `WoltLabSuite/Core/Controller/Popover` is initialized with the relevant data.
-4. Links are created with the `anchor` template plugin with an additional `class` attribute whose value is the return value of `IPopoverObject::getPopoverLinkClass()`.
-
-Example:
-
-```php
-class Foo extends DatabaseObject implements IPopoverObject {
- public function getPopoverLinkClass() {
- return 'fooLink';
- }
-}
-
-class FooAction extends AbstractDatabaseObjectAction implements IPopoverAction {
- public function validateGetPopover() {
- // …
- }
-
- public function getPopover() {
- return [
- 'template' => '…',
- ];
- }
-}
-```
-
-```js
-require(['WoltLabSuite/Core/Controller/Popover'], function(ControllerPopover) {
- ControllerPopover.init({
- className: 'fooLink',
- dboAction: 'wcf\\data\\foo\\FooAction',
- identifier: 'com.woltlab.wcf.foo'
- });
-});
-```
-
-```smarty
-{anchor object=$foo class='fooLink'}
-```
+++ /dev/null
----
-title: Migrating from WSC 5.3 - JavaScript
-sidebar: sidebar
-permalink: migration_wsc-53_javascript.html
-folder: migration/wsc-53
----
-
-## `WCF_CLICK_EVENT`
-
-For event listeners on click events, `WCF_CLICK_EVENT` is deprecated and should no longer be used.
-Instead, use `click` directly:
-
-```javascript
-// before
-element.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-
-// after
-element.addEventListener('click', (ev) => this._click(ev));
-```
+++ /dev/null
----
-title: Migrating from WSC 5.3 - Third Party Libraries
-sidebar: sidebar
-permalink: migration_wsc-53_libraries.html
-folder: migration/wsc-53
----
-
-## Guzzle
-
-The bundled Guzzle version was updated to Guzzle 7.
-No breaking changes are expected for simple uses.
-A detailed [Guzzle migration guide](https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#60-to-70) can be found in the Guzzle documentation.
-
-The explicit `sink` that was recommended in the [migration guide for WSC 5.2](migration_wsc-52_libraries.html#guzzle) can now be removed, as [the Guzzle issue #2735](https://github.com/guzzle/guzzle/issues/2735) was fixed in Guzzle 7.
-
-## Emogrifier / CSS Inliner
-
-The Emogrifier library was updated from version 2.2 to 5.0.
-This update comes with a breaking change, as the `Emogrifier` class was removed.
-With the updated Emogrifier library, the `CssInliner` class must be used instead.
-
-No compatibility layer was added for the `Emogrifier` class, as the Emogrifier library's purpose was to be used within the email subsystem of WoltLab Suite.
-In case you use Emogrifier directly within your own code, you will need to adjust the usage.
-Refer to the [Emogrifier CHANGELOG](https://github.com/MyIntervals/emogrifier/blob/v5.0.0/CHANGELOG.md) and [WoltLab/WCF #3738](https://github.com/WoltLab/WCF/pull/3738) if you need help making the necessary adjustments.
-
-If you only use Emogrifier indirectly by sending HTML mail via the email subsystem then you might notice unexpected visual changes due to the improved CSS support.
-Double check your CSS declarations and particularly the specificity of your selectors in these cases.
-
-## Constant Time Encoder
-
-WoltLab Suite 5.4 ships the [`paragonie/constant_time_encoding` library](https://github.com/paragonie/constant_time_encoding).
-It is recommended to use this library to perform encoding and decoding of secrets to prevent leaks via cache timing attacks.
-Refer to [the library author’s blog post](https://paragonie.com/blog/2016/06/constant-time-encoding-boring-cryptography-rfc-4648-and-you) for more background detail.
-
-For the common case of encoding the bytes taken from a CSPRNG in hexadecimal form, the required change would look like the following:
-
-Previously:
-
-```php
-<?php
-$encoded = hex2bin(random_bytes(16));
-```
-
-Now:
-
-```php
-<?php
-use ParagonIE\ConstantTime\Hex;
-
-// For security reasons you should add the backslash
-// to ensure you refer to the `random_bytes` function
-// within the global namespace and not a function
-// defined in the current namespace.
-$encoded = Hex::encode(\random_bytes(16));
-```
-
-Please refer to the documentation and source code of the `paragonie/constant_time_encoding` library to learn how to use the library with different encodings (e.g. base64).
+++ /dev/null
----
-title: Migrating from WSC 5.3 - PHP
-sidebar: sidebar
-permalink: migration_wsc-53_php.html
-folder: migration/wsc-53
----
-
-## Minimum requirements
-
-The minimum requirements have been increased to the following:
-
-- **PHP:** 7.2.24
-- **MySQL:** 5.7.31 or 8.0.19
-- **MariaDB:** 10.1.44
-
-Most notably PHP 7.2 contains usable support for scalar types by the addition of nullable types in PHP 7.1 and parameter type widening in PHP 7.2.
-
-It is recommended to make use of scalar types and other newly introduced features whereever possible.
-Please refer to the PHP documentation for details.
-
-## Flood Control
-
-To prevent users from creating massive amounts of contents in short periods of time, i.e., spam, existing systems already use flood control mechanisms to limit the amount of contents created within a certain period of time.
-With WoltLab Suite 5.4, we have added a general API that manages such rate limiting.
-Leveraging this API is easily done.
-
-1. Register an object type for the definition `com.woltlab.wcf.floodControl`: `com.example.foo.myContent`.
-2. Whenever the active user creates content of this type, call
- ```php
- FloodControl::getInstance()->registerContent('com.example.foo.myContent');
- ```
- You should only call this method if the user creates the content themselves.
- If the content is automatically created by the system, for example when copying / duplicating existing content, no activity should be registered.
-3. To check the last time when the active user created content of the relevant type, use
- ```php
- FloodControl::getInstance()->getLastTime('com.example.foo.myContent');
- ```
- If you want to limit the number of content items created within a certain period of time, for example within one day, use
- ```php
- $data = FloodControl::getInstance()->countContent('com.example.foo.myContent', new \DateInterval('P1D'));
- // number of content items created within the last day
- $count = $data['count'];
- // timestamp when the earliest content item was created within the last day
- $earliestTime = $data['earliestTime'];
- ```
- The method also returns `earliestTime` so that you can tell the user in the error message when they are able again to create new content of the relevant type.
- {% include callout.html content="Flood control entries are only stored for 31 days and older entries are cleaned up daily." type="info" %}
-
-The previously mentioned methods of `FloodControl` use the active user and the current timestamp as reference point.
-`FloodControl` also provides methods to register content or check flood control for other registered users or for guests via their IP address.
-For further details on these methods, please refer to the [documentation in the FloodControl class](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/flood/FloodControl.class.php).
-
-{% include callout.html content="Do not interact directly with the flood control database table but only via the `FloodControl` class!" type="warning" %}
-
-## PHP Database API
-
-The PHP API to add and change database tables during package installations and updates in the `wcf\system\database\table` namespace now also supports renaming existing table columns with the new `IDatabaseTableColumn::renameTo()` method:
-
-```php
-PartialDatabaseTable::create('wcf1_test')
- ->columns([
- NotNullInt10DatabaseTableColumn::create('oldName')
- ->renameTo('newName')
- ]);
-```
-
-{% include callout.html content="Like with every change to existing database tables, packages can only rename columns that they installed." type="info" %}
-
-## Captcha
-
-The reCAPTCHA v1 implementation was completely removed.
-This includes the `\wcf\system\recaptcha\RecaptchaHandler` class (not to be confused with the one in the `captcha` namespace).
-
-The reCAPTCHA v1 endpoints have already been turned off by Google and always return a HTTP 404.
-Thus the implementation was completely non-functional even before this change.
-
-See [WoltLab/WCF#3781](https://github.com/WoltLab/WCF/pull/3781) for details.
-
-## Search
-
-The generic implementation in the `AbstractSearchEngine::parseSearchQuery()` method was dangerous, because it did not have knowledge about the search engine’s specifics.
-The implementation was completely removed: `AbstractSearchEngine::parseSearchQuery()` now always throws a `\BadMethodCallException`.
-
-If you implemented a custom search engine and relied on this method, you can inline the previous implementation to preserve existing behavior.
-You should take the time to verify the rewritten queries against the manual of the search engine to make sure it cannot generate malformed queries or security issues.
-
-See [WoltLab/WCF#3815](https://github.com/WoltLab/WCF/issues/3815) for details.
+++ /dev/null
----
-title: Migrating from WSC 5.3 - Session Handling and Authentication
-sidebar: sidebar
-permalink: migration_wsc-53_session.html
-folder: migration/wsc-53
----
-
-WoltLab Suite 5.4 includes a completely refactored session handling.
-As long as you only interact with sessions via `WCF::getSession()`, especially when you perform read-only accesses, you should not notice any breaking changes.
-
-You might appreciate some of the new session methods if you process security sensitive data.
-
-## Summary and Concepts
-
-Most of the changes revolve around the removal of the legacy persistent login functionality and the assumption that every user has a single session only.
-Both aspects are related to each other.
-
-### Legacy Persistent Login
-
-The legacy persistent login was rather an automated login.
-Upon bootstrapping a session, it was checked whether the user had a cookie pair storing the user’s `userID` and (a single BCrypt hash of) the user’s password.
-If such a cookie pair exists and the BCrypt hash within the cookie matches the user’s password hash when hashed again, the session would immediately `changeUser()` to the respective user.
-
-This legacy persistent login was completely removed.
-Instead, any sessions that belong to an authenticated user will automatically be long-lived.
-These long-lived sessions expire no sooner than 14 days after the last activity, ensuring that the user continously stays logged in, provided that they visit the page at least once per fortnight.
-
-### Multiple Sessions
-
-To allow for a proper separation of these long-lived user sessions, WoltLab Suite now allows for multiple sessions per user.
-These sessions are completely unrelated to each other.
-Specifically, they do not share session variables and they expire independently.
-
-As the existing `wcf1_session` table is also used for the online lists and location tracking, it will be maintained on a best effort basis.
-It no longer stores any private session data.
-
-The actual sessions storing security sensitive information are in an unrelated location.
-They must only be accessed via the PHP API exposed by the `SessionHandler`.
-
-### Improved Authentication and Reauthentication
-
-WoltLab Suite 5.4 ships with multi-factor authentication support and a generic re-authentication implementation that can be used to verify the account owner’s presence.
-
-## Additions and Changes
-
-### Password Hashing
-
-WoltLab Suite 5.4 includes a new object-oriented password hashing framework that is modeled after PHP’s `password_*` API.
-Check [`PasswordAlgorithmManager`](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/authentication/password/PasswordAlgorithmManager.class.php) and [`IPasswordAlgorithm`](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/authentication/password/IPasswordAlgorithm.class.php) for details.
-
-The new default password hash is a standard BCrypt hash.
-All newly generated hashes in `wcf1_user.password` will now include a type prefix, instead of just passwords imported from other systems.
-
-### Session Storage
-
-The `wcf1_session` table will no longer be used for session storage.
-Instead, it is maintained for compatibility with existing online lists.
-
-The actual session storage is considered an implementation detail and you *must not* directly interact with the session tables.
-Future versions might support alternative session backends, such as Redis.
-
-{% include callout.html content="Do not interact directly with the session database tables but only via the `SessionHandler` class!" type="warning" %}
-
-### Reauthentication
-
-For security sensitive processing, you might want to ensure that the account owner is actually present instead of a third party accessing a session that was accidentally left logged in.
-
-WoltLab Suite 5.4 ships with a generic reauthentication framework.
-To request reauthentication within your controller you need to:
-
-1. Use the `wcf\system\user\authentication\TReauthenticationCheck` trait.
-2. Call:
- ```php
- $this->requestReauthentication(LinkHandler::getInstance()->getControllerLink(static::class, [
- /* additional parameters */
- ]));
- ```
-
-`requestReauthentication()` will check if the user has recently authenticated themselves.
-If they did, the request proceeds as usual.
-Otherwise, they will be asked to reauthenticate themselves.
-After the successful authentication, they will be redirected to the URL that was passed as the first parameter (the current controller within the example).
-
-Details can be found in [WoltLab/WCF#3775](https://github.com/WoltLab/WCF/pull/3775).
-
-### Multi-factor Authentication
-
-To implement multi-factor authentication securely, WoltLab Suite 5.4 implements the concept of a “pending user change”.
-The user will not be logged in (i.e. `WCF::getUser()->userID` returns `null`) until they authenticate themselves with their second factor.
-
-Requesting multi-factor authentication is done on an opt-in basis for compatibility reasons.
-If you perform authentication yourself and do not trust the authentication source to perform multi-factor authentication itself, you will need to adjust your logic to request multi-factor authentication from WoltLab Suite:
-
-Previously:
-
-```php
-WCF::getSession()->changeUser($targetUser);
-```
-
-Now:
-
-```php
-$isPending = WCF::getSession()->changeUserAfterMultifactorAuthentication($targetUser);
-if ($isPending) {
- // Redirect to the authentication form. The user will not be logged in.
- // Note: Do not use `getControllerLink` to support both the frontend as well as the ACP.
- HeaderUtil::redirect(LinkHandler::getInstance()->getLink('MultifactorAuthentication', [
- 'url' => /* Return To */,
- ]);
- exit;
-}
-// Proceed as usual. The user will be logged in.
-```
-
-#### Adding Multi-factor Methods
-
-Adding your own multi-factor method requires the implementation of a single object type:
-
-```xml
-<type>
- <name>com.example.multifactor.foobar</name>
- <definitionname>com.woltlab.wcf.multifactor</definitionname>
- <icon><!-- Font Awesome 4 Icon Name goes here. --></icon>
- <priority><!-- Determines the sort order, higher priority will be preferred for authentication. --></priority>
- <classname>wcf\system\user\multifactor\FoobarMultifactorMethod</classname>
-</type>
-```
-
-The given classname must implement the [`IMultifactorMethod`](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php) interface.
-
-As a self-contained example, you can find the initial implementation of the email multi-factor method in [WoltLab/WCF#3729](https://github.com/WoltLab/WCF/pull/3729).
-Please check [the version history](https://github.com/WoltLab/WCF/commits/master/wcfsetup/install/files/lib/system/user/multifactor/EmailMultifactorMethod.class.php) of the PHP class to make sure you do not miss important changes that were added later.
-
-{% include callout.html content="Multi-factor authentication is security sensitive.
-Make sure to carefully read the remarks in IMultifactorMethod for possible issues.
-Also make sure to carefully test your implementation against all sorts of incorrect input and consider attack vectors such as race conditions.
-It is strongly recommended to generously check the current state by leveraging assertions and exceptions." type="warning" %}
-
-## Deprecations and Removals
-
-### SessionHandler
-
-Most of the changes with regard to the new session handling happened in `SessionHandler`.
-Most notably, `SessionHandler` now is marked `final` to ensure proper encapsulation of data.
-
-A number of methods in `SessionHandler` are now deprecated and result in a noop.
-This change mostly affects methods that have been used to bootstrap the session, such as `setHasValidCookie()`.
-
-Additionally, accessing the following keys on the session is deprecated.
-They directly map to an existing method in another class and any uses can easily be updated:
-- `ipAddress`
-- `userAgent`
-- `requestURI`
-- `requestMethod`
-- `lastActivityTime`
-
-Refer to [the implementation](https://github.com/WoltLab/WCF/blob/439de4963c947c3569a0c584f795245f693155b0/wcfsetup/install/files/lib/system/session/SessionHandler.class.php#L168-L178) for details.
-
-### Cookies
-
-The `_userID`, `_password` and `_cookieHash` cookies will no longer be created nor consumed.
-
-### Virtual Sessions
-
-The virtual session logic existed to support multiple devices per single session in `wcf1_session`.
-Virtual sessions are no longer required with the refactored session handling.
-
-Anything related to virtual sessions has been completely removed as they are considered an implementation detail.
-This removal includes PHP classes and database tables.
-
-### Security Token Constants
-
-The security token constants are deprecated.
-Instead, the methods of `SessionHandler` should be used (e.g. `->getSecurityToken()`).
-Within templates, you should migrate to the `{csrfToken}` tag in place of `{@SECURITY_TOKEN_INPUT_TAG}`.
-The `{csrfToken}` tag is a drop-in replacement and was backported to WoltLab Suite 5.2+, allowing you to maintain compatibility across a broad range of versions.
-
-### PasswordUtil and Double BCrypt Hashes
-
-Most of the methods in PasswordUtil are deprecated in favor of the new password hashing framework.
+++ /dev/null
----
-title: Migrating from WSC 5.3 - Templates and Languages
-sidebar: sidebar
-permalink: migration_wsc-53_templates.html
-folder: migration/wsc-523
----
-
-## `{csrfToken}`
-
-Going forward, any uses of the `SECURITY_TOKEN_*` constants should be avoided.
-To reference the CSRF token (“Security Token”) within templates, the `{csrfToken}` template plugin was added.
-
-Before:
-
-```smarty
-{@SECURITY_TOKEN_INPUT_TAG}
-{link controller="Foo"}t={@SECURITY_TOKEN}{/link}
-```
-
-After:
-
-```smarty
-{csrfToken}
-{link controller="Foo"}t={csrfToken type=url}{/link} {* The use of the CSRF token in URLs is discouraged.
- Modifications should happen by means of a POST request. *}
-```
-
-The `{csrfToken}` plugin was backported to WoltLab Suite 5.2 and higher, allowing compatibility with a large range of WoltLab Suite branches.
-See [WoltLab/WCF #3612](https://github.com/WoltLab/WCF/pull/3612) for details.
+++ /dev/null
----
-title: Database PHP API
-permalink: package_database-php-api.html
-folder: package
-parent: package_pip
----
-
-{% include callout.html content="Available since WoltLab Suite 5.2." type="info" %}
-
-While the [sql](package_pip_sql.html) package installation plugin supports adding and removing tables, columns, and indices, it is not able to handle cases where the added table, column, or index already exist.
-We have added a new PHP-based API to manipulate the database scheme which can be used in combination with the [script](package_pip_script.html) package installation plugin that skips parts that already exist:
-
-```php
-$tables = [
- // TODO
-];
-
-(new DatabaseTableChangeProcessor(
- /** @var ScriptPackageInstallationPlugin $this */
- $this->installation->getPackage(),
- $tables,
- WCF::getDB()->getEditor())
-)->process();
-```
-
-All of the relevant components can be found in the `wcf\system\database\table` namespace.
-
-
-## Database Tables
-
-There are two classes representing database tables: `DatabaseTable` and `PartialDatabaseTable`.
-If a new table should be created, use `DatabaseTable`.
-In all other cases, `PartialDatabaseTable` should be used as it provides an additional save-guard against accidentally creating a new table by having a typo in the table name:
-If the tables does not already exist, a table represented by `PartialDatabaseTable` will cause an exception (while a `DatabaseTable` table will simply be created).
-
-To create a table, a `DatabaseTable` object with the table's name as to be created and table's columns, foreign keys and indices have to be specified:
-
-```php
-DatabaseTable::create('foo1_bar')
- ->columns([
- // columns
- ])
- ->foreignKeys([
- // foreign keys
- ])
- ->indices([
- // indices
- ])
-```
-
-To update a table, the same code as above can be used, except for `PartialDatabaseTable` being used instead of `DatabaseTable`.
-
-To drop a table, only the `drop()` method has to be called:
-
-```php
-PartialDatabaseTable::create('foo1_bar')
- ->drop()
-```
-
-
-## Columns
-
-To represent a column of a database table, you have to create an instance of the relevant column class found in the `wcf\system\database\table\column` namespace.
-Such instances are created similarly to database table objects using the `create()` factory method and passing the column name as the parameter.
-
-Every column type supports the following methods:
-
-- `defaultValue($defaultValue)` sets the default value of the column (default: none).
-- `drop()` to drop the column.
-- `notNull($notNull = true)` sets if the value of the column can be `NULL` (default: `false`).
-
-Depending on the specific column class implementing additional interfaces, the following methods are also available:
-
-- `IAutoIncrementDatabaseTableColumn::autoIncrement($autoIncrement = true)` sets if the value of the colum is auto-incremented.
-- `IDecimalsDatabaseTableColumn::decimals($decimals)` sets the number of decimals the column supports.
-- `IEnumDatabaseTableColumn::enumValues(array $values)` sets the predetermined set of valid values of the column.
-- `ILengthDatabaseTableColumn::length($length)` sets the (maximum) length of the column.
-
-Additionally, there are some additionally classes of commonly used columns with specific properties:
-
-- `DefaultFalseBooleanDatabaseTableColumn` (a `tinyint` column with length `1`, default value `0` and whose values cannot be `null`)
-- `DefaultTrueBooleanDatabaseTableColumn` (a `tinyint` column with length `0`, default value `0` and whose values cannot be `null`)
-- `NotNullInt10DatabaseTableColumn` (a `int` column with length `10` and whose values cannot be `null`)
-- `NotNullVarchar191DatabaseTableColumn` (a `varchar` column with length `191` and whose values cannot be `null`)
-- `NotNullVarchar255DatabaseTableColumn` (a `varchar` column with length `255` and whose values cannot be `null`)
-- `ObjectIdDatabaseTableColumn` (a `int` column with length `10`, whose values cannot be `null`, and whose values are auto-incremented)
-
-Examples:
-
-```php
-DefaultFalseBooleanDatabaseTableColumn::create('isDisabled')
-
-NotNullInt10DatabaseTableColumn::create('fooTypeID')
-
-SmallintDatabaseTableColumn::create('bar')
- ->length(5)
- ->notNull()
-```
-
-
-## Foreign Keys
-
-Foreign keys are represented by `DatabaseTableForeignKey` objects:
-
-```php
-DatabaseTableForeignKey::create()
- ->columns(['fooID'])
- ->referencedTable('wcf1_foo')
- ->referencedColumns(['fooID'])
- ->onDelete('CASCADE')
-```
-
-The supported actions for `onDelete()` and `onUpdate()` are `CASCADE`, `NO ACTION`, and `SET NULL`.
-To drop a foreign key, all of the relevant data to create the foreign key has to be present and the `drop()` method has to be called.
-
-`DatabaseTableForeignKey::create()` also supports the foreign key name as a parameter.
-If it is not present, `DatabaseTable::foreignKeys()` will automatically set one based on the foreign key's data.
-
-
-## Indices
-
-Indices are represented by `DatabaseTableIndex` objects:
-
-```php
-DatabaseTableIndex::create()
- ->type(DatabaseTableIndex::UNIQUE_TYPE)
- ->columns(['fooID'])
-```
-
-There are four different types: `DatabaseTableIndex::DEFAULT_TYPE` (default), `DatabaseTableIndex::PRIMARY_TYPE`, `DatabaseTableIndex::UNIQUE_TYPE`, and `DatabaseTableIndex::FULLTEXT_TYPE`.
-For primary keys, there is also the `DatabaseTablePrimaryIndex` class which automatically sets the type to `DatabaseTableIndex::PRIMARY_TYPE`.
-To drop a index, all of the relevant data to create the index has to be present and the `drop()` method has to be called.
-
-`DatabaseTableIndex::create()` also supports the index name as a parameter.
-If it is not present, `DatabaseTable::indices()` will automatically set one based on the index data.
+++ /dev/null
----
-title: package.xml
-sidebar: sidebar
-permalink: package_package-xml.html
-folder: package
----
-
-The `package.xml` is the core component of every package.
-It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<package name="com.example.package" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/package.xsd">
- <packageinformation>
- <packagename>Simple Package</packagename>
- <packagedescription>A simple package to demonstrate the package system of WoltLab Suite Core</packagedescription>
- <version>1.0.0</version>
- <date>2016-12-18</date>
- </packageinformation>
-
- <authorinformation>
- <author>YOUR NAME</author>
- <authorurl>http://www.example.com</authorurl>
- </authorinformation>
-
- <requiredpackages>
- <requiredpackage minversion="3.0.0">com.woltlab.wcf</requiredpackage>
- </requiredpackages>
-
- <excludedpackages>
- <excludedpackage version="6.0.0 Alpha 1">com.woltlab.wcf</excludedpackage>
- </excludedpackages>
-
- <instructions type="install">
- <instruction type="file" />
- <instruction type="template">templates.tar</instruction>
- </instructions>
-</package>
-```
-
-
-## Elements
-
-### `<package>`
-
-The root node of every `package.xml` it contains the reference to the namespace and the location of the XML Schema Definition (XSD).
-
-The attribute `name` is the most important part, it holds the unique package identifier and is mandatory.
-It is based upon your domain name and the package name of your choice.
-
-For example WoltLab Suite Forum (formerly know an WoltLab Burning Board and usually abbreviated as `wbb`) is created by WoltLab which owns the domain `woltlab.com`.
-The resulting package identifier is `com.woltlab.wbb` (`<tld>.<domain>.<packageName>`).
-
-### `<packageinformation>`
-
-Holds the entire meta data of the package.
-
-#### `<packagename>`
-
-This is the actual package name displayed to the end user, this can be anything you want, try to keep it short.
-It supports the attribute `languagecode` which allows you to provide the package name in different languages, please be aware that if it is not present, `en` (English) is assumed:
-
-```xml
-<packageinformation>
- <packagename>Simple Package</packagename>
- <packagename languagecode="de">Einfaches Paket</packagename>
-</packageinformation>
-```
-
-#### `<packagedescription>`
-
-Brief summary of the package, use it to explain what it does since the package name might not always be clear enough.
-The attribute `languagecode` is available here too, please reference to [`<packagename>`](#packageName) for details.
-
-#### `<version>`
-
-The package's version number, this is a string consisting of three numbers separated with a dot and optionally followed by a keyword (must be followed with another number).
-
-The possible keywords are:
-
-- Alpha/dev (both is regarded to be the same)
-- Beta
-- RC (release candidate)
-- pl (patch level)
-
-Valid examples:
-
-- 1.0.0
-- 1.12.13 Alpha 19
-- 7.0.0 pl 3
-
-Invalid examples:
-
-- 1.0.0 Beta (keyword Beta must be followed by a number)
-- 2.0 RC 3 (version number must consists of 3 blocks of numbers)
-- 1.2.3 dev 4.5 (4.5 is not an integer, 4 or 5 would be valid but not the fraction)
-
-#### `<date>`
-
-Must be a valid [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) date, e.g. `2013-12-27`.
-
-### `<authorinformation>`
-
-Holds meta data regarding the package's author.
-
-#### `<author>`
-
-Can be anything you want.
-
-#### `<authorurl>`
-
-> (optional)
-
-URL to the author's website.
-
-### `<requiredpackages>`
-
-A list of packages including their version required for this package to work.
-
-#### `<requiredpackage>`
-
-Example:
-
-```xml
-<requiredpackage minversion="2.0.0" file="requirements/com.woltlab.wcf.tar">com.woltlab.wcf</requiredpackage>
-```
-
-The attribute `minversion` must be a valid version number as described in [`<version>`](#version).
-The `file` attribute is optional and specifies the location of the required package's archive relative to the `package.xml`.
-
-### `<optionalpackage>`
-
-A list of optional packages which can be selected by the user at the very end of the installation process.
-
-#### `<optionalpackage>`
-
-Example:
-
-```xml
-<optionalpackage file="optionals/com.woltlab.wcf.moderatedUserGroup.tar">com.woltlab.wcf.moderatedUserGroup</optionalpackage>
-```
-
-The `file` attribute specifies the location of the optional package's archive relative to the `package.xml`.
-
-### `<excludedpackages>`
-
-List of packages which conflict with this package. It is not possible to install it if any of the specified packages is installed. In return you cannot install an excluded package if this package is installed.
-
-#### `<excludedpackage>`
-
-Example:
-
-```xml
-<excludedpackage version="3.1.0 Alpha 1">com.woltlab.wcf</excludedpackage>
-```
-
-The attribute `version` must be a valid version number as described in the [\<version\>](#version) section. In the example above it will be impossible to install this package in WoltLab Suite Core 3.1.0 Alpha 1 or higher.
-
-### `<compatibility>`
-{% include callout.html content="Available since WoltLab Suite 3.1" type="info" %}
-{% include callout.html content="With the release of WoltLab Suite 5.2 the API versions were abolished. Instead of using API versions packages should exclude version `6.0.0 Alpha 1` of `com.woltlab.wcf` going forward." type="warning" %}
-
-WoltLab Suite 3.1 introduced a new versioning system that focused around the API compatibility and is intended to replace the `<excludedpackage>` instruction for the Core for most plugins.
-
-The `<compatibility>`-tag holds a list of compatible API versions, and while only a single version is available at the time of writing, future versions will add more versions with backwards-compatibility in mind.
-
-Example:
-
-```xml
-<compatibility>
- <api version="2018" />
-</compatibility>
-```
-
-#### Existing API versions
-
-| WoltLab Suite Core | API-Version | Backwards-Compatible to API-Version |
-|---|---|---|
-| 3.1 | 2018 | n/a |
-
-### `<instructions>`
-
-List of instructions to be executed upon install or update. The order is important, the topmost `<instruction>` will be executed first.
-
-#### `<instructions type="install">`
-
-List of instructions for a new installation of this package.
-
-#### `<instructions type="update" fromversion="…">`
-
-The attribute `fromversion` must be a valid version number as described in the [\<version\>](#version) section and specifies a possible update from that very version to the package's version.
-
-{% include callout.html content="The installation process will pick exactly one update instruction, ignoring everything else. Please read the explanation below!" type="warning" %}
-
-Example:
-
-- Installed version: `1.0.0`
-- Package version: `1.0.2`
-
-```xml
-<instructions type="update" fromversion="1.0.0">
- <!-- … -->
-</instructions>
-<instructions type="update" fromversion="1.0.1">
- <!-- … -->
-</instructions>
-```
-
-In this example WoltLab Suite Core will pick the first update block since it allows an update from `1.0.0 -> 1.0.2`.
-The other block is not considered, since the currently installed version is `1.0.0`. After applying the update block (`fromversion="1.0.0"`), the version now reads `1.0.2`.
-
-#### `<instruction>`
-
-Example:
-
-```xml
-<instruction type="objectTypeDefinition">objectTypeDefinition.xml</instruction>
-```
-
-The attribute `type` specifies the instruction type which is used to determine the package installation plugin (PIP) invoked to handle its value.
-The value must be a valid file relative to the location of `package.xml`.
-Many PIPs provide default file names which are used if no value is given:
-
-```xml
-<instruction type="objectTypeDefinition" />
-```
-
-There is a [list of all default PIPs](package_pip.html) available.
-
-{% include callout.html content="Both the `type`-attribute and the element value are case-sensitive. Windows does not care if the file is called `objecttypedefinition.xml` but was referenced as `objectTypeDefinition.xml`, but both Linux and Mac systems will be unable to find the file." type="warning" %}
-
-In addition to the `type` attribute, an optional `run` attribute (with `standalone` as the only valid value) is supported which forces the installation to execute this PIP in an isolated request, allowing a single, resource-heavy PIP to execute without encountering restrictions such as PHP’s `memory_limit` or `max_execution_time`:
-
-```xml
-<instruction type="file" run="standalone" />
-```
-
-#### `<void/>`
-
-Sometimes a package update should only adjust the metadata of the package, for example, an optional package was added.
-However, WoltLab Suite Core requires that the list of `<instructions>` is non-empty.
-Instead of using a dummy `<instruction>` that idempotently updates some PIP, the `<void/>` tag can be used for this use-case.
-
-Using the `<void/>` tag is only valid for `<instructions type="update">` and must not be accompanied by other `<instruction>` tags.
-
-Example:
-
-```xml
-<instructions type="update" fromversion="1.0.0">
- <void/>
-</instructions>
-```
+++ /dev/null
----
-title: Package Installation Plugins
-sidebar: sidebar
-permalink: package_pip.html
-folder: package
----
-
-Package Installation Plugins (PIPs) are interfaces to deploy and edit content as well as components.
-
-{% include callout.html content="For XML-based PIPs: `<![CDATA[]]>` must be used for language items and page contents. In all other cases it may only be used when necessary." type="info" %}
-
-## Built-In PIPs
-
-| Name | Description |
-|------|-------------|
-| [aclOption][package_pip_acl-option] | Customizable permissions for individual objects |
-| [acpMenu][package_pip_acp-menu] | Admin panel menu categories and items |
-| [acpSearchProvider][package_pip_acp-search-provider] | Data provider for the admin panel search |
-| [acpTemplate][package_pip_acp-template] | Admin panel templates |
-| [bbcode][package_pip_bbcode] | BBCodes for rich message formatting |
-| [box][package_pip_box] | Boxes that can be placed anywhere on a page |
-| [clipboardAction][package_pip_clipboard_action] | Perform bulk operations on marked objects |
-| [coreObject][package_pip_core-object] | Access Singletons from within the template |
-| [cronjob][package_pip_cronjob] | Periodically execute code with customizable intervals |
-| [eventListener][package_pip_event-listener] | Register listeners for the event system |
-| [file][package_pip_file] | Deploy any type of files with the exception of templates |
-| [language][package_pip_language] | Language items |
-| [mediaProvider][package_pip_media-provider] | Detect and convert links to media providers |
-| [menu][package_pip_menu] | Side-wide and custom per-page menus |
-| [menuItem][package_pip_menu-item] | Menu items for menus created through the menu PIP |
-| [objectType][package_pip_object-type] | Flexible type registry based on definitions |
-| [objectTypeDefinition][package_pip_object-type-definition] | Groups objects and classes by functionality |
-| [option][package_pip_option] | Side-wide configuration options |
-| [page][package_pip_page] | Register page controllers and text-based pages |
-| [pip][package_pip_pip] | Package Installation Plugins |
-| [script][package_pip_script] | Execute arbitrary PHP code during installation, update and uninstallation |
-| [smiley][package_pip_smiley] | Smileys |
-| [sql][package_pip_sql] | Execute SQL instructions using a MySQL-flavored syntax (also see [database PHP API](package_database-php-api.html)) |
-| [style][package_pip_style] | Style |
-| [template][package_pip_template] | Frontend templates |
-| [templateListener][package_pip_template-listener] | Embed template code into templates without altering the original |
-| [userGroupOption][package_pip_user-group-option] | Permissions for user groups |
-| [userMenu][package_pip_user-menu] | User menu categories and items |
-| [userNotificationEvent][package_pip_user-notification-event] | Events of the user notification system |
-| [userOption][package_pip_user-option] | User settings |
-| [userProfileMenu][package_pip_user-profile-menu] | User profile tabs |
-
-{% include links.html %}
+++ /dev/null
----
-title: ACL Option Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_acl-option.html
-folder: package/pip
-parent: package_pip
----
-
-Add customizable permissions for individual objects.
-
-## Option Components
-
-Each acl option is described as an `<option>` element with the mandatory attribute `name`.
-
-### `<categoryname>`
-
-<span class="label label-info">Optional</span>
-
-The name of the acl option category to which the option belongs.
-
-### `<objecttype>`
-
-The name of the acl object type (of the object type definition `com.woltlab.wcf.acl`).
-
-
-## Category Components
-
-Each acl option category is described as an `<category>` element with the mandatory attribute `name` that should follow the naming pattern `<permissionName>` or `<permissionType>.<permissionName>`, with `<permissionType>` generally having `user` or `mod` as value.
-
-### `<objecttype>`
-
-The name of the acl object type (of the object type definition `com.woltlab.wcf.acl`).
-
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/aclOption.xsd">
- <import>
- <categories>
- <category name="user.example">
- <objecttype>com.example.wcf.example</objecttype>
- </category>
- <category name="mod.example">
- <objecttype>com.example.wcf.example</objecttype>
- </category>
- </categories>
-
- <options>
- <option name="canAddExample">
- <categoryname>user.example</categoryname>
- <objecttype>com.example.wcf.example</objecttype>
- </option>
- <option name="canDeleteExample">
- <categoryname>mod.example</categoryname>
- <objecttype>com.example.wcf.example</objecttype>
- </option>
- </options>
- </import>
-
- <delete>
- <optioncategory name="old.example">
- <objecttype>com.example.wcf.example</objecttype>
- </optioncategory>
- <option name="canDoSomethingWithExample">
- <objecttype>com.example.wcf.example</objecttype>
- </option>
- </delete>
-</data>
-```
+++ /dev/null
----
-title: ACP Menu Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_acp-menu.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new ACP menu items.
-
-## Components
-
-Each item is described as an `<acpmenuitem>` element with the mandatory attribute `name`.
-
-### `<parent>`
-
-<span class="label label-info">Optional</span>
-
-The item’s parent item.
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Specifies the order of this item within the parent item.
-
-### `<controller>`
-
-The fully qualified class name of the target controller.
-If not specified this item serves as a category.
-
-### `<link>`
-
-Additional components if `<controller>` is set,
-the full external link otherwise.
-
-### `<icon>`
-
-{% include tip.html content="Use an icon only for top-level and 4th-level items." %}
-
-Name of the Font Awesome icon class.
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the tab to be shown.
-
-### `<permissions>`
-
-<span class="label label-info">Optional</span>
-
-The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the tab to be shown.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/acpMenu.xsd">
- <import>
- <acpmenuitem name="foo.acp.menu.link.example">
- <parent>wcf.acp.menu.link.application</parent>
- </acpmenuitem>
-
- <acpmenuitem name="foo.acp.menu.link.example.list">
- <controller>foo\acp\page\ExampleListPage</controller>
- <parent>foo.acp.menu.link.example</parent>
- <permissions>admin.foo.canManageExample</permissions>
- <showorder>1</showorder>
- </acpmenuitem>
-
- <acpmenuitem name="foo.acp.menu.link.example.add">
- <controller>foo\acp\form\ExampleAddForm</controller>
- <parent>foo.acp.menu.link.example.list</parent>
- <permissions>admin.foo.canManageExample</permissions>
- <icon>fa-plus</icon>
- </acpmenuitem>
- </import>
-</data>
-```
+++ /dev/null
----
-title: ACP Search Provider Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_acp-search-provider.html
-folder: package/pip
-parent: package_pip
----
-
-Registers data provider for the admin panel search.
-
-## Components
-
-Each acp search result provider is described as an `<acpsearchprovider>` element with the mandatory attribute `name`.
-
-### `<classname>`
-
-The name of the class providing the search results,
-the class has to implement the `wcf\system\search\acp\IACPSearchResultProvider` interface.
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Determines at which position of the search result list the provided results are shown.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/acpSearchProvider.xsd">
- <import>
- <acpsearchprovider name="com.woltlab.wcf.example">
- <classname>wcf\system\search\acp\ExampleACPSearchResultProvider</classname>
- <showorder>1</showorder>
- </acpsearchprovider>
- </import>
-</data>
-```
+++ /dev/null
----
-title: ACP Template Installation Plugin
-sidebar: sidebar
-permalink: package_pip_acp-template.html
-folder: package/pip
-parent: package_pip
----
-
-Add templates for acp pages and forms by providing an archive containing the template files.
-
-{% include callout.html content="You cannot overwrite acp templates provided by other packages." type="warning" %}
-
-
-## Archive
-
-The `acpTemplate` package installation plugins expects a `.tar` (recommended) or `.tar.gz` archive.
-The templates must all be in the root of the archive.
-Do not include any directories in the archive.
-The file path given in the `instruction` element as its value must be relative to the `package.xml` file.
-
-
-## Attributes
-
-### `application`
-
-The `application` attribute determines to which application the installed acp templates belong and thus in which directory the templates are installed.
-The value of the `application` attribute has to be the abbreviation of an installed application.
-If no `application` attribute is given, the following rules are applied:
-
-- If the package installing the acp templates is an application, then the templates will be installed in this application's directory.
-- If the package installing the acp templates is no application, then the templates will be installed in WoltLab Suite Core's directory.
-
-
-## Example in `package.xml`
-
-```xml
-<instruction type="acpTemplate" />
-<!-- is the same as -->
-<instruction type="acpTemplate">acptemplates.tar</instruction>
-
-<!-- if an application "com.woltlab.example" is being installed, the following lines are equivalent -->
-<instruction type="acpTemplate" />
-<instruction type="acpTemplate" application="example" />
-```
+++ /dev/null
----
-title: BBCode Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_bbcode.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new BBCodes.
-
-## Components
-
-Each bbcode is described as an `<bbcode>` element with the mandatory attribute `name`.
-The `name` attribute must contain alphanumeric characters only and is exposed to the user.
-
-### `<htmlopen>`
-
-{% include callout.html content="Optional: Must not be provided if the BBCode is being processed a PHP class (`<classname>`)." type="info" %}
-
-The contents of this tag are literally copied into the opening tag of the bbcode.
-
-### `<htmlclose>`
-
-{% include callout.html content="Optional: Must not be provided if `<htmlopen>` is not given." type="info" %}
-
-Must match the `<htmlopen>` tag.
-Do not provide for self-closing tags.
-
-### `<classname>`
-
-The name of the class providing the bbcode output,
-the class has to implement the `wcf\system\bbcode\IBBCode` interface.
-
-BBCodes can be statically converted to HTML during input processing using a
-`wcf\system\html\metacode\converter\*MetaConverter` class. This class does not
-need to be registered.
-
-### `<wysiwygicon>`
-
-<span class="label label-info">Optional</span>
-
-Name of the Font Awesome icon class or path to a `gif`, `jpg`, `jpeg`, `png`, or `svg` image (placed inside the `icon/` directory) to show in the editor toolbar.
-
-### `<buttonlabel>`
-
-{% include callout.html content="Optional: Must be provided if an icon is given." type="info" %}
-
-Explanatory text to show when hovering the icon.
-
-### `<sourcecode>`
-
-{% include warning.html content="Do not set this to `1` if you don't specify a PHP class for processing. You must perform XSS sanitizing yourself!" %}
-
-If set to `1` contents of this BBCode will not be interpreted,
-but literally passed through instead.
-
-### `<isBlockElement>`
-
-Set to `1` if the output of this BBCode is a HTML block element (according to the HTML specification).
-
-### `<attributes>`
-
-Each bbcode is described as an `<attribute>` element with the mandatory attribute `name`.
-The `name` attribute is a 0-indexed integer.
-
-#### `<html>`
-
-{% include callout.html content="Optional: Must not be provided if the BBCode is being processed a PHP class (`<classname>`)." type="info" %}
-
-The contents of this tag are copied into the opening tag of the bbcode.
-`%s` is replaced by the attribute value.
-
-#### `<validationpattern>`
-
-<span class="label label-info">Optional</span>
-
-Defines a regular expression that is used to validate the value of the attribute.
-
-#### `<required>`
-
-<span class="label label-info">Optional</span>
-
-Specifies whether this attribute must be provided.
-
-#### `<usetext>`
-
-<span class="label label-info">Optional</span>
-{% include callout.html content="Should only be set to `1` for the attribute with name `0`." type="info" %}
-
-Specifies whether the text content of the BBCode should become this attribute's value.
-
-## Example
-
-```
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/bbcode.xsd">
- <import>
- <bbcode name="foo">
- <classname>wcf\system\bbcode\FooBBCode</classname>
- <attributes>
- <attribute name="0">
- <validationpattern>^\d+$</validationpattern>
- <required>1</required>
- </attribute>
- </attributes>
- </bbcode>
-
- <bbcode name="example">
- <htmlopen>div</htmlopen>
- <htmlclose>div</htmlclose>
- <isBlockElement>1</isBlockElement>
- <wysiwygicon>fa-bath</wysiwygicon>
- <buttonlabel>wcf.editor.button.example</buttonlabel>
- </bbcode>
- </import>
-</data>
-```
+++ /dev/null
----
-title: Box Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_box.html
-folder: package/pip
-parent: package_pip
----
-
-Deploy and manage boxes that can be placed anywhere on the site, they come in two flavors: system and content-based.
-
-## Components
-
-Each item is described as a `<box>` element with the mandatory attribute `name` that should follow the naming pattern `<packageIdentifier>.<BoxName>`, e.g. `com.woltlab.wcf.RecentActivity`.
-
-### `<name>`
-
-{% include languageCode.html %}
-
-The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple `<name>` elements.
-
-### `<boxType>`
-
-#### `system`
-
-The special `system` type is reserved for boxes that pull their properties and content from a registered PHP class. Requires the `<objectType>` element.
-
-#### `html`, `text` or `tpl`
-
-Provide arbitrary content, requires the `<content>` element.
-
-### `<objectType>`
-
-Required for boxes with `boxType = system`, must be registered through [the objectType PIP](package_pip_object-type.html) for the definition `com.woltlab.wcf.boxController`.
-
-### `<position>`
-
-The default display position of this box, can be any of the following:
-
-* bottom
-* contentBottom
-* contentTop
-* footer
-* footerBoxes
-* headerBoxes
-* hero
-* sidebarLeft
-* sidebarRight
-* top
-
-#### Placeholder Positions
-
-{% include image.html file="boxPlaceholders.png" alt="Visual illustration of placeholder positions" %}
-
-### `<showHeader>`
-
-Setting this to `0` will suppress display of the box title, useful for boxes containing advertisements or similar. Defaults to `1`.
-
-### `<visibleEverywhere>`
-
-Controls the display on all pages (`1`) or none (`0`), can be used in conjunction with `<visibilityExceptions>`.
-
-### `<visibilityExceptions>`
-
-Inverts the `<visibleEverywhere>` setting for the listed pages only.
-
-### `<cssClassName>`
-
-Provide a custom CSS class name that is added to the menu container, allowing further customization of the menu's appearance.
-
-### `<content>`
-
-{% include languageCode.html %}
-#### `<title>`
-
-The title element is required and controls the box title shown to the end users.
-
-#### `<content>`
-
-The content that should be used to populate the box, only used and required if the `boxType` equals `text`, `html` and `tpl`.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/box.xsd">
- <import>
- <box identifier="com.woltlab.wcf.RecentActivity">
- <name language="de">Letzte Aktivitäten</name>
- <name language="en">Recent Activities</name>
- <boxType>system</boxType>
- <objectType>com.woltlab.wcf.recentActivityList</objectType>
- <position>contentBottom</position>
- <showHeader>0</showHeader>
- <visibleEverywhere>0</visibleEverywhere>
- <visibilityExceptions>
- <page>com.woltlab.wcf.Dashboard</page>
- </visibilityExceptions>
- <limit>10</limit>
-
- <content language="de">
- <title>Letzte Aktivitäten</title>
- </content>
- <content language="en">
- <title>Recent Activities</title>
- </content>
- </box>
- </import>
-
- <delete>
- <box identifier="com.woltlab.wcf.RecentActivity" />
- </delete>
-</data>
-```
+++ /dev/null
----
-title: Clipboard Action Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_clipboard_action.html
-folder: package/pip
-parent: package_pip
----
-
-Registers clipboard actions.
-
-## Components
-
-Each clipboard action is described as an `<action>` element with the mandatory attribute `name`.
-
-### `<actionclassname>`
-
-The name of the class used by the clipboard API to process the concrete action.
-The class has to implement the `wcf\system\clipboard\action\IClipboardAction` interface, best by extending `wcf\system\clipboard\action\AbstractClipboardAction`.
-
-### `<pages>`
-
-Element with `<page>` children whose value contains the class name of the controller of the page on which the clipboard action is available.
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Determines at which position of the clipboard action list the action is shown.
-
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/clipboardAction.xsd">
- <import>
- <action name="delete">
- <actionclassname>wcf\system\clipboard\action\ExampleClipboardAction</actionclassname>
- <showorder>1</showorder>
- <pages>
- <page>wcf\acp\page\ExampleListPage</page>
- </pages>
- </action>
- <action name="foo">
- <actionclassname>wcf\system\clipboard\action\ExampleClipboardAction</actionclassname>
- <showorder>2</showorder>
- <pages>
- <page>wcf\acp\page\ExampleListPage</page>
- </pages>
- </action>
- <action name="bar">
- <actionclassname>wcf\system\clipboard\action\ExampleClipboardAction</actionclassname>
- <showorder>3</showorder>
- <pages>
- <page>wcf\acp\page\ExampleListPage</page>
- </pages>
- </action>
- </import>
-</data>
-```
+++ /dev/null
----
-title: Core Object Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_core-object.html
-folder: package/pip
-parent: package_pip
----
-
-Registers `wcf\system\SingletonFactory` objects to be accessible in templates.
-
-## Components
-
-Each item is described as a `<coreobject>` element with the mandatory element `objectname`.
-
-### `<objectname>`
-
-The fully qualified class name of the class.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/coreObject.xsd">
- <import>
- <coreobject>
- <objectname>wcf\system\example\ExampleHandler</objectname>
- </coreobject>
- </import>
-</data>
-```
-
-This object can be accessed in templates via `$__wcf->getExampleHandler()` (in general: the method name begins with `get` and ends with the unqualified class name).
+++ /dev/null
----
-title: Cronjob Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_cronjob.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new cronjobs.
-The cronjob schedular works similar to the `cron(8)` daemon, which might not available to web applications on regular webspaces.
-The main difference is that WoltLab Suite’s cronjobs do not guarantee execution at the specified points in time:
-WoltLab Suite’s cronjobs are triggered by regular visitors in an AJAX request, once the next execution point lies in the past.
-
-## Components
-
-Each cronjob is described as an `<cronjob>` element with the mandatory attribute `name`.
-
-### `<classname>`
-
-The name of the class providing the cronjob's behaviour,
-the class has to implement the `wcf\system\cronjob\ICronjob` interface.
-
-### `<description>`
-
-{% include languageCode.html requirement="optional" %}
-
-Provides a human readable description for the administrator.
-
-### `<start*>`
-
-All of the five `startMinute`, `startHour`, `startDom` (Day Of Month), `startMonth`, `startDow` (Day Of Week) are required.
-They correspond to the fields in `crontab(5)` of a cron daemon and accept the same syntax.
-
-### `<canBeEdited>`
-
-Controls whether the administrator may edit the fields of the cronjob.
-
-### `<canBeDisabled>`
-
-Controls whether the administrator may disable the cronjob.
-
-### `<options>`
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the template listener to be executed.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/cronjob.xsd">
- <import>
- <cronjob name="com.example.package.example">
- <classname>wcf\system\cronjob\ExampleCronjob</classname>
- <description>Serves as an example</description>
- <description language="de">Stellt ein Beispiel dar</description>
- <startminute>0</startminute>
- <starthour>2</starthour>
- <startdom>*/2</startdom>
- <startmonth>*</startmonth>
- <startdow>*</startdow>
- <canbeedited>1</canbeedited>
- <canbedisabled>1</canbedisabled>
- </cronjob>
- </import>
-</data>
-```
-
+++ /dev/null
----
-title: Event Listener Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_event-listener.html
-folder: package/pip
-parent: package_pip
----
-
-Registers event listeners.
-An explanation of events and event listeners can be found [here](php_api_events.html).
-
-## Components
-
-Each event listener is described as an `<eventlistener>` element with a `name` attribute.
-As the `name` attribute has only be introduced with WSC 3.0, it is not yet mandatory to allow backwards compatibility.
-If `name` is not given, the system automatically sets the name based on the id of the event listener in the database.
-
-### `<eventclassname>`
-
-The event class name is the name of the class in which the event is fired.
-
-### `<eventname>`
-
-The event name is the name given when the event is fired to identify different events within the same class.
-You can either give a single event name or a comma-separated list of event names in which case the event listener listens to all of the listed events.
-
-### `<listenerclassname>`
-
-The listener class name is the name of the class which is triggered if the relevant event is fired.
-The PHP class has to implement the `wcf\system\event\listener\IParameterizedEventListener` interface.
-
-{% include callout.html content="Legacy event listeners are only required to implement the deprecated `wcf\system\event\IEventListener` interface. When writing new code or update existing code, you should always implement the `wcf\system\event\listener\IParameterizedEventListener` interface!" type="warning" %}
-
-### `<inherit>`
-
-The inherit value can either be `0` (default value if the element is omitted) or `1` and determines if the event listener is also triggered for child classes of the given event class name.
-This is the case if `1` is used as the value.
-
-### `<environment>`
-
-The value of the environment element must be one of `user`, `admin` or `all` and defaults to `user` if no value is given.
-The value determines if the event listener will be executed in the frontend (`user`), the backend (`admin`) or both (`all`).
-
-### `<nice>`
-
-The nice value element can contain an integer value out of the interval `[-128,127]` with `0` being the default value if the element is omitted.
-The nice value determines the execution order of event listeners.
-Event listeners with smaller nice values are executed first.
-If the nice value of two event listeners is equal, they are sorted by the listener class name.
-
-{% include callout.html content="If you pass a value out of the mentioned interval, the value will be adjusted to the closest value in the interval." type="info" %}
-
-### `<options>`
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the event listener to be executed.
-
-### `<permissions>`
-
-The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the event listener to be executed.
-
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/eventListener.xsd">
- <import>
- <eventlistener name="inheritedAdminExample">
- <eventclassname>wcf\acp\form\UserAddForm</eventclassname>
- <eventname>assignVariables,readFormParameters,save,validate</eventname>
- <listenerclassname>wcf\system\event\listener\InheritedAdminExampleListener</listenerclassname>
- <inherit>1</inherit>
- <environment>admin</environment>
- </eventlistener>
-
- <eventlistener name="nonInheritedUserExample">
- <eventclassname>wcf\form\SettingsForm</eventclassname>
- <eventname>assignVariables</eventname>
- <listenerclassname>wcf\system\event\listener\NonInheritedUserExampleListener</listenerclassname>
- </eventlistener>
- </import>
-
- <delete>
- <eventlistener name="oldEventListenerName" />
- </delete>
-</data>
-
-```
+++ /dev/null
----
-title: File Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_file.html
-folder: package/pip
-parent: package_pip
----
-
-Adds any type of files with the exception of templates.
-
-{% include callout.html content="You cannot overwrite files provided by other packages." type="warning" %}
-
-The `application` attribute behaves like it does for [acp templates](package_pip_acp-template.html#application).
-
-
-## Archive
-
-The `acpTemplate` package installation plugins expects a `.tar` (recommended) or `.tar.gz` archive.
-The file path given in the `instruction` element as its value must be relative to the `package.xml` file.
-
-
-## Example in `package.xml`
-
-```xml
-<instruction type="file" />
-<!-- is the same as -->
-<instruction type="file">files.tar</instruction>
-
-<!-- if an application "com.woltlab.example" is being installed, the following lines are equivalent -->
-<instruction type="file" />
-<instruction type="file" application="example" />
-
-<!-- if the same application wants to install additional files, in WoltLab Suite Core's directory: -->
-<instruction type="file" application="wcf">files_wcf.tar</instruction>
-```
+++ /dev/null
----
-title: Language Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_language.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new language items.
-
-## Components
-
-{% include languageCode.html attribute="languagecode" %}
-
-The top level `<language>` node must contain a `languagecode` attribute.
-
-### `<category>`
-
-Each category must contain a `name` attribute containing two or three components consisting of alphanumeric character only, separated by a single full stop (`.`, U+002E).
-
-#### `<item>`
-
-Each language item must contain a `name` attribute containing at least three components consisting of alphanumeric character only, separated by a single full stop (`.`, U+002E). The `name` of the parent `<category>` node followed by a full stop must be a prefix of the `<item>`’s `name`.
-
-{% include tip.html content="Wrap the text content inside a CDATA to avoid escaping of special characters." %}
-{% include warning.html content="Do not use the `{lang}` tag inside a language item." %}
-
-The text content of the `<item>` node is the value of the language item. Language items that are not in the `wcf.global` category support template scripting.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/language.xsd" languagecode="de">
- <category name="wcf.example">
- <item name="wcf.example.foo"><![CDATA[<strong>Look!</strong>]]></item>
- </category>
-</language>
-```
+++ /dev/null
----
-title: Media Provider Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_media-provider.html
-folder: package/pip
-parent: package_pip
----
-
-{% include callout.html content="Available since WoltLab Suite 3.1" type="info" %}
-
-Media providers are responsible to detect and convert links to a 3rd party service inside messages.
-
-## Components
-
-Each item is described as a `<provider>` element with the mandatory attribute `name` that should equal the lower-cased provider name. If a provider provides multiple components that are (largely) unrelated to each other, it is recommended to use a dash to separate the name and the component, e. g. `youtube-playlist`.
-
-### `<title>`
-
-The title is displayed in the administration control panel and is only used there, the value is neither localizable nor is it ever exposed to regular users.
-
-### `<regex>`
-
-The regular expression used to identify links to this provider, it must not contain anchors or delimiters. It is strongly recommended to capture the primary object id using the `(?P<ID>...)` group.
-
-### `<className>`
-
-{% include callout.html content="`<className>` and `<html>` are mutually exclusive." type="warning" %}
-
-PHP-Callback-Class that is invoked to process the matched link in case that additional logic must be applied that cannot be handled through a simple replacement as defined by the `<html>` element.
-
-The callback-class must implement the interface `\wcf\system\bbcode\media\provider\IBBCodeMediaProvider`.
-
-### `<html>`
-
-{% include callout.html content="`<className>` and `<html>` are mutually exclusive." type="warning" %}
-
-Replacement HTML that gets populated using the captured matches in `<regex>`, variables are accessed as `{$VariableName}`. For example, the capture group `(?P<ID>...)` is accessed using `{$ID}`.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/mediaProvider.xsd">
- <import>
- <provider name="youtube">
- <title>YouTube</title>
- <regex><![CDATA[https?://(?:.+?\.)?youtu(?:\.be/|be\.com/(?:#/)?watch\?(?:.*?&)?v=)(?P<ID>[a-zA-Z0-9_-]+)(?:(?:\?|&)t=(?P<start>[0-9hms]+)$)?]]></regex>
- <!-- advanced PHP callback -->
- <className><![CDATA[wcf\system\bbcode\media\provider\YouTubeBBCodeMediaProvider]]></className>
- </provider>
-
- <provider name="youtube-playlist">
- <title>YouTube Playlist</title>
- <regex><![CDATA[https?://(?:.+?\.)?youtu(?:\.be/|be\.com/)playlist\?(?:.*?&)?list=(?P<ID>[a-zA-Z0-9_-]+)]]></regex>
- <!-- uses a simple HTML replacement -->
- <html><![CDATA[<div class="videoContainer"><iframe src="https://www.youtube.com/embed/videoseries?list={$ID}" allowfullscreen></iframe></div>]]></html>
- </provider>
- </import>
-
- <delete>
- <provider identifier="example" />
- </delete>
-</data>
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Menu Item Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_menu-item.html
-folder: package/pip
-parent: package_pip
----
-
-Adds menu items to existing menus.
-
-## Components
-
-Each item is described as an `<item>` element with the mandatory attribute `identifier` that should follow the naming pattern `<packageIdentifier>.<PageName>`, e.g. `com.woltlab.wcf.Dashboard`.
-
-### `<menu>`
-
-The target menu that the item should be added to, requires the internal identifier set by creating a menu through the [menu.xml][package_pip_menu].
-
-### `<title>`
-
-{% include languageCode.html %}
-
-The title is displayed as the link title of the menu item and can be fully customized by the administrator, thus is immutable after deployment. Supports multiple `<title>` elements to provide localized values.
-
-### `<page>`
-
-The page that the link should point to, requires the internal identifier set by creating a page through the [page.xml][package_pip_page].
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/menuItem.xsd">
- <import>
- <item identifier="com.woltlab.wcf.Dashboard">
- <menu>com.woltlab.wcf.MainMenu</menu>
- <title language="de">Dashboard</title>
- <title language="en">Dashboard</title>
- <page>com.woltlab.wcf.Dashboard</page>
- </item>
- </import>
-
- <delete>
- <item identifier="com.woltlab.wcf.FooterLinks" />
- </delete>
-</data>
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Menu Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_menu.html
-folder: package/pip
-parent: package_pip
----
-
-Deploy and manage menus that can be placed anywhere on the site.
-
-## Components
-
-Each item is described as a `<menu>` element with the mandatory attribute `identifier` that should follow the naming pattern `<packageIdentifier>.<MenuName>`, e.g. `com.woltlab.wcf.MainMenu`.
-
-### `<title>`
-
-{% include languageCode.html %}
-
-The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple `<title>` elements.
-
-### `<box>`
-
-The following elements of the [box PIP](package_pip_box.html) are supported, please refer to the documentation to learn more about them:
-
-* `<position>`
-* `<showHeader>`
-* `<visibleEverywhere>`
-* `<visibilityExceptions>`
-* `cssClassName`
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/menu.xsd">
- <import>
- <menu identifier="com.woltlab.wcf.FooterLinks">
- <title language="de">Footer-Links</title>
- <title language="en">Footer Links</title>
-
- <box>
- <position>footer</position>
- <cssClassName>boxMenuLinkGroup</cssClassName>
- <showHeader>0</showHeader>
- <visibleEverywhere>1</visibleEverywhere>
- </box>
- </menu>
- </import>
-
- <delete>
- <menu identifier="com.woltlab.wcf.FooterLinks" />
- </delete>
-</data>
-```
+++ /dev/null
----
-title: Object Type Definition Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_object-type-definition.html
-folder: package/pip
-parent: package_pip
----
-
-Registers an object type definition.
-An object type definition is a blueprint for a certain behaviour that is particularized by [objectTypes](package_pip_object-type.html).
-As an example: Tags can be attached to different types of content (such as forum posts or gallery images).
-The bulk of the work is implemented in a generalized fashion, with all the tags stored in a single database table.
-Certain things, such as permission checking, need to be particularized for the specific type of content, though.
-Thus tags (or rather “taggable content”) are registered as an object type definition.
-Posts are then registered as an object type, implementing the “taggable content” behaviour.
-
-Other types of object type definitions include attachments, likes, polls, subscriptions, or even the category system.
-
-## Components
-
-Each item is described as a `<definition>` element with the mandatory child `<name>` that should follow the naming pattern `<packageIdentifier>.<definition>`, e.g. `com.woltlab.wcf.example`.
-
-### `<interfacename>`
-
-<span class="label label-info">Optional</span>
-
-The name of the PHP interface [objectTypes](package_pip_object-type.html) have to implement.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/objectTypeDefinition.xsd">
- <import>
- <definition>
- <name>com.woltlab.wcf.example</name>
- <interfacename>wcf\system\example\IExampleObjectType</interfacename>
- </definition>
- </import>
-</data>
-```
+++ /dev/null
----
-title: Object Type Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_object-type.html
-folder: package/pip
-parent: package_pip
----
-
-Registers an object type.
-Read about object types in the [objectTypeDefinition](package_pip_object-type-definition.html) PIP.
-
-## Components
-
-Each item is described as a `<type>` element with the mandatory child `<name>` that should follow the naming pattern `<packageIdentifier>.<definition>`, e.g. `com.woltlab.wcf.example`.
-
-### `<definitionname>`
-
-The `<name>` of the [objectTypeDefinition](package_pip_object-type-definition.html).
-
-### `<classname>`
-
-The name of the class providing the object types's behaviour,
-the class has to implement the `<interfacename>` interface of the object type definition.
-
-### `<*>`
-
-<span class="label label-info">Optional</span>
-
-Additional fields may be defined for specific definitions of object types.
-Refer to the documentation of these for further explanation.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/objectType.xsd">
- <import>
- <type>
- <name>com.woltlab.wcf.example</name>
- <definitionname>com.woltlab.wcf.rebuildData</definitionname>
- <classname>wcf\system\worker\ExampleRebuildWorker</classname>
- <nicevalue>130</nicevalue>
- </type>
- </import>
-</data>
-```
+++ /dev/null
----
-title: Option Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_option.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new options.
-Options allow the administrator to configure the behaviour of installed packages.
-The specified values are exposed as PHP constants.
-
-## Category Components
-
-Each category is described as an `<category>` element with the mandatory attribute `name`.
-
-### `<parent>`
-
-<span class="label label-info">Optional</span>
-
-The category’s parent category.
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Specifies the order of this option within the parent category.
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the category to be shown to the administrator.
-
-## Option Components
-
-Each option is described as an `<option>` element with the mandatory attribute `name`.
-The `name` is transformed into a PHP constant name by uppercasing it.
-
-### `<categoryname>`
-
-The option’s category.
-
-### `<optiontype>`
-
-The type of input to be used for this option.
-Valid types are defined by the `wcf\system\option\*OptionType` classes.
-
-### `<defaultvalue>`
-
-The value that is set after installation of a package.
-Valid values are defined by the `optiontype`.
-
-### `<validationpattern>`
-
-<span class="label label-info">Optional</span>
-
-Defines a regular expression that is used to validate the value of a free form option (such as `text`).
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Specifies the order of this option within the category.
-
-### `<selectoptions>`
-
-<span class="label label-info">Optional</span>
-{% include callout.html content="Defined only for `select`, `multiSelect` and `radioButton` types." type="warning" %}
-
-Specifies a newline-separated list of selectable values.
-Each line consists of an internal handle, followed by a colon (`:`, U+003A), followed by a language item.
-The language item is shown to the administrator, the internal handle is what is saved and exposed to the code.
-
-### `<enableoptions>`
-
-<span class="label label-info">Optional</span>
-{% include callout.html content="Defined only for `boolean`, `select` and `radioButton` types." type="warning" %}
-
-Specifies a comma-separated list of options which should be visually enabled when this option is enabled.
-A leading exclamation mark (`!`, U+0021) will disable the specified option when this option is enabled.
-For `select` and `radioButton` types the list should be prefixed by the internal [`selectoptions`](#selectoptions) handle followed by a colon (`:`, U+003A).
-
-This setting is a visual helper for the administrator only.
-It does not have an effect on the server side processing of the option.
-
-### `<hidden>`
-
-<span class="label label-info">Optional</span>
-
-If `hidden` is set to `1` the option will not be shown to the administrator.
-It still can be modified programmatically.
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the option to be shown to the administrator.
-
-### `<supporti18n>`
-
-<span class="label label-info">Optional</span>
-
-Specifies whether this option supports localized input.
-
-### `<requirei18n>`
-
-<span class="label label-info">Optional</span>
-
-Specifies whether this option requires localized input (i.e. the administrator must specify a value for every installed language).
-
-### `<*>`
-
-<span class="label label-info">Optional</span>
-
-Additional fields may be defined by specific types of options.
-Refer to the documentation of these for further explanation.
-
-## Language Items
-
-All relevant language items have to be put into the `wcf.acp.option` language item category.
-
-### Categories
-
-If you install a category named `example.sub`, you have to provide the language item `wcf.acp.option.category.example.sub`, which is used when displaying the options.
-If you want to provide an optional description of the category, you have to provide the language item `wcf.acp.option.category.example.sub.description`.
-Descriptions are only relevant for categories whose parent has a parent itself, i.e. categories on the third level.
-
-### Options
-
-If you install an option named `module_example`, you have to provide the language item `wcf.acp.option.module_example`, which is used as a label for setting the option value.
-If you want to provide an optional description of the option, you have to provide the language item `wcf.acp.option.module_example.description`.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/option.xsd">
- <import>
- <categories>
- <category name="example" />
- <category name="example.sub">
- <parent>example</parent>
- <options>module_example</options>
- </category>
- </categories>
-
- <options>
- <option name="module_example">
- <categoryname>module.community</categoryname>
- <optiontype>boolean</optiontype>
- <defaultvalue>1</defaultvalue>
- </option>
-
- <option name="example_integer">
- <categoryname>example.sub</categoryname>
- <optiontype>integer</optiontype>
- <defaultvalue>10</defaultvalue>
- <minvalue>5</minvalue>
- <maxvalue>40</maxvalue>
- </option>
-
- <option name="example_select">
- <categoryname>example.sub</categoryname>
- <optiontype>select</optiontype>
- <defaultvalue>DESC</defaultvalue>
- <selectoptions>ASC:wcf.global.sortOrder.ascending
-DESC:wcf.global.sortOrder.descending</selectoptions>
- </option>
- </options>
- </import>
-
- <delete>
- <option name="outdated_example" />
- </delete>
-</data>
-```
+++ /dev/null
----
-title: Page Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_page.html
-folder: package/pip
-parent: package_pip
----
-
-Registers page controllers, making them available for selection and configuration, including but not limited to boxes and menus.
-
-## Components
-
-Each item is described as a `<page>` element with the mandatory attribute `identifier` that should follow the naming pattern `<packageIdentifier>.<PageName>`, e.g. `com.woltlab.wcf.MembersList`.
-
-### `<pageType>`
-
-#### `system`
-
-The special `system` type is reserved for pages that pull their properties and content from a registered PHP class. Requires the `<controller>` element.
-
-#### `html`, `text` or `tpl`
-
-Provide arbitrary content, requires the `<content>` element.
-
-### `<controller>`
-
-Fully qualified class name for the controller, must implement `wcf\page\IPage` or `wcf\form\IForm`.
-
-### `<handler>`
-
-Fully qualified class name that can be optionally set to provide additional methods, such as displaying a badge for unread content and verifying permissions per page object id.
-
-### `<name>`
-
-{% include languageCode.html %}
-
-The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple `<name>` elements.
-
-### `<parent>`
-
-Sets the default parent page using its internal identifier, this setting controls the breadcrumbs and active menu item hierarchy.
-
-### `<hasFixedParent>`
-
-Pages can be assigned any other page as parent page by default, set to `1` to make the parent setting immutable.
-
-### `<permissions>`
-
-{% include callout.html content="The comma represents a logical `or`, the check is successful if at least one permission is set." type="warning" %}
-
-Comma separated list of permission names that will be checked one after another until at least one permission is set.
-
-### `<options>`
-
-{% include callout.html content="The comma represents a logical `or`, the check is successful if at least one option is enabled." type="warning" %}
-
-Comma separated list of options that will be checked one after another until at least one option is set.
-
-### `<excludeFromLandingPage>`
-
-Some pages should not be used as landing page, because they may not always be
-available and/or accessible to the user. For example, the account management
-page is available to logged-in users only and any guest attempting to visit that
-page would be presented with a permission denied message.
-
-Set this to `1` to prevent this page from becoming a landing page ever.
-
-### `<content>`
-
-{% include languageCode.html %}
-
-#### `<title>`
-
-The title element is required and controls the page title shown to the end users.
-
-#### `<content>`
-
-The content that should be used to populate the page, only used and required if the `pageType` equals `text`, `html` and `tpl`.
-
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/page.xsd">
- <import>
- <page identifier="com.woltlab.wcf.MembersList">
- <pageType>system</pageType>
- <controller>wcf\page\MembersListPage</controller>
- <name language="de">Mitglieder</name>
- <name language="en">Members</name>
- <permissions>user.profile.canViewMembersList</permissions>
- <options>module_members_list</options>
-
- <content language="en">
- <title>Members</title>
- </content>
- <content language="de">
- <title>Mitglieder</title>
- </content>
- </page>
- </import>
-
- <delete>
- <page identifier="com.woltlab.wcf.MembersList" />
- </delete>
-</data>
-```
+++ /dev/null
----
-title: Package Installation Plugin Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_pip.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new package installation plugins.
-
-## Components
-
-Each package installation plugin is described as an `<pip>` element with a `name` attribute and a PHP classname as the text content.
-
-{% include callout.html content="The package installation plugin’s class file must be installed into the `wcf` application and must not include classes outside the `\wcf\*` hierarchy to allow for proper uninstallation!" type="warning" %}
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/packageInstallationPlugin.xsd">
- <import>
- <pip name="custom">wcf\system\package\plugin\CustomPackageInstallationPlugin</pip>
- </import>
- <delete>
- <pip name="outdated" />
- </delete>
-</data>
-```
+++ /dev/null
----
-title: Script Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_script.html
-folder: package/pip
-parent: package_pip
----
-
-Execute arbitrary PHP code during installation, update and uninstallation of the package.
-
-{% include callout.html content="You must install the PHP script through the [file package installation plugin](package_pip_file.html)." type="warning" %}
-
-{% include callout.html content="The installation will attempt to delete the script after successful execution." type="warning" %}
-
-## Attributes
-
-### `application`
-
-The `application` attribute must have the same value as the `application` attribute of the `file` package installation plugin instruction so that the correct file in the intended application directory is executed.
-For further information about the `application` attribute, refer to its documentation on the [acpTemplate package installation plugin page](package_pip_acp-template.html#application).
-
-
-## Expected value
-
-The `script`-PIP expects a relative path to a `.php` file.
-
-### Naming convention
-
-The PHP script is deployed by using the [file package installation plugin](package_pip_file.html).
-To prevent it from colliding with other install script (remember: You cannot overwrite files created by another plugin), we highly recommend to make use of these naming conventions:
-
-- Installation: `install_<package>_<version>.php` (example: `install_com.woltlab.wbb_5.0.0.php`)
-- Update: `update_<package>_<targetVersion>.php` (example: `update_com.woltlab.wbb_5.0.0_pl_1.php`)
-
-`<targetVersion>` equals the version number of the current package being installed.
-If you're updating from `1.0.0` to `1.0.1`, `<targetVersion>` should read `1.0.1`.
-
-
-## Execution environment
-
-The script is included using `include()` within [ScriptPackageInstallationPlugin::run()](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/ScriptPackageInstallationPlugin.class.php#L69).
-This grants you access to the class members, including `$this->installation`.
-
-You can retrieve the package id of the current package through `$this->installation->getPackageID()`.
+++ /dev/null
----
-title: Smiley Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_smiley.html
-folder: package/pip
-parent: package_pip
----
-
-Installs new smileys.
-
-## Components
-
-Each smiley is described as an `<smiley>` element with the mandatory attribute `name`.
-
-### `<title>`
-
-Short human readable description of the smiley.
-
-### `<path(2x)?>`
-
-{% include important.html content="The files must be installed using the [file](package_pip_file.html) PIP." %}
-
-File path relative to the root of WoltLab Suite Core.
-`path2x` is optional and being used for High-DPI screens.
-
-### `<aliases>`
-
-<span class="label label-info">Optional</span>
-
-List of smiley aliases.
-Aliases must be separated by a line feed character (`\n`, U+000A).
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Determines at which position of the smiley list the smiley is shown.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/smiley.xsd">
- <import>
- <smiley name=":example:">
- <title>example</title>
- <path>images/smilies/example.png</path>
- <path2x>images/smilies/example@2x.png</path2x>
- <aliases><![CDATA[:alias:
-:more_aliases:]]></aliases>
- </smiley>
- </import>
-</data>
-```
+++ /dev/null
----
-title: SQL Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_sql.html
-folder: package/pip
-parent: package_pip
----
-
-Execute SQL instructions using a MySQL-flavored syntax.
-
-{% include callout.html content="This file is parsed by WoltLab Suite Core to allow reverting of certain changes, but not every syntax MySQL supports is recognized by the parser. To avoid any troubles, you should always use statements relying on the SQL standard." type="warning" %}
-
-
-## Expected Value
-
-The `sql` package installation plugin expects a relative path to a `.sql` file.
-
-
-## Features
-
-### Logging
-
-WoltLab Suite Core uses a SQL parser to extract queries and log certain actions.
-This allows WoltLab Suite Core to revert some of the changes you apply upon package uninstallation.
-
-The logged changes are:
-
-- `CREATE TABLE`
-- `ALTER TABLE … ADD COLUMN`
-- `ALTER TABLE … ADD … KEY`
-
-### Instance Number
-
-It is possible to use different instance numbers, e.g. two separate WoltLab Suite Core installations within one database.
-WoltLab Suite Core requires you to always use `wcf1_<tableName>` or `<app>1_<tableName>` (e.g. `blog1_blog` in WoltLab Suite Blog), the number (`1`) will be automatically replaced prior to execution.
-If you every use anything other but `1`, you will eventually break things, thus always use `1`!
-
-### Table Type
-
-WoltLab Suite Core will determine the type of database tables on its own:
-If the table contains a `FULLTEXT` index, it uses `MyISAM`, otherwise `InnoDB` is used.
-
-
-## Limitations
-
-### Logging
-
-WoltLab Suite Core cannot revert changes to the database structure which would cause to the data to be either changed or new data to be incompatible with the original format.
-Additionally, WoltLab Suite Core does not track regular SQL queries such as `DELETE` or `UPDATE`.
-
-### Triggers
-
-WoltLab Suite Core does not support trigger since MySQL does not support execution of triggers if the event was fired by a cascading foreign key action.
-If you really need triggers, you should consider adding them by custom SQL queries using a [script](package_pip_script.html).
-
-
-## Example
-
-`package.xml`:
-
-```xml
-<instruction type="sql">install.sql</instruction>
-```
-
-Example content:
-
-```sql
-CREATE TABLE wcf1_foo_bar (
- fooID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
- packageID INT(10) NOT NULL,
- bar VARCHAR(255) NOT NULL DEFAULT '',
- foobar VARCHAR(50) NOT NULL DEFAULT '',
-
- UNIQUE KEY baz (bar, foobar)
-);
-
-ALTER TABLE wcf1_foo_bar ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
-```
+++ /dev/null
----
-title: Style Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_style.html
-folder: package/pip
-parent: package_pip
----
-
-Install styles during package installation.
-
-The `style` package installation plugins expects a relative path to a `.tar` file, a`.tar.gz` file or a `.tgz` file.
-Please use the ACP's export mechanism to export styles.
-
-## Example in `package.xml`
-
-```xml
-<instruction type="style">style.tgz</instruction>
-```
+++ /dev/null
----
-title: Template Listener Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_template-listener.html
-folder: package/pip
-parent: package_pip
----
-
-Registers template listeners.
-Template listeners supplement [event listeners](package_pip_event-listener.html), which modify server side behaviour, by adding additional template code to display additional elements.
-The added template code behaves as if it was part of the original template (i.e. it has access to all local variables).
-
-## Components
-
-Each event listener is described as an `<templatelistener>` element with a `name` attribute.
-As the `name` attribute has only be introduced with WSC 3.0, it is not yet mandatory to allow backwards compatibility.
-If `name` is not given, the system automatically sets the name based on the id of the event listener in the database.
-
-### `<templatename>`
-
-The template name is the name of the template in which the event is fired. It correspondes to the `eventclassname` field of event listeners.
-
-### `<eventname>`
-
-The event name is the name given when the event is fired to identify different events within the same template.
-
-### `<templatecode>`
-
-The given template code is literally copied into the target template during compile time.
-The original template is not modified.
-If multiple template listeners listen to a single event their output is concatenated using the line feed character (`\n`, U+000A) in the order defined by the [`niceValue`](#niceValue).
-
-{% include callout.html content="It is recommend that the only code is an `{include}` of a template to enable changes by the administrator. Names of templates included by a template listener start with two underscores by convention." type="warning" %}
-
-### `<environment>`
-
-The value of the environment element can either be `admin` or `user` and is `user` if no value is given.
-The value determines if the template listener will be executed in the frontend (`user`) or the backend (`admin`).
-
-### `<nice>`
-
-<span class="label label-info">Optional</span>
-
-The nice value element can contain an integer value out of the interval `[-128,127]` with `0` being the default value if the element is omitted.
-The nice value determines the execution order of template listeners.
-Template listeners with smaller nice values are executed first.
-If the nice value of two template listeners is equal, the order is undefined.
-
-{% include callout.html content="If you pass a value out of the mentioned interval, the value will be adjusted to the closest value in the interval." type="info" %}
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the template listener to be executed.
-
-### `<permissions>`
-
-<span class="label label-info">Optional</span>
-
-The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the template listener to be executed.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/templatelistener.xsd">
- <import>
- <templatelistener name="example">
- <environment>user</environment>
- <templatename>headIncludeJavaScript</templatename>
- <eventname>javascriptInclude</eventname>
- <templatecode><![CDATA[{include file='__myCustomJavaScript'}]]></templatecode>
- </templatelistener>
- </import>
-
- <delete>
- <templatelistener name="oldTemplateListenerName" />
- </delete>
-</data>
-```
+++ /dev/null
----
-title: Template Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_template.html
-folder: package/pip
-parent: package_pip
----
-
-Add templates for frontend pages and forms by providing an archive containing the template files.
-
-{% include callout.html content="You cannot overwrite templates provided by other packages." type="warning" %}
-
-This package installation plugin behaves exactly like the [acpTemplate package installation plugin](package_pip_acp-template.html) except for installing frontend templates instead of backend/acp templates.
+++ /dev/null
----
-title: User Group Option Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_user-group-option.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new user group options (“permissions”).
-The behaviour of this package installation plugin closely follows the [option](package_pip_option.html) PIP.
-
-## Category Components
-
-The category definition works exactly like the option PIP.
-
-## Option Components
-
-The fields `hidden`, `supporti18n` and `requirei18n` do not apply.
-The following extra fields are defined:
-
-### `<(admin|mod|user)defaultvalue>`
-
-Defines the `defaultvalue`s for subsets of the groups:
-
-| Type | Description |
-| ----- | ---------------------------------------------------------------------------------------------- |
-| admin | Groups where the `admin.user.accessibleGroups` user group option includes every group. |
-| mod | Groups where the `mod.general.canUseModeration` is set to `true`. |
-| user | Groups where the internal group type is neither `UserGroup::EVERYONE` nor `UserGroup::GUESTS`. |
-
-### `<usersonly>`
-
-Makes the option unavailable for groups with the group type `UserGroup::GUESTS`.
-
-## Language Items
-
-All relevant language items have to be put into the `wcf.acp.group` language item category.
-
-### Categories
-
-If you install a category named `user.foo`, you have to provide the language item `wcf.acp.group.option.category.user.foo`, which is used when displaying the options.
-If you want to provide an optional description of the category, you have to provide the language item `wcf.acp.group.option.category.user.foo.description`.
-Descriptions are only relevant for categories whose parent has a parent itself, i.e. categories on the third level.
-
-### Options
-
-If you install an option named `user.foo.canBar`, you have to provide the language item `wcf.acp.group.option.user.foo.canBar`, which is used as a label for setting the option value.
-If you want to provide an optional description of the option, you have to provide the language item `wcf.acp.group.option.user.foo.canBar.description`.
+++ /dev/null
----
-title: User Menu Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_user-menu.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new user menu items.
-
-## Components
-
-Each item is described as an `<usermenuitem>` element with the mandatory attribute `name`.
-
-### `<parent>`
-
-<span class="label label-info">Optional</span>
-
-The item’s parent item.
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Specifies the order of this item within the parent item.
-
-### `<controller>`
-
-The fully qualified class name of the target controller.
-If not specified this item serves as a category.
-
-### `<link>`
-
-Additional components if `<controller>` is set,
-the full external link otherwise.
-
-### `<iconclassname>`
-
-{% include tip.html content="Use an icon only for top-level items." %}
-
-Name of the Font Awesome icon class.
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the menu item to be shown.
-
-### `<permissions>`
-
-<span class="label label-info">Optional</span>
-
-The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the menu item to be shown.
-
-### `<classname>`
-
-The name of the class providing the user menu item’s behaviour,
-the class has to implement the `wcf\system\menu\user\IUserMenuItemProvider` interface.
-
-
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userMenu.xsd">
- <import>
- <usermenuitem name="wcf.user.menu.foo">
- <iconclassname>fa-home</iconclassname>
- </usermenuitem>
-
- <usermenuitem name="wcf.user.menu.foo.bar">
- <controller>wcf\page\FooBarListPage</controller>
- <parent>wcf.user.menu.foo</parent>
- <permissions>user.foo.canBar</permissions>
- <classname>wcf\system\menu\user\FooBarMenuItemProvider</classname>
- </usermenuitem>
-
- <usermenuitem name="wcf.user.menu.foo.baz">
- <controller>wcf\page\FooBazListPage</controller>
- <parent>wcf.user.menu.foo</parent>
- <permissions>user.foo.canBaz</permissions>
- <options>module_foo_bar</options>
- </usermenuitem>
- </import>
-</data>
-```
+++ /dev/null
----
-title: User Notification Event Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_user-notification-event.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new user notification events.
-
-## Components
-
-Each package installation plugin is described as an `<event>` element with the mandatory child `<name>`.
-
-### `<objectType>`
-
-{% include warning.html content="The `(name, objectType)` pair must be unique." %}
-
-The given object type must implement the `com.woltlab.wcf.notification.objectType` definition.
-
-### `<classname>`
-
-The name of the class providing the event's behaviour,
-the class has to implement the `wcf\system\user\notification\event\IUserNotificationEvent` interface.
-
-### `<preset>`
-
-Defines whether this event is enabled by default.
-
-### `<presetmailnotificationtype>`
-
-{% include callout.html content="Avoid using this option, as sending unsolicited mail can be seen as spamming." type="info" %}
-
-One of `instant` or `daily`.
-Defines whether this type of email notifications is enabled by default.
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the notification type to be available.
-
-### `<permissions>`
-
-<span class="label label-info">Optional</span>
-
-The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the notification type to be available.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userNotificationEvent.xsd">
- <import>
- <event>
- <name>like</name>
- <objecttype>com.woltlab.example.comment.like.notification</objecttype>
- <classname>wcf\system\user\notification\event\ExampleCommentLikeUserNotificationEvent</classname>
- <preset>1</preset>
- <options>module_like</options>
- </event>
- </import>
-</data>
-```
+++ /dev/null
----
-title: User Option Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_user-option.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new user options (profile fields / user settings).
-The behaviour of this package installation plugin closely follows the [option](package_pip_option.html) PIP.
-
-## Category Components
-
-The category definition works exactly like the option PIP.
-
-## Option Components
-
-The fields `hidden`, `supporti18n` and `requirei18n` do not apply.
-The following extra fields are defined:
-
-### `<required>`
-
-Requires that a value is provided.
-
-### `<askduringregistration>`
-
-If set to `1` the field is shown during user registration in the frontend.
-
-### `<editable>`
-
-Bitfield with the following options (constants in `wcf\data\user\option\UserOption`)
-
-| Name | Value |
-| ---------------------------------------- | ----- |
-| EDITABILITY_OWNER | 1 |
-| EDITABILITY_ADMINISTRATOR | 2 |
-| EDITABILITY_OWNER_DURING_REGISTRATION | 4 |
-
-### `<visible>`
-
-Bitfield with the following options (constants in `wcf\data\user\option\UserOption`)
-
-| Name | Value |
-| ------------------------ | ----- |
-| VISIBILITY_OWNER | 1 |
-| VISIBILITY_ADMINISTRATOR | 2 |
-| VISIBILITY_REGISTERED | 4 |
-| VISIBILITY_GUEST | 8 |
-
-### `<searchable>`
-
-If set to `1` the field is searchable.
-
-### `<outputclass>`
-
-PHP class responsible for output formatting of this field.
-the class has to implement the `wcf\system\option\user\IUserOptionOutput` interface.
-
-## Language Items
-
-All relevant language items have to be put into the `wcf.user.option` language item category.
-
-### Categories
-
-If you install a category named `example`, you have to provide the language item `wcf.user.option.category.example`, which is used when displaying the options.
-If you want to provide an optional description of the category, you have to provide the language item `wcf.user.option.category.example.description`.
-Descriptions are only relevant for categories whose parent has a parent itself, i.e. categories on the third level.
-
-### Options
-
-If you install an option named `exampleOption`, you have to provide the language item `wcf.user.option.exampleOption`, which is used as a label for setting the option value.
-If you want to provide an optional description of the option, you have to provide the language item `wcf.user.option.exampleOption.description`.
+++ /dev/null
----
-title: User Profile Menu Package Installation Plugin
-sidebar: sidebar
-permalink: package_pip_user-profile-menu.html
-folder: package/pip
-parent: package_pip
----
-
-Registers new user profile tabs.
-
-## Components
-
-Each tab is described as an `<userprofilemenuitem>` element with the mandatory attribute `name`.
-
-### `<classname>`
-
-The name of the class providing the tab’s behaviour,
-the class has to implement the `wcf\system\menu\user\profile\content\IUserProfileMenuContent` interface.
-
-### `<showorder>`
-
-<span class="label label-info">Optional</span>
-
-Determines at which position of the tab list the tab is shown.
-
-### `<options>`
-
-<span class="label label-info">Optional</span>
-
-The options element can contain a comma-separated list of options of which at least one needs to be enabled for the tab to be shown.
-
-### `<permissions>`
-
-<span class="label label-info">Optional</span>
-
-The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the tab to be shown.
-
-## Example
-
-```xml
-<?xml version="1.0" encoding="utf-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userProfileMenu.xsd">
- <import>
- <userprofilemenuitem name="example">
- <classname>wcf\system\menu\user\profile\content\ExampleProfileMenuContent</classname>
- <showorder>3</showorder>
- <options>module_example</options>
- </userprofilemenuitem>
- </import>
-</data>
-```
+++ /dev/null
----
-title: Persistent Caches
-sidebar: sidebar
-permalink: php_api_caches_persistent-caches.html
-folder: php/api/cache
-parent: php_api_caches
----
-
-Relational databases are designed around the principle of normalized data that
-is organized across clearly separated tables with defined releations between
-data rows. While this enables you to quickly access and modify individual rows
-and columns, it can create the problem that re-assembling this data into a more
-complex structure can be quite expensive.
-
-For example, the user group permissions are stored for each user group and each
-permissions separately, but in order to be applied, they need to be fetched and
-the cumulative values across all user groups of an user have to be calculated.
-These repetitive tasks on barely ever changing data make them an excellent
-target for caching, where all sub-sequent requests are accelerated because they
-no longer have to perform the same expensive calculations every time.
-
-It is easy to get lost in the realm of caching, especially when it comes to the
-decision if you should use a cache or not. When in doubt, you should opt to not
-use them, because they also come at a hidden cost that cannot be expressed through
-simple SQL query counts. If you haven't already, it is recommended that you read
-the [introduction article on caching][php_api_caches] first, it provides a bit
-of background on caches and examples that should help you in your decision.
-
-## `AbstractCacheBuilder`
-
-Every cache builder should derive from the base class [AbstractCacheBuilder](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/builder/AbstractCacheBuilder.class.php)
-that already implements the mandatory interface [ICacheBuilder](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/builder/ICacheBuilder.class.php).
-
-```php
-<?php
-namespace wcf\system\cache\builder;
-
-class ExampleCacheBuilder extends AbstractCacheBuilder {
- // 3600 = 1hr
- protected $maxLifetime = 3600;
-
- public function rebuild(array $parameters) {
- $data = [];
-
- // fetch and process your data and assign it to `$data`
-
- return $data;
- }
-}
-```
-
-Reading data from your cache builder is quite simple and follows a consistent
-pattern. The callee only needs to know the name of the cache builder, which
-parameters it requires and how the returned data looks like. It does not need
-to know how the data is retrieve, where it was stored, nor if it had to be
-rebuild due to the maximum lifetime.
-
-```php
-<?php
-use wcf\system\cache\builder\ExampleCacheBuilder;
-
-$data = ExampleCacheBuilder::getInstance()->getData($parameters);
-```
-
-### `getData(array $parameters = [], string $arrayIndex = ''): array`
-
-Retrieves the data from the cache builder, the `$parameters` array is automatically
-sorted to allow sub-sequent requests for the same parameters to be recognized,
-even if their parameters are mixed. For example, `getData([1, 2])` and `getData([2, 1])`
-will have the same exact result.
-
-The optional `$arrayIndex` will instruct the cache builder to retrieve the data
-and examine if the returned data is an array that has the index `$arrayIndex`.
-If it is set, the potion below this index is returned instead.
-
-### `getMaxLifetime(): int`
-
-Returns the maximum lifetime of a cache in seconds. It can be controlled through
-the `protected $maxLifetime` property which defaults to `0`. Any cache that has
-a lifetime greater than 0 is automatically discarded when exceeding this age,
-otherwise it will remain forever until it is explicitly removed or invalidated.
-
-### `reset(array $parameters = []): void`
-
-Invalidates a cache, the `$parameters` array will again be ordered using the same
-rules that are applied for `getData()`.
-
-### `rebuild(array $parameters): array`
-
-_This method is protected._
-
-This is the only method that a cache builder deriving from `AbstractCacheBuilder`
-has to implement and it will be invoked whenever the cache is required to be
-rebuild for whatever reason.
-
-{% include links.html %}
+++ /dev/null
----
-title: Runtime Caches
-sidebar: sidebar
-permalink: php_api_caches_runtime-caches.html
-folder: php/api/cache
-parent: php_api_caches
----
-
-Runtime caches store objects created during the runtime of the script and are automatically discarded after the script terminates.
-Runtime caches are especially useful when objects are fetched by different APIs, each requiring separate requests.
-By using a runtime cache, you have two advantages:
-
-1. If the API allows it, you can delay fetching the actual objects and initially only tell the runtime cache that at some point in the future of the current request, you need the objects with the given ids.
- If multiple APIs do this one after another, all objects can be fetched using only one query instead of each API querying the database on its own.
-1. If an object with the same ID has already been fetched from database, this object is simply returned and can be reused instead of being fetched from database again.
-
-
-## `IRuntimeCache`
-
-Every runtime cache has to implement the [IRuntimeCache](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/runtime/IRuntimeCache.class.php) interface.
-It is recommended, however, that you extend [AbstractRuntimeCache](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/runtime/AbstractRuntimeCache.class.php), the default implementation of the runtime cache interface.
-In most instances, you only need to set the `AbstractRuntimeCache::$listClassName` property to the name of database object list class which fetches the cached objects from database (see [example](#example)).
-
-
-## Usage
-
-```php
-<?php
-use wcf\system\cache\runtime\UserRuntimeCache;
-
-$userIDs = [1, 2];
-
-// first (optional) step: tell runtime cache to remember user ids
-UserRuntimeCache::getInstance()->cacheObjectIDs($userIDs);
-
-// […]
-
-// second step: fetch the objects from database
-$users = UserRuntimeCache::getInstance()->getObjects($userIDs);
-
-// somewhere else: fetch only one user
-$userID = 1;
-
-UserRuntimeCache::getInstance()->cacheObjectID($userID);
-
-// […]
-
-// get user without the cache actually fetching it from database because it has already been loaded
-$user = UserRuntimeCache::getInstance()->getObject($userID);
-
-// somewhere else: fetch users directly without caching user ids first
-$users = UserRuntimeCache::getInstance()->getObjects([3, 4]);
-```
-
-
-## Example
-
-```php
-<?php
-namespace wcf\system\cache\runtime;
-use wcf\data\user\User;
-use wcf\data\user\UserList;
-
-/**
- * Runtime cache implementation for users.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2016 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\System\Cache\Runtime
- * @since 3.0
- *
- * @method User[] getCachedObjects()
- * @method User getObject($objectID)
- * @method User[] getObjects(array $objectIDs)
- */
-class UserRuntimeCache extends AbstractRuntimeCache {
- /**
- * @inheritDoc
- */
- protected $listClassName = UserList::class;
-}
-```
+++ /dev/null
----
-title: Form Node Dependencies
-sidebar: sidebar
-permalink: php_api_form_builder-dependencies.html
-folder: php/api/formBuilder
-parent: php_api_form_builder
----
-
-Form node dependencies allow to make parts of a form dynamically available or unavailable depending on the values of form fields.
-Dependencies are always added to the object whose visibility is determined by certain form fields.
-They are **not** added to the form field’s whose values determine the visibility!
-An example is a text form field that should only be available if a certain option from a single selection form field is selected.
-Form builder’s dependency system supports such scenarios and also automatically making form containers unavailable once all of its children are unavailable.
-
-If a form node has multiple dependencies and one of them is not met, the form node is unavailable.
-A form node not being available due to dependencies has to the following consequences:
-
-- The form field value is not validated. It is, however, read from the request data as all request data needs to be read first so that the dependencies can determine whether they are met or not.
-- No data is collected for the form field and returned by `IFormDocument::getData()`.
-- In the actual form, the form field will be hidden via JavaScript.
-
-
-## `IFormFieldDependency`
-
-The basis of the dependencies is the `IFormFieldDependency` interface that has to be implemented by every dependency class.
-The interface requires the following methods:
-
-- `checkDependency()` checks if the dependency is met, thus if the dependant form field should be considered available.
-- `dependentNode(IFormNode $node)` and `getDependentNode()` can be used to set and get the node whose availability depends on the referenced form field.
- `TFormNode::addDependency()` automatically calls `dependentNode(IFormNode $node)` with itself as the dependent node, thus the dependent node is automatically set by the API.
-- `field(IFormField $field)` and `getField()` can be used to set and get the form field that influences the availability of the dependent node.
-- `fieldId($fieldId)` and `getFieldId()` can be used to set and get the id of the form field that influences the availability of the dependent node.
-- `getHtml()` returns JavaScript code required to ensure the dependency in the form output.
-- `getId()` returns the id of the dependency used to identify multiple dependencies of the same form node.
-- `static create($id)` is the factory method that has to be used to create new dependencies with the given id.
-
-`AbstractFormFieldDependency` provides default implementations for all methods except for `checkDependency()`.
-
-Using `fieldId($fieldId)` instead of `field(IFormField $field)` makes sense when adding the dependency directly when setting up the form:
-
-```php
-$container->appendChildren([
- FooField::create('a'),
-
- BarField::create('b')
- ->addDependency(
- BazDependency::create('a')
- ->fieldId('a')
- )
-]);
-```
-
-Here, without an additional assignment, the first field with id `a` cannot be accessed thus `fieldId($fieldId)` should be used as the id of the relevant field is known.
-When the form is built, all dependencies that only know the id of the relevant field and do not have a reference for the actual object are populated with the actual form field objects.
-
-
-## Default Dependencies
-
-WoltLab Suite Core delivers the following two default dependency classes by default:
-
-- `NonEmptyFormFieldDependency` can be used to ensure that a node is only shown if the value of the referenced form field is not empty (being empty is determined using PHP’s `empty` function).
-- `ValueFormFieldDependency` can be used to ensure that a node is only shown if the value of the referenced form field is from a specified list of of values (see methods `values($values)` and `getValues()`).
- Additionally, via `negate($negate = true)` and `isNegated()`, the locic can also be inverted by requiring the value of the referenced form field not to be from a specified list of values.
-
-
-## JavaScript Implementation
-
-To ensure that dependent node are correctly shown and hidden when changing the value of referenced form fields, every PHP dependency class has a corresponding JavaScript module that checks the dependency in the browser.
-Every JavaScript dependency has to extend `WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract` and implement the `checkDependency()` function, the JavaScript version of `IFormFieldDependency::checkDependency()`.
-
-All of the JavaScript dependency objects automatically register themselves during initialization with the `WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager` which takes care of checking the dependencies at the correct points in time.
-
-Additionally, the dependency manager also ensures that form containers in which all children are hidden due to dependencies are also hidden and, once any child becomes available again, makes the container also available again.
-Every form container has to create a matching form container dependency object from a module based on `WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract`.
-
-
-## Examples
-
-If `$booleanFormField` is an instance of `BooleanFormField` and the text form field `$textFormField` should only be available if “Yes” has been selected, the following condition has to be set up:
-
-```php
-$textFormField->addDependency(
- NonEmptyFormFieldDependency::create('booleanFormField')
- ->field($booleanFormField)
-);
-```
-
-If `$singleSelectionFormField` is an instance of `SingleSelectionFormField` that offers the options `1`, `2`, and `3` and `$textFormField` should only be available if `1` or `3` is selected, the following condition has to be set up:
-
-```php
-$textFormField->addDependency(
- NonEmptyFormFieldDependency::create('singleSelectionFormField')
- ->field($singleSelectionFormField)
- ->values([1, 3])
-);
-```
-
-If, in contrast, `$singleSelectionFormField` has many available options and `7` is the only option for which `$textFormField` should **not** be available, `negate()` should be used:
-
-```php
-$textFormField->addDependency(
- NonEmptyFormFieldDependency::create('singleSelectionFormField')
- ->field($singleSelectionFormField)
- ->values([7])
- ->negate()
-);
-```
+++ /dev/null
----
-title: Form Builder Fields
-sidebar: sidebar
-permalink: php_api_form_builder-form_fields.html
-folder: php/api/formBuilder
-parent: php_api_form_builder
----
-
-## Abstract Form Fields
-
-The following form field classes cannot be instantiated directly because they are abstract, but they can/must be used when creating own form field classes.
-
-
-### `AbstractFormField`
-
-`AbstractFormField` is the abstract default implementation of the `IFormField` interface and it is expected that every implementation of `IFormField` implements the interface by extending this class.
-
-
-### `AbstractNumericFormField`
-
-`AbstractNumericFormField` is the abstract implementation of a form field handling a single numeric value.
-The class implements `IImmutableFormField`, `IMaximumFormField`, `IMinimumFormField`, `INullableFormField`, `IPlaceholderFormField` and `ISuffixedFormField`.
-If the property `$integerValues` is `true`, the form field works with integer values, otherwise it works with floating point numbers.
-The methods `step($step = null)` and `getStep()` can be used to set and get the step attribute of the `input` element.
-The default step for form fields with integer values is `1`.
-Otherwise, the default step is `any`.
-
-
-## General Form Fields
-
-The following form fields are general reusable fields without any underlying context.
-
-
-### `BooleanFormField`
-
-`BooleanFormField` is used for boolean (`0` or `1`, `yes` or `no`) values.
-Objects of this class require a label.
-The return value of `getSaveValue()` is the integer representation of the boolean value, i.e. `0` or `1`.
-
-
-### `ClassNameFormField`
-
-`ClassNameFormField` is a [text form field](#textformfield) that supports additional settings, specific to entering a PHP class name:
-
-- `classExists($classExists = true)` and `getClassExists()` can be used to ensure that the entered class currently exists in the installation.
- By default, the existance of the entered class is required.
-- `implementedInterface($interface)` and `getImplementedInterface()` can be used to ensure that the entered class implements the specified interface.
- By default, no interface is required.
-- `parentClass($parentClass)` and `getParentClass()` can be used to ensure that the entered class extends the specified class.
- By default, no parent class is required.
-- `instantiable($instantiable = true)` and `isInstantiable()` can be used to ensure that the entered class is instantiable.
- By default, entered classes have to instantiable.
-
-Additionally, the default id of a `ClassNameFormField` object is `className`, the default label is `wcf.form.field.className`, and if either an interface or a parent class is required, a default description is set if no description has already been set (`wcf.form.field.className.description.interface` and `wcf.form.field.className.description.parentClass`, respectively).
-
-
-### `DateFormField`
-
-`DateFormField` is a form field to enter a date (and optionally a time).
-The following methods are specific to this form field class:
-
-- `earliestDate($earliestDate)` and `getEarliestDate()` can be used to get and set the earliest selectable/valid date and `latestDate($latestDate)` and `getLatestDate()` can be used to get and set the latest selectable/valid date.
- The date passed to the setters must have the same format as set via `saveValueFormat()`.
- If a custom format is used, that format has to be set via `saveValueFormat()` before calling any of the setters.
-- `saveValueFormat($saveValueFormat)` and `getSaveValueFormat()` can be used to specify the date format of the value returned by `getSaveValue()`.
- By default, `U` is used as format.
- The [PHP manual](https://secure.php.net/manual/en/function.date.php) provides an overview of supported formats.
-- `supportTime($supportsTime = true)` and `supportsTime()` can be used to toggle whether, in addition to a date, a time can also be specified.
- By default, specifying a time is disabled.
-
-
-### `DescriptionFormField`
-
-`DescriptionFormField` is a [multi-line text form field](#multilinetextformfield) with `description` as the default id and `wcf.global.description` as the default label.
-
-
-### `FloatFormField`
-
-`FloatFormField` is an implementation of [AbstractNumericFormField](#abstractnumericformfield) for floating point numbers.
-
-
-### `IconFormField`
-
-`IconFormField` is a form field to select a FontAwesome icon.
-
-
-### `IntegerFormField`
-
-`IntegerFormField` is an implementation of [AbstractNumericFormField](#abstractnumericformfield) for integers.
-
-
-### `IsDisabledFormField`
-
-`IsDisabledFormField` is a [boolean form field](#booleanformfield) with `isDisabled` as the default id.
-
-
-### `ItemListFormField`
-
-`ItemListFormField` is a form field in which multiple values can be entered and returned in different formats as save value.
-The `saveValueType($saveValueType)` and `getSaveValueType()` methods are specific to this form field class and determine the format of the save value.
-The following save value types are supported:
-
-- `ItemListFormField::SAVE_VALUE_TYPE_ARRAY` adds a custom data processor that writes the form field data directly in the parameters array and not in the data sub-array of the parameters array.
-- `ItemListFormField::SAVE_VALUE_TYPE_CSV` lets the value be returned as a string in which the values are concatenated by commas.
-- `ItemListFormField::SAVE_VALUE_TYPE_NSV` lets the value be returned as a string in which the values are concatenated by `\n`.
-- `ItemListFormField::SAVE_VALUE_TYPE_SSV` lets the value be returned as a string in which the values are concatenated by spaces.
-
-By default, `ItemListFormField::SAVE_VALUE_TYPE_CSV` is used.
-
-If `ItemListFormField::SAVE_VALUE_TYPE_ARRAY` is used as save value type, `ItemListFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant array into the `$parameters` array directly using the object property as the array key.
-
-
-### `MultilineTextFormField`
-
-`MultilineTextFormField` is a [text form field](#textformfield) that supports multiple rows of text.
-The methods `rows($rows)` and `getRows()` can be used to set and get the number of rows of the `textarea` elements.
-The default number of rows is `10`.
-These methods do **not**, however, restrict the number of text rows that canbe entered.
-
-
-### `MultipleSelectionFormField`
-
-`MultipleSelectionFormField` is a form fields that allows the selection of multiple options out of a predefined list of available options.
-The class implements `IFilterableSelectionFormField`, `IImmutableFormField`, and `INullableFormField`.
-If the field is nullable and no option is selected, `null` is returned as the save value.
-
-
-### `RadioButtonFormField`
-
-`RadioButtonFormField` is a form fields that allows the selection of a single option out of a predefined list of available options using radiobuttons.
-The class implements `IImmutableFormField` and `ISelectionFormField`.
-
-
-### `RatingFormField`
-
-`RatingFormField` is a form field to set a rating for an object.
-The class implements `IImmutableFormField`, `IMaximumFormField`, `IMinimumFormField`, and `INullableFormField`.
-Form fields of this class have `rating` as their default id, `wcf.form.field.rating` as their default label, `1` as their default minimum, and `5` as their default maximum.
-For this field, the minimum and maximum refer to the minimum and maximum rating an object can get.
-When the field is shown, there will be `maximum() - minimum() + 1` icons be shown with additional CSS classes that can be set and gotten via `defaultCssClasses(array $cssClasses)` and `getDefaultCssClasses()`.
-If a rating values is set, the first `getValue()` icons will instead use the classes that can be set and gotten via `activeCssClasses(array $cssClasses)` and `getActiveCssClasses()`.
-By default, the only default class is `fa-star-o` and the active classes are `fa-star` and `orange`.
-
-
-### `ShowOrderFormField`
-
-`ShowOrderFormField` is a [single selection form field](#singleselectionformfield) for which the selected value determines the position at which an object is shown.
-The show order field provides a list of all siblings and the object will be positioned **after** the selected sibling.
-To insert objects at the very beginning, the `options()` automatically method prepends an additional option for that case so that only the existing siblings need to be passed.
-The default id of instances of this class is `showOrder` and their default label is `wcf.form.field.showOrder`.
-
-{% include callout.html content="It is important that the relevant object property is always kept updated. Whenever a new object is added or an existing object is edited or delete, the values of the other objects have to be adjusted to ensure consecutive numbering." type="info" %}
-
-
-### `SingleSelectionFormField`
-
-`SingleSelectionFormField` is a form fields that allows the selection of a single option out of a predefined list of available options.
-The class implements `IFilterableSelectionFormField`, `IImmutableFormField`, and `INullableFormField`.
-If the field is nullable and the current form field value is considered `empty` by PHP, `null` is returned as the save value.
-
-
-### `SortOrderFormField`
-
-`SingleSelectionFormField` is a [single selection form field](#singleselectionformfield) with default id `sortOrder`, default label `wcf.global.showOrder` and default options `ASC: wcf.global.sortOrder.ascending` and `DESC: wcf.global.sortOrder.descending`.
-
-
-### `TextFormField`
-
-`TextFormField` is a form field that allows entering a single line of text.
-The class implements `IImmutableFormField`, `II18nFormField`, `IMaximumLengthFormField`, `IMinimumLengthFormField`, and `IPlaceholderFormField`.
-
-
-### `TitleFormField`
-
-`TitleFormField` is a [text form field](#textformfield) with `title` as the default id and `wcf.global.title` as the default label.
-
-
-### `UrlFormField`
-
-`UrlFormField` is a [text form field](#textformfield) whose values are checked via `Url::is()`.
-
-
-
-## Specific Fields
-
-The following form fields are reusable fields that generally are bound to a certain API or `DatabaseObject` implementation.
-
-
-### `AclFormField`
-
-`AclFormField` is used for setting up acl values for specific objects.
-The class implements `IObjectTypeFormField` and requires an object type of the object type definition `com.woltlab.wcf.acl`.
-Additionally, the class provides the methods `categoryName($categoryName)` and `getCategoryName()` that allow setting a specific name or filter for the acl option categories whose acl options are shown.
-A category name of `null` signals that no category filter is used.
-
-`AclFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant ACL object type id into the `$parameters` array directly using `{$objectProperty}_aclObjectTypeID` as the array key.
-The relevant database object action method is expected, based on the given ACL object type id, to save the ACL option values appropriately.
-
-
-### `CaptchaFormField`
-
-`CaptchaFormField` is used to add captcha protection to the form.
-
-You must specify a captcha object type (`com.woltlab.wcf.captcha`) using the `objectType()` method.
-
-
-### `ContentLanguageFormField`
-
-`ContentLanguageFormField` is used to select the content language of an object.
-Fields of this class are only available if multilingualism is enabled and if there are content languages.
-The class implements `IImmutableFormField`.
-
-
-### `LabelFormField`
-
-`LabelFormField` is used to select a label from a specific label group.
-The class implements `IObjectTypeFormNode`.
-
-The `labelGroup(ViewableLabelGroup $labelGroup)` and `getLabelGroup()` methods are specific to this form field class and can be used to set and get the label group whose labels can be selected.
-Additionally, there is the static method `createFields($objectType, array $labelGroups, $objectProperty = 'labelIDs)` that can be used to create all relevant label form fields for a given list of label groups.
-In most cases, `LabelFormField::createFields()` should be used.
-
-
-### `OptionFormField`
-
-`OptionFormField` is an [item list form field](#itemlistformfield) to set a list of options.
-The class implements `IPackagesFormField` and only options of the set packages are considered available.
-The default label of instances of this class is `wcf.form.field.option` and their default id is `options`.
-
-
-### `SimpleAclFormField`
-
-`SimpleAclFormField` is used for setting up simple acl values (one `yes`/`no` option per user and user group) for specific objects.
-
-`SimpleAclFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant simple ACL data array into the `$parameters` array directly using the object property as the array key.
-
-
-### `SingleMediaSelectionFormField`
-
-`SingleMediaSelectionFormField` is used to select a specific media file.
-The class implements `IImmutableFormField`.
-
-The following methods are specific to this form field class:
-
-- `imageOnly($imageOnly = true)` and `isImageOnly()` can be used to set and check if only images may be selected.
-- `getMedia()` returns the media file based on the current field value if a field is set.
-
-
-### `TagFormField`
-
-`TagFormField` is a form field to enter tags.
-Arrays passed to `TagFormField::values()` can contain tag names as strings and `Tag` objects.
-The default label of instances of this class is `wcf.tagging.tags` and their default description is `wcf.tagging.tags.description`.
-
-`TagFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the array with entered tag names into the `$parameters` array directly using the object property as the array key.
-
-
-### `UploadFormField`
-
-`UploadFormField` is a form field that allows uploading files by the user.
-
-`UploadFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the array of `wcf\system\file\upload\UploadFile\UploadFile` into the `$parameters` array directly using the object property as the array key. Also it registers the removed files as an array of `wcf\system\file\upload\UploadFile\UploadFile` into the `$parameters` array directly using the object property with the suffix `_removedFiles` as the array key.
-
-The field supports additional settings:
-- `imageOnly($imageOnly = true)` and `isImageOnly()` can be used to ensure that the uploaded files are only images.
-- `allowSvgImage($allowSvgImages = true)` and `svgImageAllowed()` can be used to allow SVG images, if the image only mode is enabled (otherwise, the method will throw an exception). By default, SVG images are not allowed.
-
-#### Provide value from database object
-
-To provide values from a database object, you should implement the method `get{$objectProperty}UploadFileLocations()` to your database object class. This method must return an array of strings with the locations of the files.
-
-#### Process files
-
-To process files in the database object action class, you must [`rename`](https://secure.php.net/manual/en/function.rename.php) the file to the final destination. You get the temporary location, by calling the method `getLocation()` on the given `UploadFile` objects. After that, you call `setProcessed($location)` with `$location` contains the new file location. This method sets the `isProcessed` flag to true and saves the new location. For updating files, it is relevant, whether a given file is already processed or not. For this case, the `UploadFile` object has an method `isProcessed()` which indicates, whether a file is already processed or new uploaded.
-
-
-### `UserFormField`
-
-`UserFormField` is a form field to enter existing users.
-The class implements `IAutoFocusFormField`, `IImmutableFormField`, `IMultipleFormField`, and `INullableFormField`.
-While the user is presented the names of the specified users in the user interface, the field returns the ids of the users as data.
-The relevant `UserProfile` objects can be accessed via the `getUsers()` method.
-
-
-### `UserGroupOptionFormField`
-
-`UserGroupOptionFormField` is an [item list form field](#itemlistformfield) to set a list of user group options/permissions.
-The class implements `IPackagesFormField` and only user group options of the set packages are considered available.
-The default label of instances of this class is `wcf.form.field.userGroupOption` and their default id is `permissions`.
-
-
-### `UsernameFormField`
-
-`UsernameFormField` is used for entering one non-existing username.
-The class implements `IImmutableFormField`, `IMaximumLengthFormField`, `IMinimumLengthFormField`, `INullableFormField`, and `IPlaceholderFormField`.
-As usernames have a system-wide restriction of a minimum length of 3 and a maximum length of 100 characters, these values are also used as the default value for the field’s minimum and maximum length.
-
-
-
-## Wysiwyg form container
-
-To integrate a wysiwyg editor into a form, you have to create a `WysiwygFormContainer` object.
-This container takes care of creating all necessary form nodes listed below for a wysiwyg editor.
-
-{% include callout.html content="When creating the container object, its id has to be the id of the form field that will manage the actual text." type="warning" %}
-
-The following methods are specific to this form container class:
-
-- `addSettingsNode(IFormChildNode $settingsNode)` and `addSettingsNodes(array $settingsNodes)` can be used to add nodes to the settings tab container.
-- `attachmentData($objectType, $parentObjectID)` can be used to set the data relevant for attachment support.
- By default, not attachment data is set, thus attachments are not supported.
-- `getAttachmentField()`, `getPollContainer()`, `getSettingsContainer()`, `getSmiliesContainer()`, and `getWysiwygField()` can be used to get the different components of the wysiwyg form container once the form has been built.
-- `enablePreviewButton($enablePreviewButton)` can be used to set whether the preview button for the message is shown or not.
- By default, the preview button is shown.
- This method is only relevant before the form is built.
- Afterwards, the preview button availability can not be changed.
- Only available since WoltLab Suite Core 5.3.
-- `getObjectId()` returns the id of the edited object or `0` if no object is edited.
-- `getPreselect()`, `preselect($preselect)` can be used to set the value of the wysiwyg tab menu's `data-preselect` attribute used to determine which tab is preselected.
- By default, the preselect is `'true'` which is used to pre-select the first tab.
-- `messageObjectType($messageObjectType)` can be used to set the message object type.
-- `pollObjectType($pollObjectType)` can be used to set the poll object type.
- By default, no poll object type is set, thus the poll form field container is not available.
-- `supportMentions($supportMentions)` can be used to set if mentions are supported.
- By default, mentions are not supported.
- This method is only relevant before the form is built.
- Afterwards, mention support can only be changed via the wysiwyg form field.
-- `supportSmilies($supportSmilies)` can be used to set if smilies are supported.
- By default, smilies are supported.
- This method is only relevant before the form is built.
- Afterwards, smiley availability can only be changed via changing the availability of the smilies form container.
-
-### `WysiwygAttachmentFormField`
-
-`WysiwygAttachmentFormField` provides attachment support for a wysiwyg editor via a tab in the menu below the editor.
-This class should not be used directly but only via `WysiwygFormContainer`.
-The methods `attachmentHandler(AttachmentHandler $attachmentHandler)` and `getAttachmentHandler()` can be used to set and get the `AttachmentHandler` object that is used for uploaded attachments.
-
-### `WysiwygPollFormContainer`
-
-`WysiwygPollFormContainer` provides poll support for a wysiwyg editor via a tab in the menu below the editor.
-This class should not be used directly but only via `WysiwygFormContainer`.
-`WysiwygPollFormContainer` contains all form fields that are required to create polls and requires edited objects to implement `IPollContainer`.
-
-The following methods are specific to this form container class:
-
-- `getEndTimeField()` returns the form field to set the end time of the poll once the form has been built.
-- `getIsChangeableField()` returns the form field to set if poll votes can be changed once the form has been built.
-- `getIsPublicField()` returns the form field to set if poll results are public once the form has been built.
-- `getMaxVotesField()` returns the form field to set the maximum number of votes once the form has been built.
-- `getOptionsField()` returns the form field to set the poll options once the form has been built.
-- `getQuestionField()` returns the form field to set the poll question once the form has been built.
-- `getResultsRequireVoteField()` returns the form field to set if viewing the poll results requires voting once the form has been built.
-- `getSortByVotesField()` returns the form field to set if the results are sorted by votes once the form has been built.
-
-### `WysiwygSmileyFormContainer`
-
-`WysiwygSmileyFormContainer` provides smiley support for a wysiwyg editor via a tab in the menu below the editor.
-This class should not be used directly but only via `WysiwygFormContainer`.
-`WysiwygSmileyFormContainer` creates a sub-tab for each smiley category.
-
-#### `WysiwygSmileyFormNode`
-
-`WysiwygSmileyFormNode` is contains the smilies of a specific category.
-This class should not be used directly but only via `WysiwygSmileyFormContainer`.
-
-### Example
-
-The following code creates a WYSIWYG editor component for a `message` object property.
-As smilies are supported by default and an attachment object type is given, the tab menu below the editor has two tabs: “Smilies” and “Attachments”.
-Additionally, mentions and quotes are supported.
-
-```php
-WysiwygFormContainer::create('message')
- ->label('foo.bar.message')
- ->messageObjectType('com.example.foo.bar')
- ->attachmentData('com.example.foo.bar')
- ->supportMentions()
- ->supportQuotes()
-```
-
-
-### `WysiwygFormField`
-
-`WysiwygFormField` is used for wysiwyg editor form fields.
-This class should, in general, not be used directly but only via `WysiwygFormContainer`.
-The class implements `IMaximumLengthFormField`, `IMinimumLengthFormField`, and `IObjectTypeFormNode` and requires an object type of the object type definition `com.woltlab.wcf.message`.
-The following methods are specific to this form field class:
-
-- `autosaveId($autosaveId)` and `getAutosaveId()` can be used enable automatically saving the current editor contents in the browser using the given id.
- An empty string signals that autosaving is disabled.
-- `lastEditTime($lastEditTime)` and `getLastEditTime()` can be used to set the last time the contents have been edited and saved so that the JavaScript can determine if the contents stored in the browser are older or newer.
- `0` signals that no last edit time has been set.
-- `supportAttachments($supportAttachments)` and `supportsAttachments()` can be used to set and check if the form field supports attachments.
-
- {% include callout.html content="It is not sufficient to simply signal attachment support via these methods for attachments to work. These methods are relevant internally to signal the Javascript code that the editor supports attachments. Actual attachment support is provided by `WysiwygAttachmentFormField`." type="warning" %}
-- `supportMentions($supportMentions)` and `supportsMentions()` can be used to set and check if the form field supports mentions of other users.
-
-`WysiwygFormField` objects register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the relevant simple ACL data array into the `$parameters` array directly using the object property as the array key.
-
-
-### `TWysiwygFormNode`
-
-All form nodes that need to know the id of the `WysiwygFormField` field should use `TWysiwygFormNode`.
-This trait provides `getWysiwygId()` and `wysiwygId($wysiwygId)` to get and set the relevant wysiwyg editor id.
-
-
-
-## Single-Use Form Fields
-
-The following form fields are specific for certain forms and hardly reusable in other contexts.
-
-
-### `BBCodeAttributesFormField`
-
-`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the attributes of a BBCode.
-
-
-### `DevtoolsProjectExcludedPackagesFormField`
-
-`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the excluded packages of a devtools project.
-
-
-### `DevtoolsProjectInstructionsFormField`
-
-`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the installation and update instructions of a devtools project.
-
-
-### `DevtoolsProjectOptionalPackagesFormField`
-
-`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the optional packages of a devtools project.
-
-
-### `DevtoolsProjectRequiredPackagesFormField`
-
-`DevtoolsProjectExcludedPackagesFormField` is a form field for setting the required packages of a devtools project.
+++ /dev/null
----
-title: Structure of Form Builder
-sidebar: sidebar
-permalink: php_api_form_builder-structure.html
-folder: php/api/formBuilder
-parent: php_api_form_builder
----
-
-Forms built with form builder consist of three major structural elements listed from top to bottom:
-
-1. form document,
-1. form container,
-1. form field.
-
-The basis for all three elements are form nodes.
-
-{% include callout.html content="The form builder API uses fluent interfaces heavily, meaning that unless a method is a getter, it generally returns the objects itself to support method chaining." type="info" %}
-
-
-## Form Nodes
-
-- `IFormNode` is the base interface that any node of a form has to implement.
-- `IFormChildNode` extends `IFormNode` for such elements of a form that can be a child node to a parent node.
-- `IFormParentNode` extends `IFormNode` for such elements of a form that can be a parent to child nodes.
-- `IFormElement` extends `IFormNode` for such elements of a form that can have a description and a label.
-
-
-### `IFormNode` / `TFormNode`
-
-`IFormNode` is the base interface that any node of a form has to implement and it requires the following methods:
-
-- `addClass($class)`, `addClasses(array $classes)`, `removeClass($class)`, `getClasses()`, and `hasClass($class)` add, remove, get, and check for CSS classes of the HTML element representing the form node.
- If the form node consists of multiple (nested) HTML elements, the classes are generally added to the top element.
- `static validateClass($class)` is used to check if a given CSS class is valid.
- By default, a form node has no CSS classes.
-- `addDependency(IFormFieldDependency $dependency)`, `removeDependency($dependencyId)`, `getDependencies()`, and `hasDependency($dependencyId)` add, remove, get, and check for dependencies of this form node on other form fields.
- `checkDependencies()` checks if **all** of the node’s dependencies are met and returns a boolean value reflecting the check’s result.
- The [form builder dependency documentation](php_api_form_builder-dependencies.html) provides more detailed information about dependencies and how they work.
- By default, a form node has no dependencies.
-- `attribute($name, $value = null)`, `removeAttribute($name)`, `getAttribute($name)`, `getAttributes()`, `hasAttribute($name)` add, remove, get, and check for attributes of the HTML element represting the form node.
- The attributes are added to the same element that the CSS classes are added to.
- `static validateAttribute($name)` is used to check if a given attribute is valid.
- By default, a form node has no attributes.
-- `available($available = true)` and `isAvailable()` can be used to set and check if the node is available.
- The availability functionality can be used to easily toggle form nodes based, for example, on options without having to create a condition to append the relevant.
- This way of checking availability makes it easier to set up forms.
- By default, every form node is available.
-
- The following aspects are important when working with availability:
-
- - Unavailable fields produce no output, their value is not read, they are not validated and they are not checked for save values.
- - Form fields are also able to mark themselves as unavailable, for example, a selection field without any options.
- - Form containers are automatically unavailable if they contain no available children.
-
- Availability sets the static availability for form nodes that does not change during the lifetime of a form.
- In contrast, dependencies represent a dynamic availability for form nodes that depends on the current value of certain form fields.
-- `cleanup()` is called after the whole form is not used anymore to reset other APIs if the form fields depends on them and they expect such a reset.
- This method is not intended to clean up the form field’s value as a new form document object is created to show a clean form.
-- `getDocument()` returns the `IFormDocument` object the node belongs to.
- (As `IFormDocument` extends `IFormNode`, form document objects simply return themselves.)
-- `getHtml()` returns the HTML representation of the node.
- `getHtmlVariables()` return template variables (in addition to the form node itself) to render the node’s HTML representation.
-- `id($id)` and `getId()` set and get the id of the form node.
- Every id has to be unique within a form.
- `getPrefixedId()` returns the prefixed version of the node’s id (see `IFormDocument::getPrefix()` and `IFormDocument::prefix()`).
- `static validateId($id)` is used to check if a given id is valid.
-- `populate()` is called by `IFormDocument::build()` after all form nodes have been added.
- This method should finilize the initialization of the form node after all parent-child relations of the form document have been established.
- This method is needed because during the construction of a form node, it neither knows the form document it will belong to nor does it know its parent.
-- `validate()` checks, after the form is submitted, if the form node is valid.
- A form node with children is valid if all of its child nodes are valid.
- A form field is valid if its value is valid.
-- `static create($id)` is the factory method that has to be used to create new form nodes with the given id.
-
-`TFormNode` provides a default implementation of most of these methods.
-
-
-### `IFormChildNode` / `TFormChildNode`
-
-`IFormChildNode` extends `IFormNode` for such elements of a form that can be a child node to a parent node and it requires the `parent(IFormParentNode $parentNode)` and `getParent()` methods used to set and get the node’s parent node.
-`TFormChildNode` provides a default implementation of these two methods and also of `IFormNode::getDocument()`.
-
-
-### `IFormParentNode` / `TFormParentNode`
-
-`IFormParentNode` extends `IFormNode` for such elements of a form that can be a parent to child nodes.
-Additionally, the interface also extends `\Countable` and `\RecursiveIterator`.
-The interface requires the following methods:
-
-- `appendChild(IFormChildNode $child)`, `appendChildren(array $children)`, `insertAfter(IFormChildNode $child, $referenceNodeId)`, and `insertBefore(IFormChildNode $child, $referenceNodeId)` are used to insert new children either at the end or at specific positions.
- `validateChild(IFormChildNode $child)` is used to check if a given child node can be added.
- A child node cannot be added if it would cause an id to be used twice.
-- `children()` returns the direct children of a form node.
-- `getIterator()` return a recursive iterator for a form node.
-- `getNodeById($nodeId)` returns the node with the given id by searching for it in the node’s children and recursively in all of their children.
- `contains($nodeId)` can be used to simply check if a node with the given id exists.
-- `hasValidationErrors()` checks if a form node or any of its children has a validation error (see `IFormField::getValidationErrors()`).
-- `readValues()` recursively calls `IFormParentNode::readValues()` and `IFormField::readValue()` on its children.
-
-
-### `IFormElement` / `TFormElement`
-
-`IFormElement` extends `IFormNode` for such elements of a form that can have a description and a label and it requires the following methods:
-
-- `label($languageItem = null, array $variables = [])` and `getLabel()` can be used to set and get the label of the form element.
- `requiresLabel()` can be checked if the form element requires a label.
- A label-less form element that requires a label will prevent the form from being rendered by throwing an exception.
-- `description($languageItem = null, array $variables = [])` and `getDescription()` can be used to set and get the description of the form element.
-
-
-### `IObjectTypeFormNode` / `TObjectTypeFormNode`
-
-`IObjectTypeFormField` has to be implemented by form nodes that rely on a object type of a specific object type definition in order to function.
-The implementing class has to implement the methods `objectType($objectType)`, `getObjectType()`, and `getObjectTypeDefinition()`.
-`TObjectTypeFormNode` provides a default implementation of these three methods.
-
-
-### `CustomFormNode`
-
-`CustomFormNode` is a form node whose contents can be set directly via `content($content)`.
-
-{% include callout.html content="This class should generally not be relied on. Instead, `TemplateFormNode` should be used." type="warning" %}
-
-
-### `TemplateFormNode`
-
-`TemplateFormNode` is a form node whose contents are read from a template.
-`TemplateFormNode` has the following additional methods:
-
-- `application($application)` and `getApplicaton()` can be used to set and get the abbreviation of the application the shown template belongs to.
- If no template has been set explicitly, `getApplicaton()` returns `wcf`.
-- `templateName($templateName)` and `getTemplateName()` can be used to set and get the name of the template containing the node contents.
- If no template has been set and the node is rendered, an exception will be thrown.
-- `variables(array $variables)` and `getVariables()` can be used to set and get additional variables passed to the template.
-
-
-## Form Document
-
-A form document object represents the form as a whole and has to implement the `IFormDocument` interface.
-WoltLab Suite provides a default implementation with the `FormDocument` class.
-`IFormDocument` should not be implemented directly but instead `FormDocument` should be extended to avoid issues if the `IFormDocument` interface changes in the future.
-
-`IFormDocument` extends `IFormParentNode` and requires the following additional methods:
-
-- `action($action)` and `getAction()` can be used set and get the `action` attribute of the `<form>` HTML element.
-- `addButton(IFormButton $button)` and `getButtons()` can be used add and get form buttons that are shown at the bottom of the form.
- `addDefaultButton($addDefaultButton)` and `hasDefaultButton()` can be used to set and check if the form has the default button which is added by default unless specified otherwise.
- Each implementing class may define its own default button.
- `FormDocument` has a button with id `submitButton`, label `wcf.global.button.submit`, access key `s`, and CSS class `buttonPrimary` as its default button.
-- `ajax($ajax)` and `isAjax()` can be used to set and check if the form document is requested via an AJAX request or processes data via an AJAX request.
- These methods are helpful for form fields that behave differently when providing data via AJAX.
-- `build()` has to be called once after all nodes have been added to this document to trigger `IFormNode::populate()`.
-- `formMode($formMode)` and `getFormMode()` sets the form mode.
- Possible form modes are:
-
- - `IFormDocument::FORM_MODE_CREATE` has to be used when the form is used to create a new object.
- - `IFormDocument::FORM_MODE_UPDATE` has to be used when the form is used to edit an existing object.
-- `getData()` returns the array containing the form data and which is passed as the `$parameters` argument of the constructor of a database object action object.
-- `getDataHandler()` returns the data handler for this document that is used to process the field data into a parameters array for the constructor of a database object action object.
-- `getEnctype()` returns the encoding type of the form.
- If the form contains a `IFileFormField`, `multipart/form-data` is returned, otherwise `null` is returned.
-- `loadValues(array $data, IStorableObject $object)` is used when editing an existing object to set the form field values by calling `IFormField::loadValue()` for all form fields.
- Additionally, the form mode is set to `IFormDocument::FORM_MODE_UPDATE`.
-- `method($method)` and `getMethod()` can be used to set and get the `method` attribute of the `<form>` HTML element.
- By default, the method is `post`.
-- `prefix($prefix)` and `getPrefix()` can be used to set and get a global form prefix that is prepended to form elements’ names and ids to avoid conflicts with other forms.
- By default, the prefix is an empty string.
- If a prefix of `foo` is set, `getPrefix()` returns `foo_` (additional trailing underscore).
-- `requestData(array $requestData)`, `getRequestData($index = null)`, and `hasRequestData($index = null)` can be used to set, get and check for specific request data.
- In most cases, the relevant request data is the `$_POST` array.
- In default AJAX requests handled by database object actions, however, the request data generally is in `AbstractDatabaseObjectAction::$parameters`.
- By default, `$_POST` is the request data.
-
-The last aspect is relevant for `DialogFormDocument` objects.
-`DialogFormDocument` is a specialized class for forms in dialogs that, in contrast to `FormDocument` do not require an `action` to be set.
-Additionally, `DialogFormDocument` provides the `cancelable($cancelable = true)` and `isCancelable()` methods used to determine if the dialog from can be canceled.
-By default, dialog forms are cancelable.
-
-
-## Form Button
-
-A form button object represents a button shown at the end of the form that, for example, submits the form.
-Every form button has to implement the `IFormButton` interface that extends `IFormChildNode` and `IFormElement`.
-`IFormButton` requires four methods to be implemented:
-
-- `accessKey($accessKey)` and `getAccessKey()` can be used to set and get the access key with which the form button can be activated.
- By default, form buttons have no access key set.
-- `submit($submitButton)` and `isSubmit()` can be used to set and check if the form button is a submit button.
- A submit button is an `input[type=submit]` element.
- Otherwise, the button is a `button` element.
-
-
-## Form Container
-
-A form container object represents a container for other form containers or form field directly.
-Every form container has to implement the `IFormContainer` interface which requires the following method:
-
-- `loadValues(array $data, IStorableObject $object)` is called by `IFormDocument::loadValuesFromObject()` to inform the container that object data is loaded.
- This method is *not* intended to generally call `IFormField::loadValues()` on its form field children as these methods are already called by `IFormDocument::loadValuesFromObject()`.
- This method is intended for specialized form containers with more complex logic.
-
-There are multiple default container implementations:
-
-1. `FormContainer` is the default implementation of `IFormContainer`.
-1. `TabMenuFormContainer` represents the container of tab menu, while
-1. `TabFormContainer` represents a tab of a tab menu and
-1. `TabTabMenuFormContainer` represents a tab of a tab menu that itself contains a tab menu.
-1. The children of `RowFormContainer` are shown in a row and should use `col-*` classes.
-1. The children of `RowFormFieldContainer` are also shown in a row but does not show the labels and descriptions of the individual form fields.
- Instead of the individual labels and descriptions, the container's label and description is shown and both span all of fields.
-1. `SuffixFormFieldContainer` can be used for one form field with a second selection form field used as a suffix.
-
-The methods of the interfaces that `FormContainer` is implementing are well documented, but here is a short overview of the most important methods when setting up a form or extending a form with an event listener:
-
-- `appendChild(IFormChildNode $child)`, `appendChildren(array $children)`, and `insertBefore(IFormChildNode $child, $referenceNodeId)` are used to insert new children into the form container.
-- `description($languageItem = null, array $variables = [])` and `label($languageItem = null, array $variables = [])` are used to set the description and the label or title of the form container.
-
-
-## Form Field
-
-A form field object represents a concrete form field that allows entering data.
-Every form field has to implement the `IFormField` interface which extends `IFormChildNode` and `IFormElement`.
-
-`IFormField` requires the following additional methods:
-
-- `addValidationError(IFormFieldValidationError $error)` and `getValidationErrors()` can be used to get and set validation errors of the form field (see [form validation](php_api_form_builder-validation_data.html#form-validation)).
-- `addValidator(IFormFieldValidator $validator)`, `getValidators()`, `removeValidator($validatorId)`, and `hasValidator($validatorId)` can be used to get, set, remove, and check for validators for the form field (see [form validation](php_api_form_builder-validation_data.html#form-validation)).
-- `getFieldHtml()` returns the field's HTML output without the surrounding `dl` structure.
-- `objectProperty($objectProperty)` and `getObjectProperty()` can be used to get and set the object property that the field represents.
- When setting the object property is set to an empty string, the previously set object property is unset.
- If no object property has been set, the field’s (non-prefixed) id is returned.
-
- The object property allows having different fields (requiring different ids) that represent the same object property which is handy when available options of the field’s value depend on another field.
- Having object property allows to define different fields for each value of the other field and to use form field dependencies to only show the appropriate field.
-- `readValue()` reads the form field value from the request data after the form is submitted.
-- `required($required = true)` and `isRequired()` can be used to determine if the form field has to be filled out.
- By default, form fields do not have to be filled out.
-- `value($value)` and `getSaveValue()` can be used to get and set the value of the form field to be used outside of the context of forms.
- `getValue()`, in contrast, returns the internal representation of the form field’s value.
- In general, the internal representation is only relevant when validating the value in additional validators.
- `loadValue(array $data, IStorableObject $object)` extracts the form field value from the given data array (and additional, non-editable data from the object if the field needs them).
-
-`AbstractFormField` provides default implementations of many of the listed methods above and should be extended instead of implementing `IFormField` directly.
-
-An overview of the form fields provided by default can be found [here](php_api_form_builder-form_fields.html).
-
-
-### Form Field Interfaces and Traits
-
-WoltLab Suite Core provides a variety of interfaces and matching traits with default implementations for several common features of form fields:
-
-
-#### `IAutoFocusFormField` / `TAutoFocusFormField`
-
-`IAutoFocusFormField` has to be implemented by form fields that can be auto-focused.
-The implementing class has to implement the methods `autoFocus($autoFocus = true)` and `isAutoFocused()`.
-By default, form fields are not auto-focused.
-`TAutoFocusFormField` provides a default implementation of these two methods.
-
-
-#### `IFileFormField`
-
-`IFileFormField` has to be implemented by every form field that uploads files so that the `enctype` attribute of the form document is `multipart/form-data` (see `IFormDocument::getEnctype()`).
-
-
-#### `IFilterableSelectionFormField` / `TFilterableSelectionFormField`
-
-`IFilterableSelectionFormField` extends `ISelectionFormField` by the possibilty for users when selecting the value(s) to filter the list of available options.
-The implementing class has to implement the methods `filterable($filterable = true)` and `isFilterable()`.
-`TFilterableSelectionFormField` provides a default implementation of these two methods.
-
-
-#### `II18nFormField` / `TI18nFormField`
-
-`II18nFormField` has to be implemented by form fields if the form field value can be entered separately for all available languages.
-The implementing class has to implement the following methods:
-
-- `i18n($i18n = true)` and `isI18n()` can be used to set whether a specific instance of the class actually supports multilingual input.
-- `i18nRequired($i18nRequired = true)` and `isI18nRequired()` can be used to set whether a specific instance of the class requires separate values for all languages.
-- `languageItemPattern($pattern)` and `getLanguageItemPattern()` can be used to set the pattern/regular expression for the language item used to save the multilingual values.
-- `hasI18nValues()` and `hasPlainValue()` check if the current value is a multilingual or monolingual value.
-
-`TI18nFormField` provides a default implementation of these eight methods and additional default implementations of some of the `IFormField` methods.
-If multilingual input is enabled for a specific form field, classes using `TI18nFormField` register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the array with multilingual input into the `$parameters` array directly using `{$objectProperty}_i18n` as the array key.
-If multilingual input is enabled but only a monolingual value is entered, the custom form field data processor does nothing and the form field’s value is added by the `DefaultFormDataProcessor` into the `data` sub-array of the `$parameters` array.
-
-{% include callout.html content="`TI18nFormField` already provides a default implementation of `IFormField::validate()`." type="info" %}
-
-
-#### `IImmutableFormField` / `TImmutableFormField`
-
-`IImmutableFormField` has to be implemented by form fields that support being displayed but whose value cannot be changed.
-The implementing class has to implement the methods `immutable($immutable = true)` and `isImmutable()` that can be used to determine if the value of the form field is mutable or immutable.
-By default, form field are mutable.
-
-
-#### `IMaximumFormField` / `TMaximumFormField`
-
-`IMaximumFormField` has to be implemented by form fields if the entered value must have a maximum value.
-The implementing class has to implement the methods `maximum($maximum = null)` and `getMaximum()`.
-A maximum of `null` signals that no maximum value has been set.
-`TMaximumFormField` provides a default implementation of these two methods.
-
-{% include callout.html content="The implementing class has to validate the entered value against the maximum value manually." type="warning" %}
-
-
-#### `IMaximumLengthFormField` / `TMaximumLengthFormField`
-
-`IMaximumLengthFormField` has to be implemented by form fields if the entered value must have a maximum length.
-The implementing class has to implement the methods `maximumLength($maximumLength = null)`, `getMaximumLength()`, and `validateMaximumLength($text, Language $language = null)`.
-A maximum length of `null` signals that no maximum length has been set.
-`TMaximumLengthFormField` provides a default implementation of these two methods.
-
-{% include callout.html content="The implementing class has to validate the entered value against the maximum value manually by calling `validateMaximumLength()`." type="warning" %}
-
-
-#### `IMinimumFormField` / `TMinimumFormField`
-
-`IMinimumFormField` has to be implemented by form fields if the entered value must have a minimum value.
-The implementing class has to implement the methods `minimum($minimum = null)` and `getMinimum()`.
-A minimum of `null` signals that no minimum value has been set.
-`TMinimumFormField` provides a default implementation of these three methods.
-
-{% include callout.html content="The implementing class has to validate the entered value against the minimum value manually." type="warning" %}
-
-
-#### `IMinimumLengthFormField` / `TMinimumLengthFormField`
-
-`IMinimumLengthFormField` has to be implemented by form fields if the entered value must have a minimum length.
-The implementing class has to implement the methods `minimumLength($minimumLength = null)`, `getMinimumLength()`, and `validateMinimumLength($text, Language $language = null)`.
-A minimum length of `null` signals that no minimum length has been set.
-`TMinimumLengthFormField` provides a default implementation of these three methods.
-
-{% include callout.html content="The implementing class has to validate the entered value against the minimum value manually by calling `validateMinimumLength()`." type="warning" %}
-
-
-#### `IMultipleFormField` / `TMultipleFormField`
-
-`IMinimumLengthFormField` has to be implemented by form fields that support selecting or setting multiple values.
-The implementing class has to implement the following methods:
-
-- `multiple($multiple = true)` and `allowsMultiple()` can be used to set whether a specific instance of the class actually should support multiple values.
- By default, multiple values are not supported.
-- `minimumMultiples($minimum)` and `getMinimumMultiples()` can be used to set the minimum number of values that have to be selected/entered.
- By default, there is no required minimum number of values.
-- `maximumMultiples($minimum)` and `getMaximumMultiples()` can be used to set the maximum number of values that have to be selected/entered.
- By default, there is no maximum number of values.
- `IMultipleFormField::NO_MAXIMUM_MULTIPLES` is returned if no maximum number of values has been set and it can also be used to unset a previously set maximum number of values.
-
-`TMultipleFormField` provides a default implementation of these six methods and classes using `TMultipleFormField` register a [custom form field data processor](php_api_form_builder-validation_data.html#customformfielddataprocessor) to add the `HtmlInputProcessor` object with the text into the `$parameters` array directly using `{$objectProperty}_htmlInputProcessor` as the array key.
-
-{% include callout.html content="The implementing class has to validate the values against the minimum and maximum number of values manually." type="warning" %}
-
-
-#### `INullableFormField` / `TNullableFormField`
-
-`INullableFormField` has to be implemented by form fields that support `null` as their (empty) value.
-The implementing class has to implement the methods `nullable($nullable = true)` and `isNullable()`.
-`TNullableFormField` provides a default implementation of these two methods.
-
-`null` should be returned by `IFormField::getSaveValue()` is the field is considered empty and the form field has been set as nullable.
-
-
-#### `IPackagesFormField` / `TPackagesFormField`
-
-`IPackagesFormField` has to be implemented by form fields that, in some way, considers packages whose ids may be passed to the field object.
-The implementing class has to implement the methods `packageIDs(array $packageIDs)` and `getPackageIDs()`.
-`TPackagesFormField` provides a default implementation of these two methods.
-
-
-#### `IPlaceholderFormField` / `TPlaceholderFormField`
-
-`IPlaceholderFormField` has to be implemented by form fields that support a placeholder value for empty fields.
-The implementing class has to implement the methods `placeholder($languageItem = null, array $variables = [])` and `getPlaceholder()`.
-`TPlaceholderFormField` provides a default implementation of these two methods.
-
-
-#### `ISelectionFormField` / `TSelectionFormField`
-
-`ISelectionFormField` has to be implemented by form fields with a predefined set of possible values.
-The implementing class has to implement the getter and setter methods `options($options, $nestedOptions = false, $labelLanguageItems = true)` and `getOptions()` and additionally two methods related to nesting, i.e. whether the selectable options have a hierarchy:
-`supportsNestedOptions()` and `getNestedOptions()`.
-`TSelectionFormField` provides a default implementation of these four methods.
-
-
-#### `ISuffixedFormField` / `TSuffixedFormField`
-
-`ISuffixedFormField` has to be implemented by form fields that support supports displaying a suffix behind the actual input field.
-The implementing class has to implement the methods `suffix($languageItem = null, array $variables = [])` and `getSuffix()`.
-`TSuffixedFormField` provides a default implementation of these two methods.
-
-
-#### `TDefaultIdFormField`
-
-Form fields that have a default id have to use `TDefaultIdFormField` and have to implement the method `getDefaultId()`.
-
-
-## Displaying Forms
-
-The only thing to do in a template to display the **whole** form including all of the necessary JavaScript is to put
-
-```smarty
-{@$form->getHtml()}
-```
-
-into the template file at the relevant position.
+++ /dev/null
----
-title: Form Validation and Form Data
-sidebar: sidebar
-permalink: php_api_form_builder-validation_data.html
-folder: php/api/formBuilder
-parent: php_api_form_builder
----
-
-## Form Validation
-
-Every form field class has to implement `IFormField::validate()` according to their internal logic of what constitutes a valid value.
-If a certain constraint for the value is no met, a form field validation error object is added to the form field.
-Form field validation error classes have to implement the interface `IFormFieldValidationError`.
-
-In addition to intrinsic validations like checking the length of the value of a text form field, in many cases, there are additional constraints specific to the form like ensuring that the text is not already used by a different object of the same database object class.
-Such additional validations can be added to (and removed from) the form field via implementations of the `IFormFieldValidator` interface.
-
-
-### `IFormFieldValidationError` / `FormFieldValidationError`
-
-`IFormFieldValidationError` requires the following methods:
-
-- `__construct($type, $languageItem = null, array $information = [])` creates a new validation error object for an error with the given type and message stored in the given language items.
- The information array is used when generating the error message.
-- `getHtml()` returns the HTML element representing the error that is shown to the user.
-- `getMessage()` returns the error message based on the language item and information array given in the constructor.
-- `getInformation()` and `getType()` are getters for the first and third parameter of the constructor.
-
-`FormFieldValidationError` is a default implementation of the interface that shows the error in an `small.innerError` HTML element below the form field.
-
-Form field validation errors are added to form fields via the `IFormField::addValidationError(IFormFieldValidationError $error)` method.
-
-
-### `IFormFieldValidator` / `FormFieldValidator`
-
-`IFormFieldValidator` requires the following methods:
-
-- `__construct($id, callable $validator)` creates a new validator with the given id that passes the validated form field to the given callable that does the actual validation.
- `static validateId($id)` is used to check if the given id is valid.
-- `__invoke(IFormField $field)` is used when the form field is validated to execute the validator.
-- `getId()` returns the id of the validator.
-
-`FormFieldValidator` is a default implementation of the interface.
-
-Form field validators are added to form fields via the `addValidator(IFormFieldValidator $validator)` method.
-
-
-## Form Data
-
-After a form is successfully validated, the data of the form fields (returned by `IFormDocument::getData()`) have to be extracted which is the job of the `IFormDataHandler` object returned by `IFormDocument::getDataHandler()`.
-Form data handlers themselves, however, are only iterating through all `IFormDataProcessor` instances that have been registered with the data handler.
-
-
-### `IFormDataHandler` / `FormDataHandler`
-
-`IFormDataHandler` requires the following methods:
-
-- `addProcessor(IFormDataProcessor $processor)` adds a new data processor to the data handler.
-- `getFormData(IFormDocument $document)` returns the data of the given form by applying all registered data handlers on the form.
-- `getObjectData(IFormDocument $document, IStorableObject $object)` returns the data of the given object which will be used to populate the form field values of the given form.
-
-`FormDataHandler` is the default implementation of this interface and should also be extended instead of implementing the interface directly.
-
-
-### `IFormDataProcessor` / `DefaultFormDataProcessor`
-
-`IFormDataProcessor` requires the following methods:
-
-- `processFormData(IFormDocument $document, array $parameters)` is called by `IFormDataHandler::getFormData()`.
- The method processes the given parameters array and returns the processed version.
-- `processObjectData(IFormDocument $document, array $data, IStorableObject $object)` is called by `IFormDataHandler::getObjectData()`.
- The method processes the given object data array and returns the processed version.
-
-When `FormDocument` creates its `FormDataHandler` instance, it automatically registers an `DefaultFormDataProcessor` object as the first data processor.
-`DefaultFormDataProcessor` puts the save value of all form fields that are available and have a save value into `$parameters['data']` using the form field’s object property as the array key.
-
-{% include callout.html content="`IFormDataProcessor` should not be implemented directly. Instead, `AbstractFormDataProcessor` should be extended." type="warning" %}
-
-{% include callout.html content="All form data is put into the `data` sub-array so that the whole `$parameters` array can be passed to a database object action object that requires the actual database object data to be in the `data` sub-array." type="info" %}
-
-
-
-### Additional Data Processors
-
-#### `CustomFormDataProcessor`
-
-As mentioned above, the data in the `data` sub-array is intended to directly create or update the database object with.
-As these values are used in the database query directly, these values cannot contain arrays.
-Several form fields, however, store and return their data in form of arrays.
-Thus, this data cannot be returned by `IFormField::getSaveValue()` so that `IFormField::hasSaveValue()` returns `false` and the form field’s data is not collected by the standard `DefaultFormDataProcessor` object.
-
-Instead, such form fields register a `CustomFormDataProcessor` in their `IFormField::populate()` method that inserts the form field value into the `$parameters` array directly.
-This way, the relevant database object action method has access to the data to save it appropriately.
-
-The constructor of `CustomFormDataProcessor` requires an id (that is primarily used in error messages during the validation of the second parameter) and callables for `IFormDataProcessor::processFormData()` and `IFormDataProcessor::processObjectData()` which are passed the same parameters as the `IFormDataProcessor` methods.
-Only one of the callables has to be given, the other one then defaults to simply returning the relevant array unchanged.
-
-
-#### `VoidFormDataProcessor`
-
-Some form fields might only exist to toggle the visibility of other form fields (via dependencies) but the data of form field itself is irrelevant.
-As `DefaultFormDataProcessor` collects the data of all form fields, an additional data processor in the form of a `VoidFormDataProcessor` can be added whose constructor `__construct($property, $isDataProperty = true)` requires the name of the relevant object property/form id and whether the form field value is stored in the `data` sub-array or directory in the `$parameters` array.
-When the data processor is invoked, it checks whether the relevant entry in the `$parameters` array exists and voids it by removing it from the array.
+++ /dev/null
----
-title: Caches
-sidebar: sidebar
-permalink: php_api_caches.html
-folder: php/api
----
-
-WoltLab Suite offers two distinct types of caches:
-
-1. [Persistent caches](php_api_caches_persistent-caches.html) created by cache builders whose data can be stored using different cache sources.
-2. [Runtime caches](php_api_caches_runtime-caches.html) store objects for the duration of a single request.
-
-## Understanding Caching
-
-Every so often, plugins make use of cache builders or runtime caches to store
-their data, even if there is absolutely no need for them to do so. Usually, this
-involves a strong opinion about the total number of SQL queries on a page,
-including but not limited to some magic treshold numbers, which should not be
-exceeded for "performance reasons".
-
-This misconception can easily lead into thinking that SQL queries should be
-avoided or at least written to a cache, so that it doesn't need to be executed
-so often. Unfortunately, this completely ignores the fact that both a single
-query can take down your app (e. g. full table scan on millions of rows), but
-10 queries using a primary key on a table with a few hundred rows will not slow
-down your page.
-
-There are some queries that should go into caches by design, but most of the
-cache builders weren't initially there, but instead have been added because
-they were required to reduce the load _significantly_. You need to understand
-that caches always come at a cost, even a runtime cache does! In particular,
-they will always consume memory that is not released over the duration of the
-request lifecycle and potentially even leak memory by holding references to
-objects and data structures that are no longer required.
-
-Caching should always be a solution for a problem.
-
-### When to Use a Cache
-
-It's difficult to provide a definite answer or checklist when to use a cache
-and why it is required at this point, because the answer is: It depends. The
-permission cache for user groups is a good example for a valid cache, where
-we can achieve significant performance improvement compared to processing this
-data on every request.
-
-Its caches are build for each permutation of user group memberships that are
-encountered for a page request. Building this data is an expensive process that
-involves both inheritance and specific rules in regards to when a value for a
-permission overrules another value. The added benefit of this cache is that one
-cache usually serves a large number of users with the same group memberships and
-by computing these permissions once, we can serve many different requests. Also,
-the permissions are rather static values that change very rarely and thus we can
-expect a very high cache lifetime before it gets rebuild.
-
-### When not to Use a Cache
-
-I remember, a few years ago, there was a plugin that displayed a user's character
-from an online video game. The character sheet not only included a list of basic
-statistics, but also displayed the items that this character was wearing and or
-holding at the time.
-
-The data for these items were downloaded in bulk from the game's vendor servers
-and stored in a persistent cache file that periodically gets renewed. There is
-nothing wrong with the idea of caching the data on your own server rather than
-requesting them everytime from the vendor's servers - not only because they
-imposed a limit on the number of requests per hour.
-
-Unfortunately, the character sheet had a sub-par performance and the users were
-upset by the significant loading times compared to literally every other page
-on the same server. The author of the plugin was working hard to resolve this
-issue and was evaluating all kind of methods to improve the page performance,
-including deep-diving into the realm of micro-optimizations to squeeze out every
-last bit of performance that is possible.
-
-The real problem was the cache file itself, it turns out that it was holding the
-data for several thousand items with a total file size of about 13 megabytes.
-It doesn't look that much at first glance, after all this isn't the '90s anymore,
-but unserializing a 13 megabyte array is really slow and looking up items in such
-a large array isn't exactly fast either.
-
-The solution was rather simple, the data that was fetched from the vendor's API
-was instead written into a separate database table. Next, the persistent cache
-was removed and the character sheet would now request the item data for that
-specific character straight from the database. Previously, the character sheet
-took several seconds to load and after the change it was done in a fraction of
-a second. Although quite extreme, this illustrates a situation where the cache
-file was introduced in the design process, without evaluating if the cache -
-at least how it was implemented - was really necessary.
-
-Caching should always be a solution for a problem. Not the other way around.
-
-{% include links.html %}
+++ /dev/null
----
-title: Comments
-sidebar: sidebar
-permalink: php_api_comments.html
-folder: php/api
----
-
-## User Group Options
-
-You need to create the following permissions:
-
-| user group type | permission type | naming |
-| --------------- | --------------- | ------ |
-| user | creating comments | `user.foo.canAddComment` |
-| user | editing own comments | `user.foo.canEditComment` |
-| user | deleting own comments | `user.foo.canDeleteComment` |
-| moderator | moderating comments | `mod.foo.canModerateComment` |
-| moderator | editing comments | `mod.foo.canEditComment` |
-| moderator | deleting comments | `mod.foo.canDeleteComment` |
-
-Within their respective user group option category, the options should be listed in the same order as in the table above.
-
-
-### Language Items
-
-#### User Group Options
-
-The language items for the comment-related user group options generally have the same values:
-
-- `wcf.acp.group.option.user.foo.canAddComment`
-
- German: `Kann Kommentare erstellen`
-
- English: `Can create comments`
-
-- `wcf.acp.group.option.user.foo.canEditComment`
-
- German: `Kann eigene Kommentare bearbeiten`
-
- English: `Can edit their comments`
-
-- `wcf.acp.group.option.user.foo.canDeleteComment`
-
- German: `Kann eigene Kommentare löschen`
-
- English: `Can delete their comments`
-
-- `wcf.acp.group.option.mod.foo.canModerateComment`
-
- German: `Kann Kommentare moderieren`
-
- English: `Can moderate comments`
-
-- `wcf.acp.group.option.mod.foo.canEditComment`
-
- German: `Kann Kommentare bearbeiten`
-
- English: `Can edit comments`
-
-- `wcf.acp.group.option.mod.foo.canDeleteComment`
-
- German: `Kann Kommentare löschen`
-
- English: `Can delete comments`
+++ /dev/null
----
-title: Cronjobs
-sidebar: sidebar
-permalink: php_api_cronjobs.html
-folder: php/api
----
-
-Cronjobs offer an easy way to execute actions periodically, like cleaning up the database.
-
-{% include callout.html content="The execution of cronjobs is not guaranteed but requires someone to access the page with JavaScript enabled." type="warning" %}
-
-This page focuses on the technical aspects of cronjobs, [the cronjob package installation plugin page](package_pip_cronjob.html) covers how you can actually register a cronjob.
-
-
-## Example
-
-```php
-<?php
-namespace wcf\system\cronjob;
-use wcf\data\cronjob\Cronjob;
-use wcf\system\WCF;
-
-/**
- * Updates the last activity timestamp in the user table.
- *
- * @author Marcel Werk
- * @copyright 2001-2016 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\System\Cronjob
- */
-class LastActivityCronjob extends AbstractCronjob {
- /**
- * @inheritDoc
- */
- public function execute(Cronjob $cronjob) {
- parent::execute($cronjob);
-
- $sql = "UPDATE wcf".WCF_N."_user user_table,
- wcf".WCF_N."_session session
- SET user_table.lastActivityTime = session.lastActivityTime
- WHERE user_table.userID = session.userID
- AND session.userID <> 0";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute();
- }
-}
-
-```
-
-
-## `ICronjob` Interface
-
-Every cronjob needs to implement the `wcf\system\cronjob\ICronjob` interface which requires the `execute(Cronjob $cronjob)` method to be implemented.
-This method is called by [wcf\system\cronjob\CronjobScheduler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cronjob/CronjobScheduler.class.php) when executing the cronjobs.
-
-In practice, however, you should extend the `AbstractCronjob` class and also call the `AbstractCronjob::execute()` method as it fires an event which makes cronjobs extendable by plugins (see [event documentation](php_event.html)).
-
-
-## Executing Cronjobs Through CLI
-
-Cronjobs can be executed through the command-line interface (CLI):
-
-```
-php /path/to/wcf/cli.php << 'EOT'
-USERNAME
-PASSWORD
-cronjob execute
-EOT
-```
\ No newline at end of file
+++ /dev/null
----
-title: Event List
-sidebar: sidebar
-permalink: php_api_event_list.html
-folder: php
-parent: php_api_events
----
-
-Events whose name is marked with an asterisk are called from a static method and thus do not provide any object, just the class name.
-
-## WoltLab Suite Core
-
-| Class | Event Name |
-|-------|------------|
-| `wcf\acp\action\UserExportGdprAction` | `export` |
-| `wcf\acp\form\StyleAddForm` | `setVariables` |
-| `wcf\acp\form\UserSearchForm` | `search` |
-| `wcf\action\AbstractAction` | `checkModules` |
-| `wcf\action\AbstractAction` | `checkPermissions` |
-| `wcf\action\AbstractAction` | `execute` |
-| `wcf\action\AbstractAction` | `executed` |
-| `wcf\action\AbstractAction` | `readParameters` |
-| `wcf\data\attachment\AttachmentAction` | `generateThumbnail` |
-| `wcf\data\session\SessionAction` | `keepAlive` |
-| `wcf\data\session\SessionAction` | `poll` |
-| `wcf\data\trophy\Trophy` | `renderTrophy` |
-| `wcf\data\user\online\UserOnline` | `getBrowser` |
-| `wcf\data\user\online\UserOnlineList` | `isVisible` |
-| `wcf\data\user\trophy\UserTrophy` | `getReplacements` |
-| `wcf\data\user\UserAction` | `beforeFindUsers` |
-| `wcf\data\user\UserAction` | `rename` |
-| `wcf\data\user\UserProfile` | `getAvatar` |
-| `wcf\data\user\UserProfile` | `isAccessible` |
-| `wcf\data\AbstractDatabaseObjectAction` | `finalizeAction` |
-| `wcf\data\AbstractDatabaseObjectAction` | `initializeAction` |
-| `wcf\data\AbstractDatabaseObjectAction` | `validateAction` |
-| `wcf\data\DatabaseObjectList` | `init` |
-| `wcf\form\AbstractForm` | `readFormParameters` |
-| `wcf\form\AbstractForm` | `save` |
-| `wcf\form\AbstractForm` | `saved` |
-| `wcf\form\AbstractForm` | `submit` |
-| `wcf\form\AbstractForm` | `validate` |
-| `wcf\form\AbstractModerationForm` | `prepareSave` |
-| `wcf\page\AbstractPage` | `assignVariables` |
-| `wcf\page\AbstractPage` | `checkModules` |
-| `wcf\page\AbstractPage` | `checkPermissions` |
-| `wcf\page\AbstractPage` | `readData` |
-| `wcf\page\AbstractPage` | `readParameters` |
-| `wcf\page\AbstractPage` | `show` |
-| `wcf\page\MultipleLinkPage` | `beforeReadObjects` |
-| `wcf\page\MultipleLinkPage` | `calculateNumberOfPages` |
-| `wcf\page\MultipleLinkPage` | `countItems` |
-| `wcf\page\SortablePage` | `validateSortField` |
-| `wcf\page\SortablePage` | `validateSortOrder` |
-| `wcf\system\bbcode\MessageParser` | `afterParsing` |
-| `wcf\system\bbcode\MessageParser` | `beforeParsing` |
-| `wcf\system\bbcode\SimpleMessageParser` | `afterParsing` |
-| `wcf\system\bbcode\SimpleMessageParser` | `beforeParsing` |
-| `wcf\system\box\AbstractBoxController` | `__construct` |
-| `wcf\system\box\AbstractBoxController` | `afterLoadContent` |
-| `wcf\system\box\AbstractBoxController` | `beforeLoadContent` |
-| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `afterLoadContent` |
-| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `beforeLoadContent` |
-| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `hasContent` |
-| `wcf\system\box\AbstractDatabaseObjectListBoxController` | `readObjects` |
-| `wcf\system\cronjob\AbstractCronjob` | `execute` |
-| `wcf\system\email\Email` | `getJobs` |
-| `wcf\system\form\builder\container\wysiwyg\WysiwygFormContainer` | `populate` |
-| `wcf\system\html\input\filter\MessageHtmlInputFilter` | `setAttributeDefinitions` |
-| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `afterProcess` |
-| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `beforeEmbeddedProcess` |
-| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `beforeProcess` |
-| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `convertPlainLinks` |
-| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `getTextContent` |
-| `wcf\system\html\input\node\HtmlInputNodeProcessor` | `parseEmbeddedContent` |
-| `wcf\system\html\input\node\HtmlInputNodeWoltlabMetacodeMarker` | `filterGroups` |
-| `wcf\system\html\output\node\HtmlOutputNodePre` | `selectHighlighter` |
-| `wcf\system\html\output\node\HtmlOutputNodeProcessor` | `beforeProcess` |
-| `wcf\system\image\adapter\ImagickImageAdapter` | `getResizeFilter` |
-| `wcf\system\menu\user\profile\UserProfileMenu` | `init` |
-| `wcf\system\menu\user\profile\UserProfileMenu` | `loadCache` |
-| `wcf\system\menu\TreeMenu` | `init` |
-| `wcf\system\menu\TreeMenu` | `loadCache` |
-| `wcf\system\message\QuickReplyManager` | `addFullQuote` |
-| `wcf\system\message\QuickReplyManager` | `allowedDataParameters` |
-| `wcf\system\message\QuickReplyManager` | `beforeRenderQuote` |
-| `wcf\system\message\QuickReplyManager` | `createMessage` |
-| `wcf\system\message\QuickReplyManager` | `createdMessage` |
-| `wcf\system\message\QuickReplyManager` | `getMessage` |
-| `wcf\system\message\QuickReplyManager` | `validateParameters` |
-| `wcf\system\option\OptionHandler` | `afterReadCache` |
-| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `construct` |
-| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `hasUninstall` |
-| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `install` |
-| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `uninstall` |
-| `wcf\system\package\plugin\AbstractPackageInstallationPlugin` | `update` |
-| `wcf\system\package\plugin\ObjectTypePackageInstallationPlugin` | `addConditionFields` |
-| `wcf\system\package\PackageInstallationDispatcher` | `postInstall` |
-| `wcf\system\package\PackageUninstallationDispatcher` | `postUninstall` |
-| `wcf\system\reaction\ReactionHandler` | `getDataAttributes` |
-| `wcf\system\request\RouteHandler` | `didInit` |
-| `wcf\system\session\ACPSessionFactory` | `afterInit` |
-| `wcf\system\session\ACPSessionFactory` | `beforeInit` |
-| `wcf\system\session\SessionHandler` | `afterChangeUser` |
-| `wcf\system\session\SessionHandler` | `beforeChangeUser` |
-| `wcf\system\style\StyleCompiler` | `compile` |
-| `wcf\system\template\TemplateEngine` | `afterDisplay` |
-| `wcf\system\template\TemplateEngine` | `beforeDisplay` |
-| `wcf\system\upload\DefaultUploadFileSaveStrategy` | `generateThumbnails` |
-| `wcf\system\upload\DefaultUploadFileSaveStrategy` | `save` |
-| `wcf\system\user\authentication\UserAuthenticationFactory` | `init` |
-| `wcf\system\user\notification\UserNotificationHandler` | `createdNotification` |
-| `wcf\system\user\notification\UserNotificationHandler` | `fireEvent` |
-| `wcf\system\user\notification\UserNotificationHandler` | `markAsConfirmed` |
-| `wcf\system\user\notification\UserNotificationHandler` | `markAsConfirmedByIDs` |
-| `wcf\system\user\notification\UserNotificationHandler` | `removeNotifications` |
-| `wcf\system\user\notification\UserNotificationHandler` | `updateTriggerCount` |
-| `wcf\system\user\UserBirthdayCache` | `loadMonth` |
-| `wcf\system\worker\AbstractRebuildDataWorker` | `execute` |
-| `wcf\system\CLIWCF` | `afterArgumentParsing` |
-| `wcf\system\CLIWCF` | `beforeArgumentParsing` |
-| `wcf\system\WCF` | `initialized` |
-| `wcf\util\HeaderUtil` | `parseOutput`*|
-
-## WoltLab Suite Forum
-
-| Class | Event Name |
-|-------|------------|
-| `wbb\data\board\BoardAction` | `cloneBoard` |
-| `wbb\data\post\PostAction` | `quickReplyShouldMerge` |
-| `wbb\system\thread\ThreadHandler` | `didInit` |
+++ /dev/null
----
-title: Events
-sidebar: sidebar
-permalink: php_api_events.html
-folder: php/api
----
-
-WoltLab Suite's event system allows manipulation of program flows and data without having to change any of the original source code.
-At many locations throughout the PHP code of WoltLab Suite Core and mainly through inheritance also in the applications and plugins, so called *events* are fired which trigger registered *event listeners* that get access to the object firing the event (or at least the class name if the event has been fired in a static method).
-
-This page focuses on the technical aspects of events and event listeners, [the eventListener package installation plugin page](package_pip_event-listener.html) covers how you can actually register an event listener.
-A comprehensive list of all available events is provided [here](php_api_event_list.html).
-
-
-## Introductory Example
-
-Let's start with a simple example to illustrate how the event system works.
-Consider this pre-existing class:
-
-```php
-<?php
-namespace wcf\system\example;
-use wcf\system\event\EventHandler;
-
-class ExampleComponent {
- public $var = 1;
-
- public function getVar() {
- EventHandler::getInstance()->fireAction($this, 'getVar');
-
- return $this->var;
- }
-}
-```
-
-where an event with event name `getVar` is fired in the `getVar()` method.
-
-If you create an object of this class and call the `getVar()` method, the return value will be `1`, of course:
-
-```php
-<?php
-
-$example = new wcf\system\example\ExampleComponent();
-if ($example->getVar() == 1) {
- echo "var is 1!";
-}
-else if ($example->getVar() == 2) {
- echo "var is 2!";
-}
-else {
- echo "No, var is neither 1 nor 2.";
-}
-
-// output: var is 1!
-```
-
-Now, consider that we have registered the following event listener to this event:
-
-```php
-<?php
-namespace wcf\system\event\listener;
-
-class ExampleEventListener implements IParameterizedEventListener {
- public function execute($eventObj, $className, $eventName, array &$parameters) {
- $eventObj->var = 2;
- }
-}
-
-```
-
-Whenever the event in the `getVar()` method is called, this method (of the same event listener object) is called.
-In this case, the value of the method's first parameter is the `ExampleComponent` object passed as the first argument of the `EventHandler::fireAction()` call in `ExampleComponent::getVar()`.
-As `ExampleComponent::$var` is a public property, the event listener code can change it and set it to `2`.
-
-If you now execute the example code from above again, the output will change from `var is 1!` to `var is 2!` because prior to returning the value, the event listener code changes the value from `1` to `2`.
-
-This introductory example illustrates how event listeners can change data in a non-intrusive way.
-Program flow can be changed, for example, by throwing a `wcf\system\exception\PermissionDeniedException` if some additional constraint to access a page is not fulfilled.
-
-
-## Listening to Events
-
-In order to listen to events, you need to register the event listener and the event listener itself needs to implement the interface `wcf\system\event\listener\IParameterizedEventListener` which only contains the `execute` method (see example above).
-
-The first parameter `$eventObj` of the method contains the passed object where the event is fired or the name of the class in which the event is fired if it is fired from a static method.
-The second parameter `$className` always contains the name of the class where the event has been fired.
-The third parameter `$eventName` provides the name of the event within a class to uniquely identify the exact location in the class where the event has been fired.
-The last parameter `$parameters` is a reference to the array which contains additional data passed by the method firing the event.
-If no additional data is passed, `$parameters` is empty.
-
-
-## Firing Events
-
-If you write code and want plugins to have access at certain points, you can fire an event on your own.
-The only thing to do is to call the `wcf\system\event\EventHandler::fireAction($eventObj, $eventName, array &$parameters = [])` method and pass the following parameters:
-
-1. `$eventObj` should be `$this` if you fire from an object context, otherwise pass the class name `static::class`.
-2. `$eventName` identifies the event within the class and generally has the same name as the method.
- In cases, were you might fire more than one event in a method, for example before and after a certain piece of code, you can use the prefixes `before*` and `after*` in your event names.
-3. `$parameters` is an optional array which allows you to pass additional data to the event listeners without having to make this data accessible via a property explicitly only created for this purpose.
- This additional data can either be just additional information for the event listeners about the context of the method call or allow the event listener to manipulate local data if the code, where the event has been fired, uses the passed data afterwards.
-
-### Example: Using `$parameters` argument
-
-Consider the following method which gets some text that the methods parses.
-
-```php
-<?php
-namespace wcf\system\example;
-use wcf\system\event\EventHandler;
-
-class ExampleParser {
- public function parse($text) {
- // [some parsing done by default]
-
- $parameters = ['text' => $text];
- EventHandler::getInstance()->fireAction($this, 'parse', $parameters);
-
- return $parameters['text'];
- }
-}
-```
-
-After the default parsing by the method itself, the author wants to enable plugins to do additional parsing and thus fires an event and passes the parsed text as an additional parameter.
-Then, a plugin can deliver the following event listener
-
-```php
-<?php
-namespace wcf\system\event\listener;
-
-class ExampleParserEventListener implements IParameterizedEventListener {
- public function execute($eventObj, $className, $eventName, array &$parameters) {
- $text = $parameters['text'];
-
- // [some additional parsing which changes $text]
-
- $parameters['text'] = $text;
- }
-}
-```
-
-which can access the text via `$parameters['text']`.
-
-This example can also be perfectly used to illustrate how to name multiple events in the same method.
-Let's assume that the author wants to enable plugins to change the text before and after the method does its own parsing and thus fires two events:
-
-```php
-<?php
-namespace wcf\system\example;
-use wcf\system\event\EventHandler;
-
-class ExampleParser {
- public function parse($text) {
- $parameters = ['text' => $text];
- EventHandler::getInstance()->fireAction($this, 'beforeParsing', $parameters);
- $text = $parameters['text'];
-
- // [some parsing done by default]
-
- $parameters = ['text' => $text];
- EventHandler::getInstance()->fireAction($this, 'afterParsing', $parameters);
-
- return $parameters['text'];
- }
-}
-```
-
-
-## Advanced Example: Additional Form Field
-
-One common reason to use event listeners is to add an additional field to a pre-existing form (in combination with template listeners, which we will not cover here).
-We will assume that users are able to do both, create and edit the objects via this form.
-The points in the program flow of [AbstractForm](php_pages.html#abstractform) that are relevant here are:
-
-- adding object (after the form has been submitted):
- 1. reading the value of the field
- 2. validating the read value
- 3. saving the additional value after successful validation and resetting locally stored value or assigning the current value of the field to the template after unsuccessful validation
-
-- editing object:
- - on initial form request:
- 1. reading the pre-existing value of the edited object
- 2. assigning the field value to the template
- - after the form has been submitted:
- 1. reading the value of the field
- 2. validating the read value
- 3. saving the additional value after successful validation
- 4. assigning the current value of the field to the template
-
-All of these cases can be covered the by following code in which we assume that `wcf\form\ExampleAddForm` is the form to create example objects and that `wcf\form\ExampleEditForm` extends `wcf\form\ExampleAddForm` and is used for editing existing example objects.
-
-```php
-<?php
-namespace wcf\system\event\listener;
-use wcf\form\ExampleAddForm;
-use wcf\form\ExampleEditForm;
-use wcf\system\exception\UserInputException;
-use wcf\system\WCF;
-
-class ExampleAddFormListener implements IParameterizedEventListener {
- protected $var = 0;
-
- public function execute($eventObj, $className, $eventName, array &$parameters) {
- $this->$eventName($eventObj);
- }
-
- protected function assignVariables() {
- WCF::getTPL()->assign('var', $this->var);
- }
-
- protected function readData(ExampleEditForm $eventObj) {
- if (empty($_POST)) {
- $this->var = $eventObj->example->var;
- }
- }
-
- protected function readFormParameters() {
- if (isset($_POST['var'])) $this->var = intval($_POST['var']);
- }
-
- protected function save(ExampleAddForm $eventObj) {
- $eventObj->additionalFields = array_merge($eventObj->additionalFields, ['var' => $this->var]);
- }
-
- protected function saved() {
- $this->var = 0;
- }
-
- protected function validate() {
- if ($this->var < 0) {
- throw new UserInputException('var', 'isNegative');
- }
- }
-}
-```
-
-The `execute` method in this example just delegates the call to a method with the same name as the event so that this class mimics the structure of a form class itself.
-The form object is passed to the methods but is only given in the method signatures as a parameter here whenever the form object is actually used.
-Furthermore, the type-hinting of the parameter illustrates in which contexts the method is actually called which will become clear in the following discussion of the individual methods:
-
-- `assignVariables()` is called for the add and the edit form and simply assigns the current value of the variable to the template.
-- `readData()` reads the pre-existing value of `$var` if the form has not been submitted and thus is only relevant when editing objects which is illustrated by the explicit type-hint of `ExampleEditForm`.
-- `readFormParameters()` reads the value for both, the add and the edit form.
-- `save()` is, of course, also relevant in both cases but requires the form object to store the additional value in the `wcf\form\AbstractForm::$additionalFields` array which can be used if a `var` column has been added to the database table in which the example objects are stored.
-- `saved()` is only called for the add form as it clears the internal value so that in the `assignVariables()` call, the default value will be assigned to the template to create an "empty" form.
- During edits, this current value is the actual value that should be shown.
-- `validate()` also needs to be called in both cases as the input data always has to be validated.
-
-Lastly, the following XML file has to be used to register the event listeners (you can find more information about how to register event listeners on [the eventListener package installation plugin page](package_pip_event-listener.html)):
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/eventListener.xsd">
- <import>
- <eventlistener name="exampleAddInherited">
- <eventclassname>wcf\form\ExampleAddForm</eventclassname>
- <eventname>assignVariables,readFormParameters,save,validate</eventname>
- <listenerclassname>wcf\system\event\listener\ExampleAddFormListener</listenerclassname>
- <inherit>1</inherit>
- </eventlistener>
-
- <eventlistener name="exampleAdd">
- <eventclassname>wcf\form\ExampleAddForm</eventclassname>
- <eventname>saved</eventname>
- <listenerclassname>wcf\system\event\listener\ExampleAddFormListener</listenerclassname>
- </eventlistener>
-
- <eventlistener name="exampleEdit">
- <eventclassname>wcf\form\ExampleEditForm</eventclassname>
- <eventname>readData</eventname>
- <listenerclassname>wcf\system\event\listener\ExampleAddFormListener</listenerclassname>
- </eventlistener>
- </import>
-</data>
-```
\ No newline at end of file
+++ /dev/null
----
-title: Form Builder
-sidebar: sidebar
-permalink: php_api_form_builder.html
-folder: php/api
----
-
-{% include callout.html content="Form builder is only available since WoltLab Suite Core 5.2." type="info" %}
-
-{% include callout.html content="The [migration guide for WoltLab Suite Core 5.2](migration_wsc-31_form-builder.html) provides some examples of how to migrate existing forms to form builder that can also help in understanding form builder if the old way of creating forms is familiar." type="info" %}
-
-
-## Advantages of Form Builder
-
-WoltLab Suite 5.2 introduces a new powerful way of creating forms: form builder.
-Before taking a closer look at form builder, let us recap how forms are created in previous versions:
-In general, for each form field, there is a corresponding property of the form's PHP class whose value has to be read from the request data, validated, and passed to the database object action to store the value in a database table.
-When editing an object, the property's value has to be set using the value of the corresponding property of the edited object.
-In the form's template, you have to write the `<form>` element with all of its children: the `<section>` elements, the `<dl>` elements, and, of course, the form fields themselves.
-In summary, this way of creating forms creates much duplicate or at least very similar code and makes it very time consuming if the structure of forms in general or a specific type of form field has to be changed.
-
-Form builder, in contrast, relies on PHP objects representing each component of the form, from the form itself down to each form field.
-This approach makes creating forms as easy as creating some PHP objects, populating them with all the relevant data, and one line of code in the template to print the form.
-
-
-## Form Builder Components
-
-Form builder consists of several components that are presented on the following pages:
-
-1. [Structure of form builder](php_api_form_builder-structure.html)
-1. [Form validation and form data](php_api_form_builder-validation_data.html)
-1. [Form node dependencies](php_api_form_builder-dependencies.html)
-
-{% include callout.html content="In general, form builder provides default implementation of interfaces by providing either abstract classes or traits.
- It is expected that the interfaces are always implemented using these abstract classes and traits!
- This way, if new methods are added to the interfaces, default implementations can be provided by the abstract classes and traits without causing backwards compatibility problems." type="warning" %}
-
-
-## `AbstractFormBuilderForm`
-
-To make using form builder easier, `AbstractFormBuilderForm` extends `AbstractForm` and provides most of the code needed to set up a form (of course without specific fields, those have to be added by the concrete form class), like reading and validating form values and using a database object action to use the form data to create or update a database object.
-
-In addition to the existing methods inherited by `AbstractForm`, `AbstractFormBuilderForm` provides the following methods:
-
-- `buildForm()` builds the form in the following steps:
-
- 1. Call `AbtractFormBuilderForm::createForm()` to create the `IFormDocument` object and add the form fields.
- 2. Call `IFormDocument::build()` to build the form.
- 3. Call `AbtractFormBuilderForm::finalizeForm()` to finalize the form like adding dependencies.
-
- Additionally, between steps 1 and 2 and after step 3, the method provides two events, `createForm` and `buildForm` to allow plugins to register event listeners to execute additional code at the right point in time.
-- `createForm()` creates the `FormDocument` object and sets the form mode.
- Classes extending `AbstractFormBuilderForm` have to override this method (and call `parent::createForm()` as the first line in the overridden method) to add concrete form containers and form fields to the bare form document.
-- `finalizeForm()` is called after the form has been built and the complete form hierarchy has been established.
- This method should be overridden to add dependencies, for example.
-- `setFormAction()` is called at the end of `readData()` and sets the form document’s action based on the controller class name and whether an object is currently edited.
-- If an object is edited, at the beginning of `readData()`, `setFormObjectData()` is called which calls `IFormDocument::loadValuesFromObject()`.
- If values need to be loaded from additional sources, this method should be used for that.
-
-`AbstractFormBuilderForm` also provides the following (public) properties:
-
-- `$form` contains the `IFormDocument` object created in `createForm()`.
-- `$formAction` is either `create` (default) or `edit` and handles which method of the database object is called by default (`create` is called for `$formAction = 'create'` and `update` is called for `$formAction = 'edit'`) and is used to set the value of the `action` template variable.
-- `$formObject` contains the `IStorableObject` if the form is used to edit an existing object.
- For forms used to create objects, `$formObject` is always `null`.
- Edit forms have to manually identify the edited object based on the request data and set the value of `$formObject`.
-- `$objectActionName` can be used to set an alternative action to be executed by the database object action that deviates from the default action determined by the value of `$formAction`.
-- `$objectActionClass` is the name of the database object action class that is used to create or update the database object.
-
-
-## `DialogFormDocument`
-
-Form builder forms can also be used in dialogs.
-For such forms, `DialogFormDocument` should be used which provides the additional methods `cancelable($cancelable = true)` and `isCancelable()` to set and check if the dialog can be canceled.
-If a dialog form can be canceled, a cancel button is added.
-
-If the dialog form is fetched via an AJAX request, `IFormDocument::ajax()` has to be called.
-AJAX forms are registered with `WoltLabSuite/Core/Form/Builder/Manager` which also supports getting all of the data of a form via the `getData(formId)` function.
-The `getData()` function relies on all form fields creating and registering a `WoltLabSuite/Core/Form/Builder/Field/Field` object that provides the data of a specific field.
-
-To make it as easy as possible to work with AJAX forms in dialogs, `WoltLabSuite/Core/Form/Builder/Dialog` (abbreviated as `FormBuilderDialog` from now on) should generally be used instead of `WoltLabSuite/Core/Form/Builder/Manager` directly.
-The constructor of `FormBuilderDialog` expects the following parameters:
-
-- `dialogId`: id of the dialog element
-- `className`: PHP class used to get the form dialog (and save the data if `options.submitActionName` is set)
-- `actionName`: name of the action/method of `className` that returns the dialog; the method is expected to return an array with `formId` containg the id of the returned form and `dialog` containing the rendered form dialog
-- `options`: additional options:
- - `actionParameters` (default: empty): additional parameters sent during AJAX requests
- - `destroyOnClose` (default: `false`): if `true`, whenever the dialog is closed, the form is destroyed so that a new form is fetched if the dialog is opened again
- - `dialog`: additional dialog options used as `options` during dialog setup
- - `onSubmit`: callback when the form is submitted (takes precedence over `submitActionName`)
- - `submitActionName` (default: not set): name of the action/method of `className` called when the form is submitted
-
-The three public functions of `FormBuilderDialog` are:
-
-- `destroy()` destroys the dialog, the form, and all of the form fields.
-- `getData()` returns a Promise that returns the form data.
-- `open()` opens the dialog.
-
-Example:
-
-```javascript
-require(['WoltLabSuite/Core/Form/Builder/Dialog'], function(FormBuilderDialog) {
- var dialog = new FormBuilderDialog(
- 'testDialog',
- 'wcf\\data\\test\\TestAction',
- 'getDialog',
- {
- destroyOnClose: true,
- dialog: {
- title: 'Test Dialog'
- },
- submitActionName: 'saveDialog'
- }
- );
-
- elById('testDialogButton').addEventListener('click', function() {
- dialog.open();
- });
-});
-```
+++ /dev/null
----
-title: Package Installation Plugins
-sidebar: sidebar
-permalink: php_api_package_installation_plugins.html
-folder: php/api
----
-
-A package installation plugin (PIP) defines the behavior to handle a specific [instruction](package_package-xml.html#instruction) during package installation, update or uninstallation.
-
-## `AbstractPackageInstallationPlugin`
-
-Any package installation plugin has to implement the [IPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/IPackageInstallationPlugin.class.php) interface.
-It is recommended however, to extend the abstract implementation [AbstractPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/AbstractPackageInstallationPlugin.class.php) of this interface instead of directly implementing the interface.
-The abstract implementation will always provide sane methods in case of any API changes.
-
-### Class Members
-
-Package Installation Plugins have a few notable class members easing your work:
-
-#### `$installation`
-
-This member contains an instance of [PackageInstallationDispatcher](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php) which provides you with all meta data related to the current package being processed.
-The most common usage is the retrieval of the package ID via `$this->installation->getPackageID()`.
-
-#### `$application`
-
-Represents the abbreviation of the target application, e.g. `wbb` (default value: `wcf`), used for the name of database table in which the installed data is stored.
-
-
-## `AbstractXMLPackageInstallationPlugin`
-
-[AbstractPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/AbstractPackageInstallationPlugin.class.php) is the default implementation for all package installation plugins based upon a single XML document.
-It handles the evaluation of the document and provide you an object-orientated approach to handle its data.
-
-### Class Members
-
-#### `$className`
-
-Value must be the qualified name of a class deriving from [DatabaseObjectEditor](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/DatabaseObjectEditor.class.php) which is used to create and update objects.
-
-#### `$tagName`
-
-Specifies the tag name within a `<import>` or `<delete>` section of the XML document used for each installed object.
-
-#### `prepareImport(array $data)`
-
-The passed array `$data` contains the parsed value from each evaluated tag in the `<import>` section:
-
-- `$data['elements']` contains a list of tag names and their value.
-- `$data['attributes']` contains a list of attributes present on the tag identified by [$tagName](#tagname).
-
-This method should return an one-dimensional array, where each key maps to the corresponding database column name (key names are case-sensitive).
-It will be passed to either `DatabaseObjectEditor::create()` or `DatabaseObjectEditor::update()`.
-
-Example:
-
-```php
-<?php
-return [
- 'environment' => $data['elements']['environment'],
- 'eventName' => $data['elements']['eventname'],
- 'name' => $data['attributes']['name']
-];
-```
-
-#### `validateImport(array $data)`
-
-The passed array `$data` equals the data returned by [prepareImport()](#prepareimportarray-data).
-This method has no return value, instead you should throw an exception if the passed data is invalid.
-
-
-#### `findExistingItem(array $data)`
-
-The passed array `$data` equals the data returned by [prepareImport()](#prepareimportarray-data).
-This method is expected to return an array with two keys:
-
-- `sql` contains the SQL query with placeholders.
-- `parameters` contains an array with values used for the SQL query.
-
-#### 2.5.3. Example
-
-```php
-<?php
-$sql = "SELECT *
- FROM wcf".WCF_N."_".$this->tableName."
- WHERE packageID = ?
- AND name = ?
- AND templateName = ?
- AND eventName = ?
- AND environment = ?";
-$parameters = [
- $this->installation->getPackageID(),
- $data['name'],
- $data['templateName'],
- $data['eventName'],
- $data['environment']
-];
-
-return [
- 'sql' => $sql,
- 'parameters' => $parameters
-];
-```
-
-#### `handleDelete(array $items)`
-
-The passed array `$items` contains the original node data, similar to [prepareImport()](#prepareimportarray-data).
-You should make use of this data to remove the matching element from database.
-
-Example:
-```php
-<?php
-$sql = "DELETE FROM wcf".WCF_N."_".$this->tableName."
- WHERE packageID = ?
- AND environment = ?
- AND eventName = ?
- AND name = ?
- AND templateName = ?";
-$statement = WCF::getDB()->prepareStatement($sql);
-foreach ($items as $item) {
- $statement->execute([
- $this->installation->getPackageID(),
- $item['elements']['environment'],
- $item['elements']['eventname'],
- $item['attributes']['name'],
- $item['elements']['templatename']
- ]);
-}
-```
-
-#### `postImport()`
-
-Allows you to (optionally) run additionally actions after all elements were processed.
-
-
-## `AbstractOptionPackageInstallationPlugin`
-
-[AbstractOptionPackageInstallationPlugin](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php) is an abstract implementation for options, used for:
-
-- ACL Options
-- Options
-- User Options
-- User Group Options
-
-### Differences to `AbstractXMLPackageInstallationPlugin`
-
-#### `$reservedTags`
-
-`$reservedTags` is a list of reserved tag names so that any tag encountered but not listed here will be added to the database column `additionalData`.
-This allows options to store arbitrary data which can be accessed but were not initially part of the PIP specifications.
+++ /dev/null
----
-title: Sitemaps
-sidebar: sidebar
-permalink: php_api_sitemaps.html
-folder: php/api
----
-
-{% include callout.html content="This feature is available with WoltLab Suite 3.1 or newer only." type="warning" %}
-
-Since version 3.1, WoltLab Suite Core is capable of automatically creating a sitemap.
-This sitemap contains all static pages registered via the page package installation plugin and which may be indexed by search engines (checking the `allowSpidersToIndex` parameter and page permissions) and do not expect an object ID.
-Other pages have to be added to the sitemap as a separate object.
-
-The only prerequisite for sitemap objects is that the objects are instances of `wcf\data\DatabaseObject` and that there is a `wcf\data\DatabaseObjectList` implementation.
-
-First, we implement the PHP class, which provides us all database objects and optionally checks the permissions for a single object.
-The class must implement the interface `wcf\system\sitemap\object\ISitemapObjectObjectType`.
-However, in order to have some methods already implemented and ensure backwards compatibility, you should use the abstract class `wcf\system\sitemap\object\AbstractSitemapObjectObjectType`.
-The abstract class takes care of generating the `DatabaseObjectList` class name and list directly and implements optional methods with the default values.
-The only method that you have to implement yourself is the `getObjectClass()` method which returns the fully qualified name of the `DatabaseObject` class.
-The `DatabaseObject` class must implement the interface `wcf\data\ILinkableObject`.
-
-Other optional methods are:
-
-* The `getLastModifiedColumn()` method returns the name of the column in the database where the last modification date is stored.
- If there is none, this method must return `null`.
-* The `canView()` method checks whether the passed `DatabaseObject` is visible to the current user with the current user always being a guest.
-* The `getObjectListClass()` method returns a non-standard `DatabaseObjectList` class name.
-* The `getObjectList()` method returns the `DatabaseObjectList` instance.
- You can, for example, specify additional query conditions in the method.
-
-As an example, the implementation for users looks like this:
-
-```php
-<?php
-namespace wcf\system\sitemap\object;
-use wcf\data\user\User;
-use wcf\data\DatabaseObject;
-use wcf\system\WCF;
-
-/**
- * User sitemap implementation.
- *
- * @author Joshua Ruesweg
- * @copyright 2001-2017 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\Sitemap\Object
- * @since 3.1
- */
-class UserSitemapObject extends AbstractSitemapObjectObjectType {
- /**
- * @inheritDoc
- */
- public function getObjectClass() {
- return User::class;
- }
-
- /**
- * @inheritDoc
- */
- public function getLastModifiedColumn() {
- return 'lastActivityTime';
- }
-
- /**
- * @inheritDoc
- */
- public function canView(DatabaseObject $object) {
- return WCF::getSession()->getPermission('user.profile.canViewUserProfile');
- }
-}
-```
-
-Next, the sitemap object must be registered as an object type:
-
-```xml
-<type>
- <name>com.example.plugin.sitemap.object.user</name>
- <definitionname>com.woltlab.wcf.sitemap.object</definitionname>
- <classname>wcf\system\sitemap\object\UserSitemapObject</classname>
- <priority>0.5</priority>
- <changeFreq>monthly</changeFreq>
- <rebuildTime>259200</rebuildTime>
-</type>
-```
-
-In addition to the fully qualified class name, the object type definition `com.woltlab.wcf.sitemap.object` and the object type name, the parameters `priority`, `changeFreq` and `rebuildTime` must also be specified.
-`priority` ([https://www.sitemaps.org/protocol.html#prioritydef](https://www.sitemaps.org/protocol.html#prioritydef)) and `changeFreq` ([https://www.sitemaps.org/protocol.html#changefreqdef](https://www.sitemaps.org/protocol.html#changefreqdef)) are specifications in the sitemaps protocol and can be changed by the user in the ACP.
-The `priority` should be `0.5` by default, unless there is an important reason to change it.
-The parameter `rebuildTime` specifies the number of seconds after which the sitemap should be regenerated.
-
-Finally, you have to create the language variable for the sitemap object.
-The language variable follows the pattern `wcf.acp.sitemap.objectType.{objectTypeName}` and is in the category `wcf.acp.sitemap`.
\ No newline at end of file
+++ /dev/null
----
-title: User Activity Points
-sidebar: sidebar
-permalink: php_api_user_activity_points.html
-folder: php/api
----
-
-Users get activity points whenever they create content to award them for their contribution.
-Activity points are used to determine the rank of a user and can also be used for user conditions, for example for automatic user group assignments.
-
-To integrate activity points into your package, you have to register an object type for the defintion `com.woltlab.wcf.user.activityPointEvent` and specify a default number of points:
-
-```xml
-<type>
- <name>com.example.foo.activityPointEvent.bar</name>
- <definitionname>com.woltlab.wcf.user.activityPointEvent</definitionname>
- <points>10</points>
-</type>
-```
-
-The number of points awarded for this type of activity point event can be changed by the administrator in the admin control panel.
-For this form and the user activity point list shown in the frontend, you have to provide the language item
-
-```
-wcf.user.activityPoint.objectType.com.example.foo.activityPointEvent.bar
-```
-
-that contains the name of the content for which the activity points are awarded.
-
-If a relevant object is created, you have to use `UserActivityPointHandler::fireEvent()` which expects the name of the activity point event object type, the id of the object for which the points are awarded (though the object id is not used at the moment) and the user who gets the points:
-
-```php
-UserActivityPointHandler::getInstance()->fireEvent(
- 'com.example.foo.activityPointEvent.bar',
- $bar->barID,
- $bar->userID
-);
-```
-
-To remove activity points once objects are deleted, you have to use `UserActivityPointHandler::removeEvents()` which also expects the name of the activity point event object type and additionally an array mapping the id of the user whose activity points will be reduced to the number of objects that are removed for the relevant user:
-
-```php
-UserActivityPointHandler::getInstance()->removeEvents(
- 'com.example.foo.activityPointEvent.bar',
- [
- 1 => 1, // remove points for one object for user with id `1`
- 4 => 2 // remove points for two objects for user with id `4`
- ]
-);
-```
+++ /dev/null
----
-title: User Notifications
-sidebar: sidebar
-permalink: php_api_user_notifications.html
-folder: php/api
----
-
-WoltLab Suite includes a powerful user notification system that supports notifications directly shown on the website and emails sent immediately or on a daily basis.
-
-
-## `objectType.xml`
-
-For any type of object related to events, you have to define an object type for the object type definition `com.woltlab.wcf.notification.objectType`:
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/objectType.xsd">
- <import>
- <type>
- <name>com.woltlab.example.foo</name>
- <definitionname>com.woltlab.wcf.notification.objectType</definitionname>
- <classname>example\system\user\notification\object\type\FooUserNotificationObjectType</classname>
- <category>com.woltlab.example</category>
- </type>
- </import>
-</data>
-```
-
-The referenced class `FooUserNotificationObjectType` has to implement the [IUserNotificationObjectType](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/object/type/IUserNotificationObjectType.class.php) interface, which should be done by extending [AbstractUserNotificationObjectType](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/object/type/AbstractUserNotificationObjectType.class.php).
-
-
-```php
-<?php
-namespace example\system\user\notification\object\type;
-use example\data\foo\Foo;
-use example\data\foo\FooList;
-use example\system\user\notification\object\FooUserNotificationObject;
-use wcf\system\user\notification\object\type\AbstractUserNotificationObjectType;
-
-/**
- * Represents a foo as a notification object type.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2017 WoltLab GmbH
- * @license WoltLab License <http://www.woltlab.com/license-agreement.html>
- * @package WoltLabSuite\Example\System\User\Notification\Object\Type
- */
-class FooUserNotificationObjectType extends AbstractUserNotificationObjectType {
- /**
- * @inheritDoc
- */
- protected static $decoratorClassName = FooUserNotificationObject::class;
-
- /**
- * @inheritDoc
- */
- protected static $objectClassName = Foo::class;
-
- /**
- * @inheritDoc
- */
- protected static $objectListClassName = FooList::class;
-}
-```
-
-You have to set the class names of the database object (`$objectClassName`) and the related list (`$objectListClassName`).
-Additionally, you have to create a class that implements the [IUserNotificationObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/object/IUserNotificationObject.class.php) whose name you have to set as the value of the `$decoratorClassName` property.
-
-```php
-<?php
-namespace example\system\user\notification\object;
-use example\data\foo\Foo;
-use wcf\data\DatabaseObjectDecorator;
-use wcf\system\user\notification\object\IUserNotificationObject;
-
-/**
- * Represents a foo as a notification object.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2017 WoltLab GmbH
- * @license WoltLab License <http://www.woltlab.com/license-agreement.html>
- * @package WoltLabSuite\Example\System\User\Notification\Object
- *
- * @method Foo getDecoratedObject()
- * @mixin Foo
- */
-class FooUserNotificationObject extends DatabaseObjectDecorator implements IUserNotificationObject {
- /**
- * @inheritDoc
- */
- protected static $baseClass = Foo::class;
-
- /**
- * @inheritDoc
- */
- public function getTitle() {
- return $this->getDecoratedObject()->getTitle();
- }
-
- /**
- * @inheritDoc
- */
- public function getURL() {
- return $this->getDecoratedObject()->getLink();
- }
-
- /**
- * @inheritDoc
- */
- public function getAuthorID() {
- return $this->getDecoratedObject()->userID;
- }
-}
-```
-
-- The `getTitle()` method returns the title of the object.
- In this case, we assume that the `Foo` class has implemented the [ITitledObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/ITitledObject.class.php) interface so that the decorated `Foo` can handle this method call itself.
-- The `getURL()` method returns the link to the object.
- As for the `getTitle()`, we assume that the `Foo` class has implemented the [ILinkableObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/ILinkableObject.class.php) interface so that the decorated `Foo` can also handle this method call itself.
-- The `getAuthorID()` method returns the id of the user who created the decorated `Foo` object.
- We assume that `Foo` objects have a `userID` property that contains this id.
-
-
-## `userNotificationEvent.xml`
-
-Each event that you fire in your package needs to be registered using the [user notification event package installation plugin](package_pip_user-notification-event.html).
-An example file might look like this:
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/userNotificationEvent.xsd">
- <import>
- <event>
- <name>bar</name>
- <objecttype>com.woltlab.example.foo</objecttype>
- <classname>example\system\user\notification\event\FooUserNotificationEvent</classname>
- <preset>1</preset>
- </event>
- </import>
-</data>
-```
-
-Here, you reference the user notification object type created via `objectType.xml`.
-The referenced class in the `<classname>` element has to implement the [IUserNotificationEvent](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/event/IUserNotificationEvent.class.php) interface by extending the [AbstractUserNotificationEvent](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/event/AbstractUserNotificationEvent.class.php) class or the [AbstractSharedUserNotificationEvent](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/user/notification/event/AbstractSharedUserNotificationEvent.class.php) class if you want to pre-load additional data before processing notifications.
-In `AbstractSharedUserNotificationEvent::prepare()`, you can, for example, tell runtime caches to prepare to load certain objects which then are loaded all at once when the objects are needed.
-
-```php
-<?php
-namespace example\system\user\notification\event;
-use example\system\cache\runtime\BazRuntimeCache;
-use example\system\user\notification\object\FooUserNotificationObject;
-use wcf\system\email\Email;
-use wcf\system\request\LinkHandler;
-use wcf\system\user\notification\event\AbstractSharedUserNotificationEvent;
-
-/**
- * Notification event for foos.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2017 WoltLab GmbH
- * @license WoltLab License <http://www.woltlab.com/license-agreement.html>
- * @package WoltLabSuite\Example\System\User\Notification\Event
- *
- * @method FooUserNotificationObject getUserNotificationObject()
- */
-class FooUserNotificationEvent extends AbstractSharedUserNotificationEvent {
- /**
- * @inheritDoc
- */
- protected $stackable = true;
-
- /** @noinspection PhpMissingParentCallCommonInspection */
- /**
- * @inheritDoc
- */
- public function checkAccess() {
- $this->getUserNotificationObject()->setBaz(BazRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->bazID));
-
- if (!$this->getUserNotificationObject()->isAccessible()) {
- // do some cleanup, if necessary
-
- return false;
- }
-
- return true;
- }
-
- /** @noinspection PhpMissingParentCallCommonInspection */
- /**
- * @inheritDoc
- */
- public function getEmailMessage($notificationType = 'instant') {
- $this->getUserNotificationObject()->setBaz(BazRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->bazID));
-
- $messageID = '<com.woltlab.example.baz/'.$this->getUserNotificationObject()->bazID.'@'.Email::getHost().'>';
-
- return [
- 'application' => 'example',
- 'in-reply-to' => [$messageID],
- 'message-id' => 'com.woltlab.example.foo/'.$this->getUserNotificationObject()->fooID,
- 'references' => [$messageID],
- 'template' => 'email_notification_foo'
- ];
- }
-
- /**
- * @inheritDoc
- * @since 5.0
- */
- public function getEmailTitle() {
- $this->getUserNotificationObject()->setBaz(BazRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->bazID));
-
- return $this->getLanguage()->getDynamicVariable('example.foo.notification.mail.title', [
- 'userNotificationObject' => $this->getUserNotificationObject()
- ]);
- }
-
- /** @noinspection PhpMissingParentCallCommonInspection */
- /**
- * @inheritDoc
- */
- public function getEventHash() {
- return sha1($this->eventID . '-' . $this->getUserNotificationObject()->bazID);
- }
-
- /**
- * @inheritDoc
- */
- public function getLink() {
- return LinkHandler::getInstance()->getLink('Foo', [
- 'application' => 'example',
- 'object' => $this->getUserNotificationObject()->getDecoratedObject()
- ]);
- }
-
- /**
- * @inheritDoc
- */
- public function getMessage() {
- $authors = $this->getAuthors();
- $count = count($authors);
-
- if ($count > 1) {
- if (isset($authors[0])) {
- unset($authors[0]);
- }
- $count = count($authors);
-
- return $this->getLanguage()->getDynamicVariable('example.foo.notification.message.stacked', [
- 'author' => $this->author,
- 'authors' => array_values($authors),
- 'count' => $count,
- 'guestTimesTriggered' => $this->notification->guestTimesTriggered,
- 'message' => $this->getUserNotificationObject(),
- 'others' => $count - 1
- ]);
- }
-
- return $this->getLanguage()->getDynamicVariable('example.foo.notification.message', [
- 'author' => $this->author,
- 'userNotificationObject' => $this->getUserNotificationObject()
- ]);
- }
-
- /**
- * @inheritDoc
- */
- public function getTitle() {
- $count = count($this->getAuthors());
- if ($count > 1) {
- return $this->getLanguage()->getDynamicVariable('example.foo.notification.title.stacked', [
- 'count' => $count,
- 'timesTriggered' => $this->notification->timesTriggered
- ]);
- }
-
- return $this->getLanguage()->get('example.foo.notification.title');
- }
-
- /**
- * @inheritDoc
- */
- protected function prepare() {
- BazRuntimeCache::getInstance()->cacheObjectID($this->getUserNotificationObject()->bazID);
- }
-}
-```
-
-- The `$stackable` property is `false` by default and has to be explicitly set to `true` if stacking of notifications should be enabled.
- Stacking of notification does not create new notifications for the same event for a certain object if the related action as been triggered by different users.
- For example, if something is liked by one user and then liked again by another user before the recipient of the notification has confirmed it, the existing notification will be amended to include both users who liked the content.
- Stacking can thus be used to avoid cluttering the notification list of users.
-- The `checkAccess()` method makes sure that the active user still has access to the object related to the notification.
- If that is not the case, the user notification system will automatically deleted the user notification based on the return value of the method.
- If you have any cached values related to notifications, you should also reset these values here.
-- The `getEmailMessage()` method return data to create the instant email or the daily summary email.
- For instant emails (`$notificationType = 'instant'`), you have to return an array like the one shown in the code above with the following components:
- - `application`:
- abbreviation of application
- - `in-reply-to` (optional):
- message id of the notification for the parent item and used to improve the ordering in threaded email clients
- - `message-id` (optional):
- message id of the notification mail and has to be used in `in-reply-to` and `references` for follow up mails
- - `references` (optional):
- all of the message ids of parent items (i.e. recursive in-reply-to)
- - `template`:
- name of the template used to render the email body, should start with `email_`
- - `variables` (optional):
- template variables passed to the email template where they can be accessed via `$notificationContent[variables]`
-
- For daily emails (`$notificationType = 'daily'`), only `application`, `template`, and `variables` are supported.
-- The `getEmailTitle()` returns the title of the instant email sent to the user.
- By default, `getEmailTitle()` simply calls `getTitle()`.
-- The `getEventHash()` method returns a hash by which user notifications are grouped.
- Here, we want to group them not by the actual `Foo` object but by its parent `Baz` object and thus overwrite the default implementation provided by `AbstractUserNotificationEvent`.
-- The `getLink()` returns the link to the `Foo` object the notification belongs to.
-- The `getMessage()` method and the `getTitle()` return the message and the title of the user notification respectively.
- By checking the value of `count($this->getAuthors())`, we check if the notification is stacked, thus if the event has been triggered for multiple users so that different languages items are used.
- If your notification event does not support stacking, this distinction is not necessary.
-- The `prepare()` method is called for each user notification before all user notifications are rendered.
- This allows to tell runtime caches to prepare to load objects later on (see [Runtime Caches](php_api_caches_runtime-caches.html)).
-
-
-## Firing Events
-
-When the action related to a user notification is executed, you can use `UserNotificationHandler::fireEvent()` to create the notifications:
-
-```php
-$recipientIDs = []; // fill with user ids of the recipients of the notification
-UserNotificationHandler::getInstance()->fireEvent(
- 'bar', // event name
- 'com.woltlab.example.foo', // event object type name
- new FooUserNotificationObject(new Foo($fooID)), // object related to the event
- $recipientIDs
-);
-```
-
-
-## Marking Notifications as Confirmed
-
-In some instances, you might want to manually mark user notifications as confirmed without the user manually confirming them, for example when they visit the page that is related to the user notification.
-In this case, you can use `UserNotificationHandler::markAsConfirmed()`:
-
-```php
-$recipientIDs = []; // fill with user ids of the recipients of the notification
-$fooIDs = []; // fill with ids of related foo objects
-UserNotificationHandler::getInstance()->markAsConfirmed(
- 'bar', // event name
- 'com.woltlab.example.foo', // event object type name
- $recipientIDs,
- $fooIDs
-);
-```
+++ /dev/null
----
-title: Apps for WoltLab Suite
-sidebar: sidebar
-permalink: php_apps.html
-folder: php
----
-
-## Introduction
-
-Apps are among the most powerful components in WoltLab Suite. Unlike plugins
-that extend an existing functionality and pages, apps have their own frontend
-with a dedicated namespace, database table prefixes and template locations.
-
-However, apps are meant to be a logical (and to some extent physical) separation
-from other parts of the framework, including other installed apps. They offer
-an additional layer of isolation and enable you to re-use class and template
-names that are already in use by the Core itself.
-
-If you've come here, thinking about the question if your next package should be
-an app instead of a regular plugin, the result is almost always: _No._
-
-## Differences to Plugins
-
-Apps do offer a couple of unique features that are not available to plugins and
-there are valid reasons to use one instead of a plugin, but they also increase
-both the code and system complexity. There is a performance penalty for each
-installed app, regardless if it is actively used in a request or not, simplying
-being there forces the Core to include it in many places, for example, class
-resolution or even simple tasks such as constructing a link.
-
-### Unique Namespace
-
-Each app has its own unique namespace that is entirely separated from the Core
-and any other installed apps. The namespace is derived from the last part of the
-package identifier, for example, `com.example.foo` will yield the namespace `foo`.
-
-The namespace is always relative to the installation directory of the app, it
-doesn't matter if the app is installed on `example.com/foo/` or in `example.com/bar/`,
-the namespace will always resolve to the right directory.
-
-This app namespace is also used for ACP templates, frontend templates and files:
-
-```xml
-<!-- somewhere in the package.xml -->
-<instructions type="file" application="foo" />
-```
-
-### Unique Database Table Prefix
-
-All database tables make use of a generic prefix that is derived from one of the
-installed apps, including `wcf` which resolves to the Core itself. Following the
-aforementioned example, the new prefix `fooN_` will be automatically registered
-and recognized in any generated statement.
-
-Any `DatabaseObject` that uses the app's namespace is automatically assumed to
-use the app's database prefix. For instance, `foo\data\bar\Bar` is implicitly
-mapped to the database table `fooN_bar`.
-
-The app prefix is recognized in SQL-PIPs and statements that reference one of
-its database tables are automatically rewritten to use the Core's instance number.
-
-### Separate Domain and Path Configuration
-
-Any controller that is provided by a plugin is served from the configured domain
-and path of the corresponding app, such as plugins for the Core are always
-served from the Core's directory. Apps are different and use their own domain
-and/or path to present their content, additionally, this allows the app to re-use
-a controller name that is already provided by the Core or any other app itself.
-
-## Creating an App
-
-{% include callout.html content="This is a non-reversible operation! Once a package has been installed, its type cannot be changed without uninstalling and reinstalling the entire package, an app will always be an app and vice versa." type="danger" %}
-
-### `package.xml`
-
-The `package.xml` supports two additional elements in the `<packageinformation>`
-block that are unique to applications.
-
-#### `<isapplication>1</isapplication>`
-
-This element is responsible to flag a package as an app.
-
-#### `<applicationdirectory>example</applicationdirectory>`
-
-Sets the suggested name of the application directory when installing it, the
-path result in `<path-to-the-core>/example/`. If you leave this element out,
-the app identifier (`com.example.foo -> foo`) will be used instead.
-
-### Minimum Required Files
-
-An example project with the [source code can be found on GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/basic-app/),
-it includes everything that is required for a basic app.
-
-{% include links.html %}
+++ /dev/null
----
-title: Code Style
-sidebar: sidebar
-permalink: php_code-style.html
-folder: php
----
-
-{% include callout.html content="The following code style conventions are used by us for our own packages. While you do not have to follow every rule, you are encouraged to do so." type="info" %}
-
-For information about how to document your code, please refer to the [documentation page](php_code-style_documentation.html).
-
-
-## General Code Style
-
-### Naming conventions
-
-The relevant naming conventions are:
-
-- **Upper camel case**:
- The first letters of all compound words are written in upper case.
-- **Lower camel case**:
- The first letters of compound words are written in upper case, except for the first letter which is written in lower case.
-- **Screaming snake case**:
- All letters are written in upper case and compound words are separated by underscores.
-
-
-| Type | Convention | Example |
-|------|------------|---------|
-| Variable | lower camel case | `$variableName` |
-| Class | upper camel case | `class UserGroupEditor` |
-| Properties | lower camel case | `public $propertyName` |
-| Method | lower camel case | `public function getObjectByName()` |
-| Constant | screaming snake case | `MODULE_USER_THING` |
-
-### Arrays
-
-For arrays, use the short array syntax introduced with PHP 5.4.
-The following example illustrates the different cases that can occur when working with arrays and how to format them:
-
-```php
-<?php
-
-$empty = [];
-
-$oneElement = [1];
-$multipleElements = [1, 2, 3];
-
-$oneElementWithKey = ['firstElement' => 1];
-$multipleElementsWithKey = [
- 'firstElement' => 1,
- 'secondElement' => 2,
- 'thirdElement' => 3
-];
-```
-
-### Ternary Operator
-
-The ternary operator can be used for short conditioned assignments:
-
-```php
-<?php
-
-$name = isset($tagArgs['name']) ? $tagArgs['name'] : 'default';
-```
-
-The condition and the values should be short so that the code does not result in a very long line which thus decreases the readability compared to an `if-else` statement.
-
-Parentheses may only be used around the condition and not around the whole statement:
-
-```php
-<?php
-
-// do not do it like this
-$name = (isset($tagArgs['name']) ? $tagArgs['name'] : 'default');
-```
-
-Parentheses around the conditions may not be used to wrap simple function calls:
-
-```php
-<?php
-
-// do not do it like this
-$name = (isset($tagArgs['name'])) ? $tagArgs['name'] : 'default';
-```
-
-but have to be used for comparisons or other binary operators:
-
-```php
-<?php
-
-$value = ($otherValue > $upperLimit) ? $additionalValue : $otherValue;
-```
-
-If you need to use more than one binary operator, use an `if-else` statement.
-
-The same rules apply to assigning array values:
-
-```php
-<?php
-
-$values = [
- 'first' => $firstValue,
- 'second' => $secondToggle ? $secondValueA : $secondValueB,
- 'third' => ($thirdToogle > 13) ? $thirdToogleA : $thirdToogleB
-];
-```
-
-or return values:
-
-```php
-<?php
-
-return isset($tagArgs['name']) ? $tagArgs['name'] : 'default';
-```
-
-### Whitespaces
-
-You have to put a whitespace *in front* of the following things:
-
-- equal sign in assignments: `$x = 1;`
-- comparison operators: `$x == 1`
-- opening bracket of a block `public function test() {`
-
-You have to put a whitespace *behind* the following things:
-
-- equal sign in assignments: `$x = 1;`
-- comparison operators: `$x == 1`
-- comma in a function/method parameter list if the comma is not followed by a line break: `public function test($a, $b) {`
-- `if`, `for`, `foreach`, `while`: `if ($x == 1)`
-
-
-## Classes
-
-### Referencing Class Names
-
-If you have to reference a class name inside a php file, you have to use the `class` keyword.
-
-```php
-<?php
-
-// not like this
-$className = 'wcf\data\example\Example';
-
-// like this
-use wcf\data\example\Example;
-$className = Example::class;
-```
-
-### Static Getters (of `DatabaseObject` Classes)
-
-Some database objects provide static getters, either if they are decorators or for a unique combination of database table columns, like `wcf\data\box\Box::getBoxByIdentifier()`:
-
-```php
-<?php
-namespace wcf\data\box;
-use wcf\data\DatabaseObject;
-use wcf\system\WCF;
-
-class Box extends DatabaseObject {
- /**
- * Returns the box with the given identifier.
- *
- * @param string $identifier
- * @return Box|null
- */
- public static function getBoxByIdentifier($identifier) {
- $sql = "SELECT *
- FROM wcf".WCF_N."_box
- WHERE identifier = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$identifier]);
-
- return $statement->fetchObject(self::class);
- }
-}
-```
-
-Such methods should always either return the desired object or `null` if the object does not exist.
-`wcf\system\database\statement\PreparedStatement::fetchObject()` already takes care of this distinction so that its return value can simply be returned by such methods.
-
-The name of such getters should generally follow the convention `get{object type}By{column or other description}`.
-
-### Long method calls
-
-In some instances, methods with many argument have to be called which can result in lines of code like this one:
-
-```php
-<?php
-
-\wcf\system\search\SearchIndexManager::getInstance()->set('com.woltlab.wcf.article', $articleContent->articleContentID, $articleContent->content, $articleContent->title, $articles[$articleContent->articleID]->time, $articles[$articleContent->articleID]->userID, $articles[$articleContent->articleID]->username, $articleContent->languageID, $articleContent->teaser);
-```
-
-which is hardly readable.
-Therefore, the line must be split into multiple lines with each argument in a separate line:
-
-```php
-<?php
-
-\wcf\system\search\SearchIndexManager::getInstance()->set(
- 'com.woltlab.wcf.article',
- $articleContent->articleContentID,
- $articleContent->content,
- $articleContent->title,
- $articles[$articleContent->articleID]->time,
- $articles[$articleContent->articleID]->userID,
- $articles[$articleContent->articleID]->username,
- $articleContent->languageID,
- $articleContent->teaser
-);
-```
-
-In general, this rule applies to the following methods:
-
-- `wcf\system\edit\EditHistoryManager::add()`
-- `wcf\system\message\quote\MessageQuoteManager::addQuote()`
-- `wcf\system\message\quote\MessageQuoteManager::getQuoteID()`
-- `wcf\system\search\SearchIndexManager::set()`
-- `wcf\system\user\object\watch\UserObjectWatchHandler::updateObject()`
-- `wcf\system\user\notification\UserNotificationHandler::fireEvent()`
+++ /dev/null
----
-title: Documentation
-sidebar: sidebar
-permalink: php_code-style_documentation.html
-folder: php
-parent: php_code-style
----
-
-{% include callout.html content="The following documentation conventions are used by us for our own packages. While you do not have to follow every rule, you are encouraged to do so." type="info" %}
-
-
-## Database Objects
-
-### Database Table Columns as Properties
-
-As the database table columns are not explicit properties of the classes extending `wcf\data\DatabaseObject` but rather stored in `DatabaseObject::$data` and accessible via `DatabaseObject::__get($name)`, the IDE we use, PhpStorm, is neither able to autocomplete such property access nor to interfere the type of the property.
-
-To solve this problem, `@property-read` tags must be added to the class documentation which registers the database table columns as public read-only properties:
-
-```
- * @property-read propertyType $propertyName property description
-```
-
-The properties have to be in the same order as the order in the database table.
-
-The following table provides templates for common description texts so that similar database table columns have similar description texts.
-
-| property | description template and example |
-|----------|----------------------------------|
-| unique object id | `unique id of the {object name}`<br>**example:** `unique id of the acl option`|
-| id of the delivering package | `id of the package which delivers the {object name}`<br>**example:** `id of the package which delivers the acl option`|
-| show order for nested structure | `position of the {object name} in relation to its siblings`<br>**example:** `position of the ACP menu item in relation to its siblings`|
-| show order within different object | `position of the {object name} in relation to the other {object name}s in the {parent object name}`<br>**example:** `position of the label in relation to the other labels in the label group`|
-| required permissions | `comma separated list of user group permissions of which the active user needs to have at least one to see (access, …) the {object name}`<br>**example:**`comma separated list of user group permissions of which the active user needs to have at least one to see the ACP menu item`|
-| required options | `comma separated list of options of which at least one needs to be enabled for the {object name} to be shown (accessible, …)`<br>**example:**`comma separated list of options of which at least one needs to be enabled for the ACP menu item to be shown`|
-| id of the user who has created the object | ``id of the user who created (wrote, …) the {object name} (or `null` if the user does not exist anymore (or if the {object name} has been created by a guest))``<br>**example:**``id of the user who wrote the comment or `null` if the user does not exist anymore or if the comment has been written by a guest``|
-| name of the user who has created the object | ``name of the user (or guest) who created (wrote, …) the {object name}``<br>**example:**``name of the user or guest who wrote the comment``|
-| additional data | `array with additional data of the {object name}`<br>**example:**`array with additional data of the user activity event`|
-| time-related columns | `timestamp at which the {object name} has been created (written, …)`<br>**example:**`timestamp at which the comment has been written`|
-| boolean options | ``is `1` (or `0`) if the {object name} … (and thus …), otherwise `0` (or `1`)``<br>**example:**``is `1` if the ad is disabled and thus not shown, otherwise `0` ``|
-| `$cumulativeLikes` | ``cumulative result of likes (counting `+1`) and dislikes (counting `-1`) for the {object name}``<br>**example:**``cumulative result of likes (counting `+1`) and dislikes (counting `-1`) for the article``|
-| `$comments` | `number of comments on the {object name}`<br>**example:**`number of comments on the article`|
-| `$views` | `number of times the {object name} has been viewed`<br>**example:**`number of times the article has been viewed`|
-| text field with potential language item name as value | `{text type} of the {object name} or name of language item which contains the {text type}`<br>**example:**`description of the cronjob or name of language item which contains the description`|
-| `$objectTypeID` | ``id of the `{object type definition name}` object type``<br>**example:**``id of the `com.woltlab.wcf.modifiableContent` object type``|
-
-
-## Database Object Editors
-
-### Class Tags
-
-Any database object editor class comment must have to following tags to properly support autocompletion by IDEs:
-
-```php
-/**
- * …
- * @method static {DBO class name} create(array $parameters = [])
- * @method {DBO class name} getDecoratedObject()
- * @mixin {DBO class name}
- */
-```
-
-The only exception to this rule is if the class overwrites the `create()` method which itself has to be properly documentation then.
-
-The first and second line makes sure that when calling the `create()` or `getDecoratedObject()` method, the return value is correctly recognized and not just a general `DatabaseObject` instance.
-The third line tells the IDE (if `@mixin` is supported) that the database object editor decorates the database object and therefore offers autocompletion for properties and methods from the database object class itself.
-
-
-## Runtime Caches
-
-### Class Tags
-
-Any class implementing the [IRuntimeCache](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/cache/runtime/IRuntimeCache.class.php) interface must have the following class tags:
-
-```php
-/**
- * …
- * @method {DBO class name}[] getCachedObjects()
- * @method {DBO class name} getObject($objectID)
- * @method {DBO class name}[] getObjects(array $objectIDs)
- */
-```
-
-These tags ensure that when calling any of the mentioned methods, the return value refers to the concrete database object and not just generically to [DatabaseObject](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/data/DatabaseObject.class.php).
+++ /dev/null
----
-title: Database Access
-sidebar: sidebar
-permalink: php_database-access.html
-folder: php
----
-
-[Database Objects][php_database-objects] provide a convenient and object-oriented approach to work with the database, but there can be use-cases that require raw access including writing methods for model classes. This section assumes that you have either used [prepared statements](https://en.wikipedia.org/wiki/Prepared_statement) before or at least understand how it works.
-
-## The PreparedStatement Object
-
-The database access is designed around [PreparedStatement](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/database/statement/PreparedStatement.class.php), built on top of PHP's `PDOStatement` so that you call all of `PDOStatement`'s methods, and each query requires you to obtain a statement object.
-
-```php
-<?php
-$statement = \wcf\system\WCF::getDB()->prepareStatement("SELECT * FROM wcf".WCF_N."_example");
-$statement->execute();
-while ($row = $statement->fetchArray()) {
- // handle result
-}
-```
-
-### Query Parameters
-
-The example below illustrates the usage of parameters where each value is replaced with the generic `?`-placeholder. Values are provided by calling `$statement->execute()` with a continuous, one-dimensional array that exactly match the number of question marks.
-
-```php
-<?php
-$sql = "SELECT *
- FROM wcf".WCF_N."_example
- WHERE exampleID = ?
- OR bar IN (?, ?, ?, ?, ?)";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-$statement->execute([
- $exampleID,
- $list, $of, $values, $for, $bar
-]);
-while ($row = $statement->fetchArray()) {
- // handle result
-}
-```
-
-### Fetching a Single Result
-
-{% include callout.html content="Do not attempt to use `fetchSingleRow()` or `fetchSingleColumn()` if the result contains more than one row." type="danger" %}
-
-You can opt-in to retrieve only a single row from database and make use of shortcut methods to reduce the code that you have to write.
-
-```php
-<?php
-$sql = "SELECT *
- FROM wcf".WCF_N."_example
- WHERE exampleID = ?";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql, 1);
-$statement->execute([$exampleID]);
-$row = $statement->fetchSingleRow();
-```
-
-There are two distinct differences when comparing with the example on query parameters above:
-
-1. The method `prepareStatement()` receives a secondary parameter that will be appended to the query as `LIMIT 1`.
-2. Data is read using `fetchSingleRow()` instead of `fetchArray()` or similar methods, that will read one result and close the cursor.
-
-### Fetch by Column
-
-{% include callout.html content="There is no way to return another column from the same row if you use `fetchColumn()` to retrieve data." type="warning" %}
-
-Fetching an array is only useful if there is going to be more than one column per result row, otherwise accessing the column directly is much more convenient and increases the code readability.
-
-```php
-<?php
-$sql = "SELECT bar
- FROM wcf".WCF_N."_example";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-$statement->execute();
-while ($bar = $statement->fetchColumn()) {
- // handle result
-}
-$bar = $statement->fetchSingleColumn();
-```
-
-Similar to fetching a single row, you can also issue a query that will select a single row, but reads only one column from the result row.
-
-```php
-<?php
-$sql = "SELECT bar
- FROM wcf".WCF_N."_example
- WHERE exampleID = ?";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql, 1);
-$statement->execute([$exampleID]);
-$bar = $statement->fetchSingleColumn();
-```
-
-### Fetching All Results
-
-If you want to fetch all results of a query but only store them in an array without directly processing them, in most cases, you can rely on built-in methods.
-
-To fetch all rows of query, you can use `PDOStatement::fetchAll()` with `\PDO::FETCH_ASSOC` as the first parameter:
-
-```php
-<?php
-$sql = "SELECT *
- FROM wcf".WCF_N."_example";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-$statement->execute();
-$rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
-```
-
-As a result, you get an array containing associative arrays with the rows of the `wcf{WCF_N}_example` database table as content.
-
-If you only want to fetch a list of the values of a certain column, you can use `\PDO::FETCH_COLUMN` as the first parameter:
-
-```php
-<?php
-$sql = "SELECT exampleID
- FROM wcf".WCF_N."_example";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-$statement->execute();
-$exampleIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
-```
-
-As a result, you get an array with all `exampleID` values.
-
-The `PreparedStatement` class adds an additional methods that covers another common use case in our code:
-Fetching two columns and using the first column's value as the array key and the second column's value as the array value.
-This case is covered by `PreparedStatement::fetchMap()`:
-
-```php
-<?php
-$sql = "SELECT exampleID, userID
- FROM wcf".WCF_N."_example_mapping";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-$statement->execute();
-$map = $statement->fetchMap('exampleID', 'userID');
-```
-
-`$map` is a one-dimensional array where each `exampleID` value maps to the corresponding `userID` value.
-
-{% include callout.html content="If there are multiple entries for a certain `exampleID` value with different `userID` values, the existing entry in the array will be overwritten and contain the last read value from the database table. Therefore, this method should generally only be used for unique combinations." type="warning" %}
-
-If you do not have a combination of columns with unique pairs of values, but you want to get a list of `userID` values with the same `exampleID`, you can set the third parameter of `fetchMap()` to `false` and get a list:
-
-```php
-<?php
-$sql = "SELECT exampleID, userID
- FROM wcf".WCF_N."_example_mapping";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-$statement->execute();
-$map = $statement->fetchMap('exampleID', 'userID', false);
-```
-
-Now, as a result, you get a two-dimensional array with the array keys being the `exampleID` values and the array values being arrays with all `userID` values from rows with the respective `exampleID` value.
-
-
-
-## Building Complex Conditions
-
-Building conditional conditions can turn out to be a real mess and it gets even worse with SQL's `IN (…)` which requires as many placeholders as there will be values. The solutions is `PreparedStatementConditionBuilder`, a simple but useful helper class with a bulky name, it is also the class used when accessing `DatabaseObjecList::getConditionBuilder()`.
-
-```php
-<?php
-$conditions = new \wcf\system\database\util\PreparedStatementConditionBuilder();
-$conditions->add("exampleID = ?", [$exampleID]);
-if (!empty($valuesForBar)) {
- $conditions->add("(bar IN (?) OR baz = ?)", [$valuesForBar, $baz]);
-}
-```
-
-The `IN (?)` in the example above is automatically expanded to match the number of items contained in `$valuesForBar`. Be aware that the method will generate an invalid query if `$valuesForBar` is empty!
-
-## INSERT or UPDATE in Bulk
-
-Prepared statements not only protect against SQL injection by separating the logical query and the actual data, but also provides the ability to reuse the same query with different values. This leads to a performance improvement as the code does not have to transmit the query with for every data set and only has to parse and analyze the query once.
-
-```php
-<?php
-$data = ['abc', 'def', 'ghi'];
-
-$sql = "INSERT INTO wcf".WCF_N."_example
- (bar)
- VALUES (?)";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-
-\wcf\system\WCF::getDB()->beginTransaction();
-foreach ($data as $bar) {
- $statement->execute([$bar]);
-}
-\wcf\system\WCF::getDB()->commitTransaction();
-```
-
-It is generally advised to wrap bulk operations in a transaction as it allows the database to optimize the process, including fewer I/O operations.
-
-```php
-<?php
-$data = [
- 1 => 'abc',
- 3 => 'def',
- 4 => 'ghi'
-];
-
-$sql = "UPDATE wcf".WCF_N."_example
- SET bar = ?
- WHERE exampleID = ?";
-$statement = \wcf\system\WCF::getDB()->prepareStatement($sql);
-
-\wcf\system\WCF::getDB()->beginTransaction();
-foreach ($data as $exampleID => $bar) {
- $statement->execute([
- $bar,
- $exampleID
- ]);
-}
-\wcf\system\WCF::getDB()->commitTransaction();
-```
-
-{% include links.html %}
+++ /dev/null
----
-title: Database Objects
-sidebar: sidebar
-permalink: php_database-objects.html
-folder: php
----
-
-WoltLab Suite uses a unified interface to work with database rows using an object based approach instead of using native arrays holding arbitrary data. Each database table is mapped to a model class that is designed to hold a single record from that table and expose methods to work with the stored data, for example providing assistance when working with normalized datasets.
-
-Developers are required to provide the proper DatabaseObject implementations themselves, they're not automatically generated, all though the actual code that needs to be written is rather small. The following examples assume the fictional database table `wcf1_example`, `exampleID` as the auto-incrementing primary key and the column `bar` to store some text.
-
-
-## DatabaseObject
-
-The basic model derives from `wcf\data\DatabaseObject` and provides a convenient constructor to fetch a single row or construct an instance using pre-loaded rows.
-
-```php
-<?php
-namespace wcf\data\example;
-use wcf\data\DatabaseObject;
-
-class Example extends DatabaseObject {}
-```
-
-The class is intended to be empty by default and there only needs to be code if you want to add additional logic to your model. Both the class name and primary key are determined by `DatabaseObject` using the namespace and class name of the derived class. The example above uses the namespace `wcf\…` which is used as table prefix and the class name `Example` is converted into `exampleID`, resulting in the database table name `wcfN_example` with the primary key `exampleID`.
-
-You can prevent this automatic guessing by setting the class properties `$databaseTableName` and `$databaseTableIndexName` manually.
-
-
-## DatabaseObjectDecorator
-
-If you already have a `DatabaseObject` class and would like to extend it with additional data or methods, for example by providing a class `ViewableExample` which features view-related changes without polluting the original object, you can use `DatabaseObjectDecorator` which a default implementation of a decorator for database objects.
-
-```php
-<?php
-namespace wcf\data\example;
-use wcf\data\DatabaseObjectDecorator;
-
-class ViewableExample extends DatabaseObjectDecorator {
- protected static $baseClass = Example::class;
-
- public function getOutput() {
- $output = '';
-
- // [determine output]
-
- return $output;
- }
-}
-```
-
-It is mandatory to set the static `$baseClass` property to the name of the decorated class.
-
-Like for any decorator, you can directly access the decorated object's properties and methods for a decorated object by accessing the property or calling the method on the decorated object.
-You can access the decorated objects directly via `DatabaseObjectDecorator::getDecoratedObject()`.
-
-
-## DatabaseObjectEditor
-
-{% include callout.html content="This is the low-level interface to manipulate data rows, it is recommended to use `AbstractDatabaseObjectAction`." type="info" %}
-
-Adding, editing and deleting models is done using the `DatabaseObjectEditor` class that decorates a `DatabaseObject` and uses its data to perform the actions.
-
-```php
-<?php
-namespace wcf\data\example;
-use wcf\data\DatabaseObjectEditor;
-
-class ExampleEditor extends DatabaseObjectEditor {
- protected static $baseClass = Example::class;
-}
-```
-
-The editor class requires you to provide the fully qualified name of the model, that is the class name including the complete namespace. Database table name and index key will be pulled directly from the model.
-
-### Create a new row
-
-Inserting a new row into the database table is provided through `DatabaseObjectEditor::create()` which yields a `DatabaseObject` instance after creation.
-
-```php
-<?php
-$example = \wcf\data\example\ExampleEditor::create([
- 'bar' => 'Hello World!'
-]);
-
-// output: Hello World!
-echo $example->bar;
-```
-
-### Updating an existing row
-
-{% include callout.html content="The internal state of the decorated `DatabaseObject` is not altered at any point, the values will still be the same after editing or deleting the represented row. If you need an object with the latest data, you'll have to discard the current object and refetch the data from database." type="warning" %}
-
-```php
-<?php
-$example = new \wcf\data\example\Example($id);
-$exampleEditor = new \wcf\data\example\ExampleEditor($example);
-$exampleEditor->update([
- 'bar' => 'baz'
-]);
-
-// output: Hello World!
-echo $example->bar;
-
-// re-creating the object will query the database again and retrieve the updated value
-$example = new \wcf\data\example\Example($example->id);
-
-// output: baz
-echo $example->bar;
-```
-
-### Deleting a row
-
-{% include callout.html content="Similar to the update process, the decorated `DatabaseObject` is not altered and will then point to an inexistent row." type="warning" %}
-
-```php
-<?php
-$example = new \wcf\data\example\Example($id);
-$exampleEditor = new \wcf\data\example\ExampleEditor($example);
-$exampleEditor->delete();
-```
-
-
-## DatabaseObjectList
-
-Every row is represented as a single instance of the model, but the instance creation deals with single rows only. Retrieving larger sets of rows would be quite inefficient due to the large amount of queries that will be dispatched. This is solved with the `DatabaseObjectList` object that exposes an interface to query the database table using arbitrary conditions for data selection. All rows will be fetched using a single query and the resulting rows are automatically loaded into separate models.
-
-```php
-<?php
-namespace wcf\data\example;
-use wcf\data\DatabaseObjectList;
-
-class ExampleList extends DatabaseObjectList {
- public $className = Example::class;
-}
-```
-
-The following code listing illustrates loading a large set of examples and iterating over the list to retrieve the objects.
-
-```php
-<?php
-$exampleList = new \wcf\data\example\ExampleList();
-// add constraints using the condition builder
-$exampleList->getConditionBuilder()->add('bar IN (?)', [['Hello World!', 'bar', 'baz']]);
-// actually read the rows
-$exampleList->readObjects();
-foreach ($exampleList as $example) {
- echo $example->bar;
-}
-
-// retrieve the models directly instead of iterating over them
-$examples = $exampleList->getObjects();
-
-// just retrieve the number of rows
-$exampleCount = $exampleList->countObjects();
-```
-
-`DatabaseObjectList` implements both [SeekableIterator](https://secure.php.net/manual/en/class.seekableiterator.php) and [Countable](https://secure.php.net/manual/en/class.countable.php).
-
-Additionally, `DatabaseObjectList` objects has the following three public properties that are useful when fetching data with lists:
-
-- `$sqlLimit` determines how many rows are fetched.
- If its value is `0` (which is the default value), all results are fetched.
- So be careful when dealing with large tables and you only want a limited number of rows:
- Set `$sqlLimit` to a value larger than zero!
-- `$sqlOffset`:
- Paginated pages like a thread list use this feature a lot, it allows you to skip a given number of results.
- Imagine you want to display 20 threads per page but there are a total of 60 threads available.
- In this case you would specify `$sqlLimit = 20` and `$sqlOffset = 20` which will skip the first 20 threads, effectively displaying thread 21 to 40.
-- `$sqlOrderBy` determines by which column(s) the rows are sorted in which order.
- Using our example in `$sqlOffset` you might want to display the 20 most recent threads on page 1, thus you should specify the order field and its direction, e.g. `$sqlOrderBy = 'thread.lastPostTime DESC'` which returns the most recent thread first.
-
-For more advanced usage, there two additional fields that deal with the type of objects returned.
-First, let's go into a bit more detail what setting the `$className` property actually does:
-
-1. It is the type of database object in which the rows are wrapped.
-2. It determines which database table is actually queried and which index is used (see the `$databaseTableName` and `$databaseTableIndexName` properties of `DatabaseObject`).
-
-Sometimes you might use the database table of some database object but wrap the rows in another database object.
-This can be achieved by setting the `$objectClassName` property to the desired class name.
-
-In other cases, you might want to wrap the created objects in a database object decorator which can be done by setting the `$decoratorClassName` property to the desired class name:
-
-```php
-<?php
-$exampleList = new \wcf\data\example\ExampleList();
-$exampleList->decoratorClassName = \wcf\data\example\ViewableExample::class;
-```
-
-Of course, you do not have to set the property after creating the list object, you can also set it by creating a dedicated class:
-
-```php
-<?php
-namespace wcf\data\example;
-
-class ViewableExampleList extends ExampleList {
- public $decoratorClassName = ViewableExample::class;
-}
-```
-
-
-## AbstractDatabaseObjectAction
-
-Row creation and manipulation can be performed using the aforementioned `DatabaseObjectEditor` class, but this approach has two major issues:
-
-1. Row creation, update and deletion takes place silently without notifying any other components.
-2. Data is passed to the database adapter without any further processing.
-
-The `AbstractDatabaseObjectAction` solves both problems by wrapping around the editor class and thus provide an additional layer between the action that should be taken and the actual process. The first problem is solved by a fixed set of events being fired, the second issue is addressed by having a single entry point for all data editing.
-
-```php
-<?php
-namespace wcf\data\example;
-use wcf\data\AbstractDatabaseObjectAction;
-
-class ExampleAction extends AbstractDatabaseObjectAction {
- public $className = ExampleEditor::class;
-}
-```
-
-### Executing an Action
-
-{% include callout.html content="The method `AbstractDatabaseObjectAction::validateAction()` is internally used for AJAX method invocation and must not be called programmatically." type="warning" %}
-
-The next example represents the same functionality as seen for `DatabaseObjectEditor`:
-
-```php
-<?php
-use wcf\data\example\ExampleAction;
-
-// create a row
-$exampleAction = new ExampleAction([], 'create', [
- 'data' => ['bar' => 'Hello World']
-]);
-$example = $exampleAction->executeAction()['returnValues'];
-
-// update a row using the id
-$exampleAction = new ExampleAction([1], 'update', [
- 'data' => ['bar' => 'baz']
-]);
-$exampleAction->executeAction();
-
-// delete a row using a model
-$exampleAction = new ExampleAction([$example], 'delete');
-$exampleAction->executeAction();
-```
-
-You can access the return values both by storing the return value of `executeAction()` or by retrieving it via `getReturnValues()`.
-
-<span class="label label-info">Events</span> `initializeAction`, `validateAction` and `finalizeAction`
-
-### Custom Method with AJAX Support
-
-This section is about adding the method `baz()` to `ExampleAction` and calling it via AJAX.
-
-#### AJAX Validation
-
-Methods of an action cannot be called via AJAX, unless they have a validation method. This means that `ExampleAction` must define both a `public function baz()` and `public function validateBaz()`, the name for the validation method is constructed by upper-casing the first character of the method name and prepending `validate`.
-
-The lack of the companion `validate*` method will cause the AJAX proxy to deny the request instantaneously. Do not add a validation method if you don't want it to be callable via AJAX ever!
-
-#### create, update and delete
-
-The methods `create`, `update` and `delete` are available for all classes deriving from `AbstractDatabaseObjectAction` and directly pass the input data to the `DatabaseObjectEditor`. These methods deny access to them via AJAX by default, unless you explicitly enable access. Depending on your case, there are two different strategies to enable AJAX access to them.
-
-```
-<?php
-namespace wcf\data\example;
-use wcf\data\AbstractDatabaseObjectAction;
-
-class ExampleAction extends AbstractDatabaseObjectAction {
- // `create()` can now be called via AJAX if the requesting user posses the listed permissions
- protected $permissionsCreate = ['admin.example.canManageExample'];
-
- public function validateUpdate() {
- // your very own validation logic that does not make use of the
- // built-in `$permissionsUpdate` property
-
- // you can still invoke the built-in permissions check if you like to
- parent::validateUpdate();
- }
-}
-```
-
-#### Allow Invokation by Guests
-
-Invoking methods is restricted to logged-in users by default and the only way to override this behavior is to alter the property `$allowGuestAccess`. It is a simple string array that is expected to hold all methods that should be accessible by users, excluding their companion validation methods.
-
-#### ACP Access Only
-
-Method access is usually limited by permissions, but sometimes there might be the need for some added security to avoid mistakes. The `$requireACP` property works similar to `$allowGuestAccess`, but enforces the request to originate from the ACP together with a valid ACP session, ensuring that only users able to access the ACP can actually invoke these methods.
+++ /dev/null
----
-title: Exceptions
-sidebar: sidebar
-permalink: php_exceptions.html
-folder: php
----
-
-## SPL Exceptions
-
-The [Standard PHP Library (SPL)](https://secure.php.net/manual/en/book.spl.php) provides some [exceptions](https://secure.php.net/manual/en/spl.exceptions.php) that should be used whenever possible.
-
-
-## Custom Exceptions
-
-{% include callout.html content="Do not use `wcf\system\exception\SystemException` anymore, use specific exception classes!" type="warning" %}
-
-The following table contains a list of custom exceptions that are commonly used.
-
-| exception | (examples) when to use |
-|-----------|------------------------|
-| `wcf\system\exception\IllegalLinkException` | access to a page that belongs to a non-existing object, executing actions on specific non-existing objects (is shown as http 404 error to the user) |
-| `wcf\system\exception\ImplementationException` | a class does not implement an expected interface |
-| `wcf\system\exception\InvalidObjectTypeException` | object type is not of an expected object type definition |
-| `wcf\system\exception\InvalidSecurityTokenException` | given security token does not match the security token of the active user's session |
-| `wcf\system\exception\ParentClassException` | a class does not extend an expected (parent) class |
-| `wcf\system\exception\PermissionDeniedException` | page access without permission, action execution without permission (is shown as http 403 error to the user) |
-| `wcf\system\exception\UserInputException` | user input does not pass validation |
+++ /dev/null
----
-title: General Data Protection Regulation (GDPR)
-sidebar: sidebar
-permalink: php_gdpr.html
-folder: php
----
-
-## Introduction
-
-The General Data Protection Regulation (GDPR) of the European Union enters into
-force on May 25, 2018. It comes with a set of restrictions when handling users'
-personal data as well as to provide an interface to export this data on demand.
-
-If you're looking for a guide on the implications of the GDPR and what you will
-need or consider to do, please read the article [Implementation of the GDPR](https://www.woltlab.com/article/106-implementation-of-the-gdpr/)
-on woltlab.com.
-
-## Including Data in the Export
-
-The `wcf\acp\action\UserExportGdprAction` introduced with WoltLab Suite 3.1.3
-already includes the Core itself as well as all official apps, but you'll need to
-include any personal data stored for your plugin or app by yourself.
-
-The event `export` is fired before any data is sent out, but after any Core data
-has been dumped to the `$data` property.
-
-### Example code
-
-```php
-<?php
-namespace wcf\system\event\listener;
-use wcf\acp\action\UserExportGdprAction;
-use wcf\data\user\UserProfile;
-
-class MyUserExportGdprActionListener implements IParameterizedEventListener {
- public function execute(/** @var UserExportGdprAction $eventObj */$eventObj, $className, $eventName, array &$parameters) {
- /** @var UserProfile $user */
- $user = $eventObj->user;
-
- $eventObj->data['my.fancy.plugin'] = [
- 'superPersonalData' => "This text is super personal and should be included in the output",
- 'weirdIpAddresses' => $eventObj->exportIpAddresses('app'.WCF_N.'_non_standard_column_names_for_ip_addresses', 'ipAddressColumnName', 'timeColumnName', 'userIDColumnName')
- ];
- $eventObj->exportUserProperties[] = 'shouldAlwaysExportThisField';
- $eventObj->exportUserPropertiesIfNotEmpty[] = 'myFancyField';
- $eventObj->exportUserOptionSettings[] = 'thisSettingIsAlwaysExported';
- $eventObj->exportUserOptionSettingsIfNotEmpty[] = 'someSettingContainingPersonalData';
- $eventObj->ipAddresses['my.fancy.plugin'] = ['wcf'.WCF_N.'_my_fancy_table', 'wcf'.WCF_N.'_i_also_store_ipaddresses_here'];
- $eventObj->skipUserOptions[] = 'thisLooksLikePersonalDataButItIsNot';
- $eventObj->skipUserOptions[] = 'thisIsAlsoNotPersonalDataPleaseIgnoreIt';
- }
-}
-```
-
-### `$data`
-
-Contains the entire data that will be included in the exported JSON file, some
-fields may already exist (such as `'com.woltlab.wcf'`) and while you may add or
-edit any fields within, you should restrict yourself to only append data from
-your plugin or app.
-
-### `$exportUserProperties`
-
-Only a whitelist of columns in `wcfN_user` is exported by default, if your plugin
-or app adds one or more columns to this table that do hold personal data, then
-you will have to append it to this array. The listed properties will always be
-included regardless of their content.
-
-### `$exportUserPropertiesIfNotEmpty`
-
-Only a whitelist of columns in `wcfN_user` is exported by default, if your plugin
-or app adds one or more columns to this table that do hold personal data, then
-you will have to append it to this array. Empty values will not be added to the
-output.
-
-### `$exportUserOptionSettings`
-
-Any user option that exists within a `settings.*` category is automatically
-excluded from the export, with the notable exception of the `timezone` option.
-You can opt-in to include your setting by appending to this array, if it contains
-any personal data. The listed settings are always included regardless of their
-content.
-
-### `$exportUserOptionSettingsIfNotEmpty`
-
-Any user option that exists within a `settings.*` category is automatically
-excluded from the export, with the notable exception of the `timezone` option.
-You can opt-in to include your setting by appending to this array, if it contains
-any personal data.
-
-### `$ipAddresses`
-
-List of database table names per package identifier that contain ip addresses.
-The contained ip addresses will be exported when the ip logging module is enabled.
-
-It expects the database table to use the column names `ipAddress`, `time` and
-`userID`. If your table does not match this pattern for whatever reason, you'll
-need to manually probe for `LOG_IP_ADDRESS` and then call `exportIpAddresses()`
-to retrieve the list. Afterwards you are responsible to append these ip addresses
-to the `$data` array to have it exported.
-
-### `$skipUserOptions`
-
-All user options are included in the export by default, unless they start with
-`can*` or `admin*`, or are blacklisted using this array. You should append any
-of your plugin's or app's user option that should not be exported, for example
-because it does not contain personal data, such as internal data.
-
-{% include links.html %}
+++ /dev/null
----
-title: Page Types
-sidebar: sidebar
-permalink: php_pages.html
-folder: php
----
-
-## AbstractPage
-
-The default implementation for pages to present any sort of content, but are designed to handle `GET` requests only. They usually follow a fixed method chain that will be invoked one after another, adding logical sections to the request flow.
-
-### Method Chain
-
-#### \__run()
-
-This is the only method being invoked from the outside and starts the whole chain.
-
-#### readParameters()
-
-Reads and sanitizes request parameters, this should be the only method to ever read user-supplied input. Read data should be stored in class properties to be accessible at a later point, allowing your code to safely assume that the data has been sanitized and is safe to work with.
-
-A typical example is the board page from the forum app that reads the id and attempts to identify the request forum.
-
-```php
-public function readParameters() {
- parent::readParameters();
-
- if (isset($_REQUEST['id'])) $this->boardID = intval($_REQUEST['id']);
- $this->board = BoardCache::getInstance()->getBoard($this->boardID);
- if ($this->board === null) {
- throw new IllegalLinkException();
- }
-
- // check permissions
- if (!$this->board->canEnter()) {
- throw new PermissionDeniedException();
- }
-}
-```
-
-<span class="label label-info">Events</span> `readParameters`
-
-#### show()
-
-Used to be the method of choice to handle permissions and module option checks, but has been used almost entirely as an internal method since the introduction of the properties `$loginRequired`, `$neededModules` and `$neededPermissions`.
-
-<span class="label label-info">Events</span> `checkModules`, `checkPermissions` and `show`
-
-#### readData()
-
-Central method for data retrieval based on class properties including those populated with user data in `readParameters()`. It is strongly recommended to use this method to read data in order to properly separate the business logic present in your class.
-
-<span class="label label-info">Events</span> `readData`
-
-#### assignVariables()
-
-Last method call before the template engine kicks in and renders the template. All though some properties are bound to the template automatically, you still need to pass any custom variables and class properties to the engine to make them available in templates.
-
-Following the example in `readParameters()`, the code below adds the board data to the template.
-
-```php
-public function assignVariables() {
- parent::assignVariables();
-
- WCF::getTPL()->assign([
- 'board' => $this->board,
- 'boardID' => $this->boardID
- ]);
-}
-```
-
-<span class="label label-info">Events</span> `assignVariables`
-
-## AbstractForm
-
-Extends the AbstractPage implementation with additional methods designed to handle form submissions properly.
-
-### Method Chain
-
-#### \__run()
-
-*Inherited from AbstractPage.*
-
-#### readParameters()
-
-*Inherited from AbstractPage.*
-
-#### show()
-
-*Inherited from AbstractPage.*
-
-#### submit()
-
-{% include callout.html content="The methods `submit()` up until `save()` are only invoked if either `$_POST` or `$_FILES` are not empty, otherwise they won't be invoked and the execution will continue with `readData()`." type="warning" %}
-
-This is an internal method that is responsible of input processing and validation.
-
-<span class="label label-info">Events</span> `submit`
-
-#### readFormParameters()
-
-This method is quite similar to `readParameters()` that is being called earlier, but is designed around reading form data submitted through POST requests. You should avoid accessing `$_GET` or `$_REQUEST` in this context to avoid mixing up parameters evaluated when retrieving the page on first load and when submitting to it.
-
-<span class="label label-info">Events</span> `readFormParameters`
-
-#### validate()
-
-Deals with input validation and automatically catches exceptions deriving from `wcf\system\exception\UserInputException`, resulting in a clean and consistent error handling for the user.
-
-<span class="label label-info">Events</span> `validate`
-
-#### save()
-
-Saves the processed data to database or any other source of your choice. Please keep in mind to invoke `$this->saved()` before resetting the form data.
-
-<span class="label label-info">Events</span> `save`
-
-#### saved()
-
-{% include callout.html content="This method is not called automatically and must be invoked manually by executing `$this->saved()` inside `save()`." type="warning" %}
-
-The only purpose of this method is to fire the event `saved` that signals that the form data has been processed successfully and data has been saved. It is somewhat special as it is dispatched after the data has been saved, but before the data is purged during form reset. This is by default the last event that has access to the processed data.
-
-<span class="label label-info">Events</span> `saved`
-
-#### readData()
-
-*Inherited from AbstractPage.*
-
-#### assignVariables()
-
-*Inherited from AbstractPage.*
+++ /dev/null
----
-title: "Tutorial Series Part 1: Base Structure"
-sidebar: sidebar
-permalink: tutorial_tutorial-series_part-1-base-structure.html
-folder: tutorial/tutorial-series
-parent: tutorial_tutorial-series
----
-
-In the first part of this tutorial series, we will lay out what the basic version of package should be able to do and how to implement these functions.
-
-
-## Package Functionality
-
-The package should provide the following possibilities/functions:
-
-- Sortable list of all people in the ACP
-- Ability to add, edit and delete people in the ACP
-- Restrict the ability to add, edit and delete people (in short: manage people) in the ACP
-- Sortable list of all people in the front end
-
-
-## Used Components
-
-We will use the following package installation plugins:
-
-- [acpTemplate package installation plugin](package_pip_acp-template.html),
-- [acpMenu package installation plugin](package_pip_acp-menu.html),
-- [file package installation plugin](package_pip_file.html),
-- [language package installation plugin](package_pip_language.html),
-- [menuItem package installation plugin](package_pip_menu-item.html),
-- [page package installation plugin](package_pip_page.html),
-- [sql package installation plugin](package_pip_sql.html),
-- [template package installation plugin](package_pip_template.html),
-- [userGroupOption package installation plugin](package_pip_user-group-option.html),
-
-use [database objects](php_database-objects.html), create [pages](php_pages.html) and use [templates](view_templates.html).
-
-
-## Package Structure
-
-The package will have the following file structure:
-
-```
-├── acpMenu.xml
-├── acptemplates
-│ ├── personAdd.tpl
-│ └── personList.tpl
-├── files
-│ └── lib
-│ ├── acp
-│ │ ├── form
-│ │ │ ├── PersonAddForm.class.php
-│ │ │ └── PersonEditForm.class.php
-│ │ └── page
-│ │ └── PersonListPage.class.php
-│ ├── data
-│ │ └── person
-│ │ ├── PersonAction.class.php
-│ │ ├── Person.class.php
-│ │ ├── PersonEditor.class.php
-│ │ └── PersonList.class.php
-│ └── page
-│ └── PersonListPage.class.php
-├── install.sql
-├── language
-│ ├── de.xml
-│ └── en.xml
-├── menuItem.xml
-├── package.xml
-├── page.xml
-├── templates
-│ └── personList.tpl
-└── userGroupOption.xml
-```
-
-
-## Person Modeling
-
-### Database Table
-
-As the first step, we have to model the people we want to manage with this package.
-As this is only an introductory tutorial, we will keep things simple and only consider the first and last name of a person.
-Thus, the database table we will store the people in only contains three columns:
-
-1. `personID` is the unique numeric identifier of each person created,
-1. `firstName` contains the first name of the person,
-1. `lastName` contains the last name of the person.
-
-The first file for our package is the `install.sql` file used to create such a database table during package installation:
-
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-1/install.sql %}
-{% endhighlight %}
-
-### Database Object
-
-#### `Person`
-
-In our PHP code, each person will be represented by an object of the following class:
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php %}
-{% endhighlight %}
-
-The important thing here is that `Person` extends `DatabaseObject`.
-Additionally, we implement the `IRouteController` interface, which allows us to use `Person` objects to create links, and we implement PHP's magic [__toString()](https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring) method for convenience.
-
-For every database object, you need to implement three additional classes:
-an action class, an editor class and a list class.
-
-#### `PersonAction`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php %}
-{% endhighlight %}
-
-This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
-This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
-`$permissionsDelete` has to be set to the permission needed in order to delete a person.
-We will later use the [userGroupOption package installation plugin](package_pip_user-group-option.html) to create the `admin.content.canManagePeople` permission.
-`$requireACP` restricts deletion of people to the ACP.
-
-#### `PersonEditor`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php %}
-{% endhighlight %}
-
-This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
-setting the static `$baseClass` property to the database object class name.
-
-#### `PersonList`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php %}
-{% endhighlight %}
-
-Due to the default implementation of `DatabaseObjectList`, our `PersonList` class just needs to extend it and everything else is either automatically set by the code of `DatabaseObjectList` or, in the case of properties and methods, provided by that class.
-
-
-## ACP
-
-Next, we will take care of the controllers and views for the ACP.
-In total, we need three each:
-
-1. page to list people,
-1. form to add people, and
-1. form to edit people.
-
-Before we create the controllers and views, let us first create the menu items for the pages in the ACP menu.
-
-### ACP Menu
-
-We need to create three menu items:
-
-1. a “parent” menu item on the second level of the ACP menu item tree,
-1. a third level menu item for the people list page, and
-1. a fourth level menu item for the form to add new people.
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/acpMenu.xml %}
-{% endhighlight %}
-
-We choose `wcf.acp.menu.link.content` as the parent menu item for the first menu item `wcf.acp.menu.link.person` because the people we are managing is just one form of content.
-The fourth level menu item `wcf.acp.menu.link.person.add` will only be shown as an icon and thus needs an additional element `icon` which takes a FontAwesome icon class as value.
-
-### People List
-
-To list the people in the ACP, we need a `PersonListPage` class and a `personList` template.
-
-#### `PersonListPage`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php %}
-{% endhighlight %}
-
-As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
-
-1. We need to set the active ACP menu item via the `$activeMenuItem`.
-1. `$neededPermissions` contains a list of permissions of which the user needs to have at least one in order to see the person list.
- We use the same permission for both the menu item and the page.
-1. The database object list class whose name is provided via `$objectListClassName` and that handles fetching the people from database is the `PersonList` class, which we have already created.
-1. To validate the sort field passed with the request, we set `$validSortFields` to the available database table columns.
-
-#### `personList.tpl`
-
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/acptemplates/personList.tpl %}
-{% endhighlight %}
-
-We will go piece by piece through the template code:
-
-1. We include the `header` template and set the page title `wcf.acp.person.list`.
- You have to include this template for every page!
-1. We set the content header and additional provide a button to create a new person in the content header navigation.
-1. As not all people are listed on the same page if many people have been created, we need a pagination for which we use the `pages` template plugin.
- The `{hascontent}{content}{/content}{/hascontent}` construct ensures the `.paginationTop` element is only shown if the `pages` template plugin has a return value, thus if a pagination is necessary.
-1. Now comes the main part of the page, the list of the people, which will only be displayed if any people exist.
- Otherwise, an info box is displayed using the generic `wcf.global.noItems` language item.
- The `$objects` template variable is automatically assigned by `wcf\page\MultipleLinkPage` and contains the `PersonList` object used to read the people from database.
-
- The table itself consists of a `thead` and a `tbody` element and is extendable with more columns using the template events `columnHeads` and `columns`.
- In general, every table should provide these events.
- The default structure of a table is used here so that the first column of the content rows contains icons to edit and to delete the row (and provides another standard event `rowButtons`) and that the second column contains the ID of the person.
- The table can be sorted by clicking on the head of each column.
- The used variables `$sortField` and `$sortOrder` are automatically assigned to the template by `SortablePage`.
-1. The `.contentFooter` element is only shown if people exist as it basically repeats the `.contentHeaderNavigation` and `.paginationTop` element.
-1. The JavaScript code here fulfills two duties:
- Handling clicks on the delete icons and forwarding the requests via AJAX to the `PersonAction` class, and setting up some code that triggers if all people shown on the current page are deleted via JavaScript to either reload the page or show the `wcf.global.noItems` info box.
-1. Lastly, the `footer` template is included that terminates the page.
- You also have to include this template for every page!
-
-Now, we have finished the page to manage the people so that we can move on to the forms with which we actually create and edit the people.
-
-### Person Add Form
-
-Like the person list, the form to add new people requires a controller class and a template.
-
-#### `PersonAddForm`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php %}
-{% endhighlight %}
-
-The properties here consist of two types:
-the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the “data” properties `$firstName` and `$lastName`, which will contain the data entered by the user of the person to be created.
-
-Now, let's go through each method in execution order:
-
-1. `readFormParameters()` is called after the form has been submitted and reads the entered first and last name and sanitizes the values by calling `StringUtil::trim()`.
-1. `validate()` is called after the form has been submitted and is used to validate the input data.
- In case of invalid data, the method is expected to throw a `UserInputException`.
- Here, the validation for first and last name is the same and quite basic:
- We check that any name has been entered and that it is not longer than the database table column permits.
-1. `save()` is called after the form has been submitted and the entered data has been validated and it creates the new person via `PersonAction`.
- Please note that we do not just pass the first and last name to the action object but merge them with the `$this->additionalFields` array which can be used by event listeners of plugins to add additional data.
- After creating the object, the `saved()` method is called which fires an event for plugins and the data properties are cleared so that the input fields on the page are empty so that another new person can be created.
- Lastly, a `success` variable is assigned to the template which will show a message that the person has been successfully created.
-1. `assignVariables()` assigns the values of the “data” properties to the template and additionally assigns an `action` variable.
- This `action` variable will be used in the template to distinguish between adding a new person and editing an existing person so that which minimal adjustments, we can use the template for both cases.
-
-#### `personAdd.tpl`
-
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl %}
-{% endhighlight %}
-
-We will now only concentrate on the new parts compared to `personList.tpl`:
-
-1. We use the `$action` variable to distinguish between the languages items used for adding a person and for creating a person.
-1. Including the `formError` template automatically shows an error message if the validation failed.
-1. The `.success` element is shown after successful saving the data and, again, shows different a text depending on the executed action.
-1. The main part is the `form` element which has a common structure you will find in many forms in WoltLab Suite Core.
- The notable parts here are:
- - The `action` attribute of the `form` element is set depending on which controller will handle the request.
- In the link for the edit controller, we can now simply pass the edited `Person` object directly as the `Person` class implements the `IRouteController` interface.
- - The field that caused the validation error can be accessed via `$errorField`.
- - The type of the validation error can be accessed via `$errorType`.
- For an empty input field, we show the generic `wcf.global.form.error.empty` language item.
- In all other cases, we use the error type to determine the object- and property-specific language item to show.
- The approach used here allows plugins to easily add further validation error messages by simply using a different error type and providing the associated language item.
- - Input fields can be grouped into different `.section` elements.
- At the end of each `.section` element, there should be an template event whose name ends with `Fields`.
- The first part of the event name should reflect the type of fields in the particular `.section` element.
- Here, the input fields are just general “data” fields so that the event is called `dataFields`.
- - After the last `.section` element, fire a `section` event so that plugins can add further sections.
- - Lastly, the `.formSubmit` shows the submit button and `{csrfToken}` contains a CSRF token that is automatically validated after the form is submitted.
-
-### Person Edit Form
-
-As mentioned before, for the form to edit existing people, we only need a new controller as the template has already been implemented in a way that it handles both, adding and editing.
-
-#### `PersonEditForm`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php %}
-{% endhighlight %}
-
-In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
-
-After setting a different active menu item, we declare two new properties for the edited person:
-the id of the person passed in the URL is stored in `$personID` and based on this ID, a `Person` object is created that is stored in the `$person` property.
-
-Now let use go through the different methods in chronological order again:
-
-1. `readParameters()` reads the passed ID of the edited person and creates a `Person` object based on this ID.
- If the ID is invalid, `$this->person->personID` is `null` and an `IllegalLinkException` is thrown.
-1. `readData()` only executes additional code in the case if `$_POST` is empty, thus only for the initial request before the form has been submitted.
- The data properties of `PersonAddForm` are populated with the data of the edited person so that this data is shown in the form for the initial request.
-1. `save()` handles saving the changed data.
-
- {% include callout.html content="Do not call `parent::save()` because that would cause `PersonAddForm::save()` to be executed and thus a new person would to be created! In order for the `save` event to be fired, call `AbstractForm::save()` instead!" type="warning" %}
-
- The only differences compared to `PersonAddForm::save()` are that we pass the edited object to the `PersonAction` constructor, execute the `update` action instead of the `create` action and do not clear the input fields after saving the changes.
-1. In `assignVariables()`, we assign the edited `Person` object to the template, which is required to create the link in the form’s action property.
- Furthermore, we assign the template variable `$action` `edit` as value.
-
- {% include callout.html content="After calling `parent::assignVariables()`, the template variable `$action` actually has the value `add` so that here, we are overwriting this already assigned value." type="info" %}
-
-
-## Frontend
-
-For the front end, that means the part with which the visitors of a website interact, we want to implement a simple sortable page that lists the people.
-This page should also be directly linked in the main menu.
-
-### `page.xml`
-
-First, let us register the page with the system because every front end page or form needs to be explicitly registered using the [page package installation plugin](package_pip_page.html):
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/page.xml %}
-{% endhighlight %}
-
-For more information about what each of the elements means, please refer to the [page package installation plugin page](package_pip_page.html).
-
-### `menuItem.xml`
-
-Next, we register the menu item using the [menuItem package installation plugin](package_pip_menuItem.html):
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/menuItem.xml %}
-{% endhighlight %}
-
-Here, the import parts are that we register the menu item for the main menu `com.woltlab.wcf.MainMenu` and link the menu item with the page `com.woltlab.wcf.people.PersonList`, which we just registered.
-
-### People List
-
-As in the ACP, we need a controller and a template.
-You might notice that both the controller’s (unqualified) class name and the template name are the same for the ACP and the front end.
-This is no problem because the qualified names of the classes differ and the files are stored in different directories and because the templates are installed by different package installation plugins and are also stored in different directories.
-
-#### `PersonListPage`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php %}
-{% endhighlight %}
-
-This class is almost identical to the ACP version.
-In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page.
-Furthermore, `$neededPermissions` has not been set because in the front end, users do not need any special permission to access the page.
-In the front end, we explicitly set the `$defaultSortField` so that the people listed on the page are sorted by their last name (in ascending order) by default.
-
-#### `personList.tpl`
-
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/templates/personList.tpl %}
-{% endhighlight %}
-
-If you compare this template to the one used in the ACP, you will recognize similar elements like the `.paginationTop` element, the `p.info` element if no people exist, and the `.contentFooter` element.
-Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
-
-Now, let us take a closer look at the differences:
-
-- We do not explicitly create a `.contentHeader` element but simply assign the title to the `contentTitle` variable.
- The value of the assignment is simply the title of the page and a badge showing the number of listed people.
- The `header` template that we include later will handle correctly displaying the content header on its own based on the `$contentTitle` variable.
-- Next, we create additional element for the HTML document’s `<head>` element.
- In this case, we define the [canonical link of the page](https://en.wikipedia.org/wiki/Canonical_link_element) and, because we are showing paginated content, add links to the previous and next page (if they exist).
-- We want the page to be sortable but as we will not be using a table for listing the people like in the ACP, we are not able to place links to sort the people into the table head.
- Instead, usually a box is created in the sidebar on the right-hand side that contains `select` elements to determine sort field and sort order.
-- The main part of the page is the listing of the people.
- We use a structure similar to the one used for displaying registered users.
- Here, for each person, we simply display a FontAwesome icon representing a person and show the person’s full name relying on `Person::__toString()`.
- Additionally, like in the user list, we provide the initially empty `ul.inlineList.commaSeparated` and `dl.plain.inlineDataList.small` elements that can be filled by plugins using the templates events.
-
-
-## `userGroupOption.xml`
-
-We have already used the `admin.content.canManagePeople` permissions several times, now we need to install it using the [userGroupOption package installation plugin](package_pip_user-group-option.html):
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/userGroupOption.xml %}
-{% endhighlight %}
-
-We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
-As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
-This permission is only relevant for registered users so that it should not be visible when editing the guest user group.
-This is achieved by setting `usersonly` to `1`.
-
-
-## `package.xml`
-
-Lastly, we need to create the `package.xml` file.
-For more information about this kind of file, please refer to [the `package.xml` page](package_package-xml.html).
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/package.xml %}
-{% endhighlight %}
-
-As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
-We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
-Additionally, we disallow installation of the package in the next major version `3.1` by excluding the `3.1.0 Alpha 1` version.
-This ensures that if changes from WoltLab Suite Core 3.0 to 3.1 require changing some parts of the package, it will not break the instance in which the package is installed.
-
-The most important part are to installation instructions.
-First, we install the ACP templates, files and templates, create the database table and import the language item.
-Afterwards, the ACP menu items and the permission are added.
-Now comes the part of the instructions where the order of the instructions is crucial:
-In `menuItem.xml`, we refer to the `com.woltlab.wcf.people.PersonList` page that is delivered by `page.xml`.
-As the menu item package installation plugin validates the given page and throws an exception if the page does not exist, we need to install the page before the menu item!
-
----
-
-This concludes the first part of our tutorial series after which you now have a working simple package with which you can manage people in the ACP and show the visitors of your website a simple list of all created people in the front end.
-
-The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-1).
+++ /dev/null
----
-title: "Part 2: Event Listeners and Template Listeners"
-sidebar: sidebar
-permalink: tutorial_tutorial-series_part-2-event-listeners-and-template-listeners.html
-folder: tutorial/tutorial-series
-parent: tutorial_tutorial-series
----
-
-In the [first part](tutorial_tutorial-series_part-1-base-structure.html) of this tutorial series, we have created the base structure of our people management package.
-In further parts, we will use the package of the first part as a basis to directly add new features.
-In order to explain how event listeners and template works, however, we will not directly adding a new feature to the package by altering it in this part, but we will assume that somebody else created the package and that we want to extend it the “correct” way by creating a plugin.
-
-The goal of the small plugin that will be created in this part is to add the birthday of the managed people.
-As in the first part, we will not bother with careful validation of the entered date but just make sure that it is a valid date.
-
-
-## Package Functionality
-
-The package should provide the following possibilities/functions:
-
-- List person’s birthday (if set) in people list in the ACP
-- Sort people list by birthday in the ACP
-- Add or remove birthday when adding or editing person
-- List person’s birthday (if set) in people list in the front end
-- Sort people list by birthday in the front end
-
-
-## Used Components
-
-We will use the following package installation plugins:
-
-- [acpTemplate package installation plugin](package_pip_acp-template.html),
-- [eventListener package installation plugin](package_pip_event-listener.html),
-- [file package installation plugin](package_pip_file.html),
-- [language package installation plugin](package_pip_language.html),
-- [sql package installation plugin](package_pip_sql.html),
-- [template package installation plugin](package_pip_template.html),
-- [templateListener package installation plugin](package_pip_template-listener.html).
-
-For more information about the event system, please refer to the [dedicated page on events](php_api_events.html).
-
-
-## Package Structure
-
-The package will have the following file structure:
-
-```
-├── acptemplates
-│ └── __personAddBirthday.tpl
-├── eventListener.xml
-├── files
-│ └── lib
-│ └── system
-│ └── event
-│ └── listener
-│ ├── BirthdayPersonAddFormListener.class.php
-│ └── BirthdaySortFieldPersonListPageListener.class.php
-├── install.sql
-├── language
-│ ├── de.xml
-│ └── en.xml
-├── package.xml
-├── templateListener.xml
-└── templates
- ├── __personListBirthday.tpl
- └── __personListBirthdaySortField.tpl
-```
-
-
-## Extending Person Model (`install.sql`)
-
-The existing model of a person only contains the person’s first name and their last name (in additional to the id used to identify created people).
-To add the birthday to the model, we need to create an additional database table column using the [sql package installation plugin](package_pip_sql.html):
-
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/install.sql %}
-{% endhighlight %}
-
-If we have a [Person object](tutorial_tutorial-series_part-1-base-structure.html#person), this new property can be accessed the same way as the `personID` property, the `firstName` property, or the `lastName` property from the base package: `$person->birthday`.
-
-
-## Setting Birthday in ACP
-
-To set the birthday of a person, we need to extend the `personAdd` template to add an additional birthday field.
-This can be achieved using the `dataFields` template event at whose position we inject the following template code:
-
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl %}
-{% endhighlight %}
-
-which we store in a `__personAddBirthday.tpl` template file.
-The used language item `wcf.person.birthday` is actually the only new one for this package:
-
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/language/de.xml %}
-{% endhighlight %}
-
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/language/en.xml %}
-{% endhighlight %}
-
-The template listener needs to be registered using the [templateListener package installation plugin](package_pip_template-listener.html).
-The corresponding complete `templateListener.xml` file is included [below](#templatelistenerxml).
-
-The template code alone is not sufficient because the `birthday` field is, at the moment, neither read, nor processed, nor saved by any PHP code.
-This can be be achieved, however, by adding event listeners to `PersonAddForm` and `PersonEditForm` which allow us to execute further code at specific location of the program.
-Before we take a look at the event listener code, we need to identify exactly which additional steps we need to undertake:
-
-1. If a person is edited and the form has not been submitted, the existing birthday of that person needs to be read.
-1. If a person is added or edited and the form has been submitted, the new birthday value needs to be read.
-1. If a person is added or edited and the form has been submitted, the new birthday value needs to be validated.
-1. If a person is added or edited and the new birthday value has been successfully validated, the new birthday value needs to be saved.
-1. If a person is added and the new birthday value has been successfully saved, the internally stored birthday needs to be reset so that the birthday field is empty when the form is shown again.
-1. The internally stored birthday value needs to be assigned to the template.
-
-The following event listeners achieves these requirements:
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php %}
-{% endhighlight %}
-
-Some notes on the code:
-
-- We are inheriting from `AbstractEventListener`, instead of just implementing the `IParameterizedEventListener` interface.
- The `execute()` method of `AbstractEventListener` contains a dispatcher that automatically calls methods called `on` followed by the event name with the first character uppercased, passing the event object and the `$parameters` array.
- This simple pattern results in the event `foo` being forwarded to the method `onFoo($eventObj, $parameters)`.
-- The `birthday` column has a default value of `0000-00-00`, which we interpret as “birthday not set”.
- To show an empty input field in this case, we empty the `birthday` property after reading such a value in `readData()`.
-- The validation of the date is, as mentioned before, very basic and just checks the form of the string and uses PHP’s [checkdate](https://secure.php.net/manual/en/function.checkdate.php) function to validate the components.
-- The `save` needs to make sure that the passed date is actually a valid date and set it to `0000-00-00` if no birthday is given.
- To actually save the birthday in the database, we do not directly manipulate the database but can add an additional field to the data array passed to `PersonAction::create()` via `AbstractForm::$additionalFields`.
- As the `save` event is the last event fired before the actual save process happens, this is the perfect event to set this array element.
-
-The event listeners are installed using the `eventListener.xml` file shown [below](#eventlistenerxml).
-
-
-## Adding Birthday Table Column in ACP
-
-To add a birthday column to the person list page in the ACP, we need three parts:
-
-1. an event listener that makes the `birthday` database table column a valid sort field,
-1. a template listener that adds the birthday column to the table’s head, and
-1. a template listener that adds the birthday column to the table’s rows.
-
-The first part is a very simple class:
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php %}
-{% endhighlight %}
-
-{% include callout.html content="We use `SortablePage` as a type hint instead of `wcf\acp\page\PersonListPage` because we will be using the same event listener class in the front end to also allow sorting that list by birthday." type="info" %}
-
-As the relevant template codes are only one line each, we will simply put them directly in the `templateListener.xml` file that will be shown [later on](#templatelistenerxml).
-The code for the table head is similar to the other `th` elements:
-
-```smarty
-<th class="columnDate columnBirthday{if $sortField == 'birthday'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=birthday&sortOrder={if $sortField == 'birthday' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.birthday{/lang}</a></th>
-```
-
-For the table body’s column, we need to make sure that the birthday is only show if it is actually set:
-
-```smarty
-<td class="columnDate columnBirthday">{if $person->birthday !== '0000-00-00'}{@$person->birthday|strtotime|date}{/if}</td>
-```
-
-
-## Adding Birthday in Front End
-
-In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person’s “statistics”.
-
-To add the birthday as a valid sort field, we use `BirthdaySortFieldPersonListPageListener` just as in the ACP.
-In the front end, we will now use a template (`__personListBirthdaySortField.tpl`) instead of a directly putting the template code in the `templateListener.xml` file:
-
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl %}
-{% endhighlight %}
-
-{% include callout.html content="You might have noticed the two underscores at the beginning of the template file. For templates that are included via template listeners, this is the naming convention we use." type="info" %}
-
-Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable.
-
-To show the birthday, we use the following template code for the `personStatistics` template event, which again makes sure that the birthday is only shown if it is actually set:
-
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl %}
-{% endhighlight %}
-
-
-## `templateListener.xml`
-
-The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/templateListener.xml %}
-{% endhighlight %}
-
-In cases where a template is used, we simply use the `include` syntax to load the template.
-
-
-## `eventListener.xml`
-
-There are two event listeners, `birthdaySortFieldAdminPersonList` and `birthdaySortFieldPersonList`, that make `birthday` a valid sort field in the ACP and the front end, respectively, and the rest takes care of setting the birthday.
-The event listener `birthdayPersonAddFormInherited` takes care of the events that are relevant for both adding and editing people, thus it listens to the `PersonAddForm` class but has `inherit` set to `1` so that it also listens to the events of the `PersonEditForm` class.
-In contrast, reading the existing birthday from a person is only relevant for editing so that the event listener `birthdayPersonEditForm` only listens to that class.
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/eventListener.xml %}
-{% endhighlight %}
-
-
-## `package.xml`
-
-The only relevant difference between the `package.xml` file of the base page from part 1 and the `package.xml` file of this package is that this package requires the base package `com.woltlab.wcf.people` (see `<requiredpackages>`):
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/package.xml %}
-{% endhighlight %}
-
----
-
-This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people.
-
-The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-2).
+++ /dev/null
----
-title: "Tutorial Series Part 3: Person Page and Comments"
-sidebar: sidebar
-permalink: tutorial_tutorial-series_part-3-person-page-and-comments.html
-folder: tutorial/tutorial-series
-parent: tutorial_tutorial-series
----
-
-In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details.
-To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite’s reusable comment functionality.
-
-
-## Package Functionality
-
-In addition to the existing functions from [part 1](tutorial_tutorial-series_part-1-base-structure.html), the package will provide the following possibilities/functions after this part of the tutorial:
-
-- Details page for each person linked in the front end person list
-- Comment on people on their respective page (can be disabled per person)
-- User online location for person details page with name and link to person details page
-- Create menu items linking to specific person details pages
-
-
-## Used Components
-
-In addition to the components used in [part 1](tutorial_tutorial-series_part-1-base-structure.html), we will use the [objectType package installation plugin](package_pip_object-type.html), use the [comment API](php_api_comments.html), create a [runtime cache](php_api_caches_runtime-caches.html), and create a page handler.
-
-
-## Package Structure
-
-The complete package will have the following file structure (including the files from [part 1](tutorial_tutorial-series_part-1-base-structure.html)):
-
-```
-├── acpMenu.xml
-├── acptemplates
-│ ├── personAdd.tpl
-│ └── personList.tpl
-├── files
-│ └── lib
-│ ├── acp
-│ │ ├── form
-│ │ │ ├── PersonAddForm.class.php
-│ │ │ └── PersonEditForm.class.php
-│ │ └── page
-│ │ └── PersonListPage.class.php
-│ ├── data
-│ │ └── person
-│ │ ├── Person.class.php
-│ │ ├── PersonAction.class.php
-│ │ ├── PersonEditor.class.php
-│ │ └── PersonList.class.php
-│ ├── page
-│ │ ├── PersonListPage.class.php
-│ │ └── PersonPage.class.php
-│ └── system
-│ ├── cache
-│ │ └── runtime
-│ │ └── PersonRuntimeCache.class.php
-│ ├── comment
-│ │ └── manager
-│ │ └── PersonCommentManager.class.php
-│ └── page
-│ └── handler
-│ └── PersonPageHandler.class.php
-├── install.sql
-├── language
-│ ├── de.xml
-│ └── en.xml
-├── menuItem.xml
-├── objectType.xml
-├── package.xml
-├── page.xml
-├── templates
-│ ├── person.tpl
-│ └── personList.tpl
-└── userGroupOption.xml
-```
-
-{% include callout.html content="We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new `Person::getLink()` method and new language items have been added. For all changes, please refer to the [source code on GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3)." type="warning" %}
-
-
-## Runtime Cache
-
-To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](php_api_caches_runtime-caches.html) for people:
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php %}
-{% endhighlight %}
-
-
-## Comments
-
-To allow users to comment on people, we need to tell the system that people support comments.
-This is done by registering a `com.woltlab.wcf.comment.commentableContent` object type whose processor implements [ICommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php):
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-3/objectType.xml %}
-{% endhighlight %}
-
-The `PersonCommentManager` class extended `ICommentManager`’s default implementation [AbstractCommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/AbstractCommentManager.class.php):
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php %}
-{% endhighlight %}
-
-- First, the system is told the names of the permissions via the `$permission*` properties.
- More information about comment permissions can be found [here](php_api_comments.html#user-group-options).
-- The `getLink()` method returns the link to the person with the passed comment id.
- As in `isAccessible()`, `PersonRuntimeCache` is used to potentially save database queries.
-- The `isAccessible()` method checks if the active user can access the relevant person.
- As we do not have any special restrictions for accessing people, we only need to check if the person exists.
-- The `getTitle()` method returns the title used for comments and responses, which is just a generic language item in this case.
-- The `updateCounter()` updates the comments’ counter of the person.
- We have added a new `comments` database table column to the `wcf1_person` database table in order to keep track on the number of comments.
-
-Additionally, we have added a new `enableComments` database table column to the `wcf1_person` database table whose value can be set when creating or editing a person in the ACP.
-With this option, comments on individual people can be disabled.
-
-{% include callout.html content="Liking comments is already built-in and only requires some extra code in the `PersonPage` class for showing the likes of pre-loaded comments." type="info" %}
-
-
-## Person Page
-
-### `PersonPage`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php %}
-{% endhighlight %}
-
-The `PersonPage` class is similar to the `PersonEditForm` in the ACP in that it reads the id of the requested person from the request data and validates the id in `readParameters()`.
-The rest of the code only handles fetching the list of comments on the requested person.
-In `readData()`, this list is fetched using `CommentHandler::getCommentList()` if comments are enabled for the person.
-The `assignVariables()` method assigns some additional template variables like `$commentCanAdd`, which is `1` if the active person can add comments and is `0` otherwise, `$lastCommentTime`, which contains the UNIX timestamp of the last comment, and `$likeData`, which contains data related to the likes for the disabled comments.
-
-### `person.tpl`
-
-{% highlight tpl %}
-{% include tutorial/tutorial-series/part-3/templates/person.tpl %}
-{% endhighlight %}
-
-For now, the `person` template is still very empty and only shows the comments in the content area.
-The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container `ul#personCommentList` element for the comments shown by `commentList` template.
-The `ul#personCommentList` elements has five additional `data-` attributes required by the JavaScript API for comments for loading more comments or creating new ones.
-The `commentListAddComment` template adds the WYSIWYG support.
-The attribute `wysiwygSelector` should be the id of the comment list `personCommentList` with an additional `AddComment` suffix.
-
-### `page.xml`
-
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-3/page.xml %}
-{% endhighlight %}
-
-The `page.xml` file has been extended for the new person page with identifier `com.woltlab.wcf.people.Person`.
-Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are four differences:
-
-1. It has a `<handler>` element with a class name as value.
- This aspect will be discussed in more detail in the next section.
-1. There are no `<content>` elements because, both, the title and the content of the page are dynamically generated in the template.
-1. The `<requireObjectID>` tells the system that this page requires an object id to properly work, in this case a valid person id.
-1. This page has a `<parent>` page, the person list page.
- In general, the details page for any type of object that is listed on a different page has the list page as its parent.
-
-### `PersonPageHandler`
-
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php %}
-{% endhighlight %}
-
-Like any page handler, the `PersonPageHandler` class has to implement the [IMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IMenuPageHandler.class.php) interface, which should be done by extending the [AbstractMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractMenuPageHandler.class.php) class.
-As we want administrators to link to specific people in menus, for example, we have to also implement the [ILookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/ILookupPageHandler.class.php) interface by extending the [AbstractLookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractLookupPageHandler.class.php) class.
-
-For the `ILookupPageHandler` interface, we need to implement three methods:
-
-1. `getLink($objectID)` returns the link to the person page with the given id.
- In this case, we simply delegate this method call to the `Person` object returned by `PersonRuntimeCache::getObject()`.
-1. `isValid($objectID)` returns `true` if the person with the given id exists, otherwise `false`.
- Here, we use `PersonRuntimeCache::getObject()` again and check if the return value is `null`, which is the case for non-existing people.
-1. `lookup($searchString)` is used when setting up an internal link and when searching for the linked person.
- This method simply searches the first and last name of the people and returns an array with the person data.
- While the `link`, the `objectID`, and the `title` element are self-explanatory, the `image` element can either contain an HTML `<img>` tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with `fa-`).
-
-Additionally, the class also implements [IOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IOnlineLocationPageHandler.class.php) which is used to determine the online location of users.
-To ensure upwards-compatibility if the `IOnlineLocationPageHandler` interface changes, the [TOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/TOnlineLocationPageHandler.class.php) trait is used.
-The `IOnlineLocationPageHandler` interface requires two methods to be implemented:
-
-1. `getOnlineLocation(Page $page, UserOnline $user)` returns the textual description of the online location.
- The language item for the user online locations should use the pattern `wcf.page.onlineLocation.{page identifier}`.
-1. `prepareOnlineLocation(Page $page, UserOnline $user)` is called for each user online before the `getOnlineLocation()` calls.
- In this case, calling `prepareOnlineLocation()` first enables us to add all relevant person ids to the person runtime cache so that for all `getOnlineLocation()` calls combined, only one database query is necessary to fetch all person objects.
-
----
-
-This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person.
-
-The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3).
-
+++ /dev/null
----
-title: Tutorial Series
-sidebar: sidebar
-permalink: tutorial_tutorial-series.html
-folder: tutorial
----
-
-In this tutorial series, we will code a package that allows administrators to create a registry of people.
-In this context, "people" does not refer to users registered on the website but anybody living, dead or fictional.
-
-We will start this tutorial series by creating a base structure for the package and then continue by adding further features step by step using different APIs.
-Note that in the context of this example, not every added feature might make perfect sense but the goal of this tutorial is not to create a useful package but to introduce you to WoltLab Suite.
-
-- [Part 1: Base Structure](tutorial_tutorial-series_part-1-base-structure.html)
-- [Part 2: Event Listeners and Template Listeners](tutorial_tutorial-series_part-2-event-listeners-and-template-listeners.html)
-- [Part 3: Person Page and Comments](tutorial_tutorial-series_part-3-person-page-and-comments.html)
+++ /dev/null
----
-title: CSS
-sidebar: sidebar
-permalink: view_css.html
-folder: view
----
-
-## SCSS and CSS
-
-SCSS is a scripting language that features a syntax similar to CSS and compiles into native CSS at runtime. It provides many great additions to CSS such as declaration nesting and variables, it is recommended to read the [official guide](http://sass-lang.com/guide) to learn more.
-
-You can create `.scss` files containing only pure CSS code and it will work just fine, you are at no point required to write actual SCSS code.
-
-### File Location
-
-Please place your style files in a subdirectory of the `style/` directory of the target application or the Core's style directory, for example `style/layout/pageHeader.scss`.
-
-### Variables
-
-You can access variables with `$myVariable`, variable interpolation (variables inside strings) is accomplished with `#{$myVariable}`.
-
-#### Linking images
-
-Images used within a style must be located in the style's image folder. To get the folder name within the CSS the SCSS variable `#{$style_image_path}` can be used. The value will contain a trailing slash.
-
-## Media Breakpoints
-
-Media breakpoints instruct the browser to apply different CSS depending on the viewport dimensions, e.g. serving a desktop PC a different view than when viewed on a smartphone.
-
-```scss
-/* red background color for desktop pc */
-@include screen-lg {
- body {
- background-color: red;
- }
-}
-
-/* green background color on smartphones and tablets */
-@include screen-md-down {
- body {
- background-color: green;
- }
-}
-```
-
-### Available Breakpoints
-
-{% include callout.html content="Some very large smartphones, for example the Apple iPhone 7 Plus, do match the media query for `Tablets (portrait)` when viewed in landscape mode." type="info" %}
-
-| Name | Devices | `@media` equivalent |
-|-------|-------|-------|
-| `screen-xs` | Smartphones only | `(max-width: 544px)` |
-| `screen-sm` | Tablets (portrait) | `(min-width: 545px) and (max-width: 768px)` |
-| `screen-sm-down` | Tablets (portrait) and smartphones | `(max-width: 768px)` |
-| `screen-sm-up` | Tablets and desktop PC | `(min-width: 545px)` |
-| `screen-sm-md` | Tablets only | `(min-width: 545px) and (max-width: 1024px)` |
-| `screen-md` | Tablets (landscape) | `(min-width: 769px) and (max-width: 1024px)` |
-| `screen-md-down` | Smartphones and Tablets | `(max-width: 1024px)` |
-| `screen-md-up` | Tablets (landscape) and desktop PC | `(min-width: 769px)` |
-| `screen-lg` | Desktop PC | `(min-width: 1025px)` |
+++ /dev/null
----
-title: Languages
-sidebar: sidebar
-permalink: view_languages.html
-folder: view
----
-
-WoltLab Suite offers full i18n support with its integrated language system,
-including but not limited to dynamic phrases using template scripting and the
-built-in support for right-to-left languages.
-
-Phrases are deployed using the [language][package_pip_language] package
-installation plugin, please also read the [naming conventions for language items](view_languages_naming-conventions.html).
-
-## Special Phrases
-
-### `wcf.date.dateFormat`
-
-{% include callout.html content="Many characters in the format have a special meaning and will be replaced with date fragments. If you want to include a literal character, you'll have to use the backslash `\` as an escape sequence to indicate that the character should be output as-is rather than being replaced. For example, `Y-m-d` will be output as `2018-03-30`, but `\Y-m-d` will result in `Y-03-30`." type="warning" %}
-
-_Defaults to `M jS Y`._
-
-The date format without time using PHP's format characters for the
-[`date()`](https://secure.php.net/manual/en/function.date.php) function. This
-value is also used inside the JavaScript implementation, where the characters
-are mapped to an equivalent representation.
-
-### `wcf.date.timeFormat`
-
-_Defaults to `g:i a`._
-
-The date format that is used to represent a time, but not a date. Please see the
-explanation on `wcf.date.dateFormat` to learn more about the format characters.
-
-### `wcf.date.firstDayOfTheWeek`
-
-_Defaults to `0`._
-
-Sets the first day of the week:
-* `0` - Sunday
-* `1` - Monday
-
-### `wcf.global.pageDirection` - RTL support
-
-_Defaults to `ltr`._
-
-Changing this value to `rtl` will reverse the page direction and enable the
-right-to-left support for phrases. Additionally, a special version of the
-stylesheet is loaded that contains all necessary adjustments for the reverse
-direction.
-
-{% include links.html %}
+++ /dev/null
----
-title: Language Naming Conventions
-sidebar: sidebar
-permalink: view_languages_naming-conventions.html
-folder: view
-parent: view_languages
----
-
-This page contains general rules for naming language items and for their values.
-API-specific rules are listed on the relevant API page:
-
-- [Comments](php_api_comments.html#language-items)
-
-
-## Forms
-
-### Fields
-
-If you have an application `foo` and a database object `foo\data\bar\Bar` with a property `baz` that can be set via a form field, the name of the corresponding language item has to be `foo.bar.baz`.
-If you want to add an additional description below the field, use the language item `foo.bar.baz.description`.
-
-### Error Texts
-
-If an error of type `{error type}` for the previously mentioned form field occurs during validation, you have to use the language item `foo.bar.baz.error.{error type}` for the language item describing the error.
-
-Exception to this rule:
-There are several general error messages like `wcf.global.form.error.empty` that have to be used for general errors like an empty field that may not be empty to avoid duplication of the same error message text over and over again in different language items.
-
-#### Naming Conventions
-
-- If the entered text does not conform to some special rules, i.e. if the text is invalid, use `invalid` as error type.
-- If the entered text is required to be unique but is already used for another object, use `notUnique` as error type.
-
-
-## Confirmation messages
-
-If the language item for an action is `foo.bar.action`, the language item for the confirmation message has to be `foo.bar.action.confirmMessage` instead of `foo.bar.action.sure` which is still used by some older language items.
-
-### Type-Specific Deletion Confirmation Message
-
-#### German
-
-```
-{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} wirklich löschen?
-```
-
-Example:
-
-```
-{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Icon wirklich löschen?
-```
-
-#### English
-
-```
-Do you really want delete the {element type}?
-```
-
-Example:
-
-```
-Do you really want delete the icon?
-```
-
-### Object-Specific Deletion Confirmation Message
-
-#### German
-
-```
-{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} <span class="confirmationObject">{object name}</span> wirklich löschen?
-```
-
-Example:
-
-```
-{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Artikel <span class="confirmationObject">{$article->getTitle()}</span> wirklich löschen?
-```
-
-#### English
-
-```
-Do you really want to delete the {element type} <span class="confirmationObject">{object name}</span>?
-```
-
-Example:
-
-```
-Do you really want to delete the article <span class="confirmationObject">{$article->getTitle()}</span>?
-```
-
-
-## User Group Options
-
-### Comments
-
-#### German
-
-| group type | action | example permission name | language item |
-| ---------- | ------ | ----------------------- | ------------- |
-| user | adding | `user.foo.canAddComment` | `Kann Kommentare erstellen` |
-| user | deleting | `user.foo.canDeleteComment` | `Kann eigene Kommentare löschen` |
-| user | editing | `user.foo.canEditComment` | `Kann eigene Kommentare bearbeiten` |
-| moderator | deleting | `mod.foo.canDeleteComment` | `Kann Kommentare löschen` |
-| moderator | editing | `mod.foo.canEditComment` | `Kann Kommentare bearbeiten` |
-| moderator | moderating | `mod.foo.canModerateComment` | `Kann Kommentare moderieren` |
-
-#### English
-
-| group type | action | example permission name | language item |
-| ---------- | ------ | ----------------------- | ------------- |
-| user | adding | `user.foo.canAddComment` | `Can create comments` |
-| user | deleting | `user.foo.canDeleteComment` | `Can delete their comments` |
-| user | editing | `user.foo.canEditComment` | `Can edit their comments` |
-| moderator | deleting | `mod.foo.canDeleteComment` | `Can delete comments` |
-| moderator | editing | `mod.foo.canEditComment` | `Can edit comments` |
-| moderator | moderating | `mod.foo.canModerateComment` | `Can moderate comments` |
+++ /dev/null
----
-title: Template Plugins
-sidebar: sidebar
-permalink: view_template-plugins.html
-folder: view
-parent: view_templates
----
-
-## <span class="label label-info">5.3+</span> `anchor`
-
-The `anchor` template plugin creates `a` HTML elements.
-The easiest way to use the template plugin is to pass it an instance of `ITitledLinkObject`:
-
-```smarty
-{anchor object=$object}
-```
-
-generates the same output as
-
-```smarty
-<a href="{$object->getLink()}">{$object->getTitle()}</a>
-```
-
-Instead of an `object` parameter, a `link` and `content` parameter can be used:
-
-```smarty
-{anchor link=$linkObject content=$content}
-```
-
-where `$linkObject` implements `ILinkableObject` and `$content` is either an object implementing `ITitledObject` or having a `__toString()` method or `$content` is a string or a number.
-
-The last special attribute is `append` whose contents are appended to the `href` attribute of the generated anchor element.
-
-All of the other attributes matching `~^[a-z]+([A-z]+)+$~`, expect for `href` which is disallowed, are added as attributes to the anchor element.
-
-If an `object` attribute is present, the object also implements `IPopoverObject` and if the return value of `IPopoverObject::getPopoverLinkClass()` is included in the `class` attribute of the `anchor` tag, `data-object-id` is automatically added.
-This functionality makes it easy to generate links with popover support.
-Instead of
-
-```smarty
-<a href="{$entry->getLink()}" class="blogEntryLink" data-object-id="{@$entry->entryID}">{$entry->subject}</a>
-```
-
-using
-
-```smarty
-{anchor object=$entry class='blogEntryLink'}
-```
-
-is sufficient if `Entry::getPopoverLinkClass()` returns `blogEntryLink`.
-
-## <span class="label label-info">5.3+</span> `anchorAttributes`
-
-`anchorAttributes` compliments the `StringUtil::getAnchorTagAttributes(string, bool): string` method.
-It allows to easily generate the necessary attributes for an anchor tag based off the destination URL.
-
-```smarty
-<a href="https://www.example.com" {anchorAttributes url='https://www.example.com' appendHref=false appendClassname=true isUgc=true}>
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `url` | destination URL |
-| `appendHref` | whether the `href` attribute should be generated; `true` by default |
-| `isUgc` | whether the `rel="ugc"` attribute should be generated; `false` by default |
-| `appendClassname` | whether the `class="externalURL"` attribute should be generated; `true` by default |
-
-## `append`
-
-If a string should be appended to the value of a variable, `append` can be used:
-
-```smarty
-{assign var=templateVariable value='newValue'}
-
-{$templateVariable} {* prints 'newValue *}
-
-{append var=templateVariable value='2'}
-
-{$templateVariable} {* now prints 'newValue2 *}
-```
-
-If the variables does not exist yet, `append` creates a new one with the given value.
-If `append` is used on an array as the variable, the value is appended to all elements of the array.
-
-
-## `assign`
-
-New template variables can be declared and new values can be assigned to existing template variables using `assign`:
-
-```smarty
-{assign var=templateVariable value='newValue'}
-
-{$templateVariable} {* prints 'newValue *}
-```
-
-
-## `capture`
-
-In some situations, `assign` is not sufficient to assign values to variables in templates if the value is complex.
-Instead, `capture` can be used:
-
-```smarty
-{capture var=templateVariable}
- {if $foo}
- <p>{$bar}</p>
- {else}
- <small>{$baz}</small>
- {/if}
-{/capture}
-```
-
-
-## `concat`
-
-`concat` is a modifier used to concatenate multiple strings:
-
-```smarty
-{assign var=foo value='foo'}
-
-{assign var=templateVariable value='bar'|concat:$foo}
-
-{$templateVariable} {* prints 'foobar *}
-```
-
-
-## `counter`
-
-`counter` can be used to generate and optionally print a counter:
-
-```smarty
-{counter name=fooCounter print=true} {* prints '1' *}
-
-{counter name=fooCounter print=true} {* prints '2' now *}
-
-{counter name=fooCounter} {* prints nothing, but counter value is '3' now internally *}
-
-{counter name=fooCounter print=true} {* prints '4' *}
-```
-
-Counter supports the following attributes:
-
-| Attribute | Description |
-|-----------|-------------|
-| `assign` | optional name of the template variable the current counter value is assigned to |
-| `direction` | counting direction, either `up` or `down`; `up` by default |
-| `name` | name of the counter, relevant if multiple counters are used simultaneously |
-| `print` | if `true`, the current counter value is printed; `false` by default |
-| `skip` | positive counting increment; `1` by default |
-| `start` | start counter value; `1` by default |
-
-
-## <span class="label label-info">5.4+</span> `csrfToken`
-
-`{csrfToken}` prints out the session's CSRF token (“Security Token”).
-
-```smarty
-<form action="{link controller="Foo"}{/link}" method="post">
- {* snip *}
-
- {csrfToken}
-</form>
-```
-
-The `{csrfToken}` template plugin supports a `type` parameter.
-Specifying this parameter might be required in rare situations.
-Please [check the implementation](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/template/plugin/CsrfTokenFunctionTemplatePlugin.class.php) for details.
-
-## `currency`
-
-`currency` is a modifier used to format currency values with two decimals using language dependent thousands separators and decimal point:
-
-```smarty
-{assign var=currencyValue value=12.345}
-
-{$currencyValue|currency} {* prints '12.34' *}
-```
-
-
-## `cycle`
-
-`cycle` can be used to cycle between different values:
-
-```smarty
-{cycle name=fooCycle values='bar,baz'} {* prints 'bar' *}
-
-{cycle name=fooCycle} {* prints 'baz' *}
-
-{cycle name=fooCycle advance=false} {* prints 'baz' again *}
-
-{cycle name=fooCycle} {* prints 'bar' *}
-```
-
-{% include callout.html content="The values attribute only has to be present for the first call. If `cycle` is used in a loop, the presence of the same values in consecutive calls has no effect. Only once the values change, the cycle is reset." type="info" %}
-
-| Attribute | Description |
-|-----------|-------------|
-| `advance` | if `true`, the current cycle value is advanced to the next value; `true` by default |
-| `assign` | optional name of the template variable the current cycle value is assigned to; if used, `print` is set to `false` |
-| `delimiter` | delimiter between the different cycle values; `,` by default |
-| `name` | name of the cycle, relevant if multiple cycles are used simultaneously |
-| `print` | if `true`, the current cycle value is printed, `false` by default |
-| `reset` | if `true`, the current cycle value is set to the first value, `false` by default |
-| `values` | string containing the different cycles values, also see `delimiter` |
-
-
-## `date`
-
-`date` generated a formatted date using `wcf\util\DateUtil::format()` with `DateUtil::DATE_FORMAT` internally.
-
-```smarty
-{$timestamp|date}
-```
-
-
-## <span class="label label-info">3.1+</span> `dateInterval`
-
-`dateInterval` calculates the difference between two unix timestamps and generated a textual date interval.
-
-```smarty
-{dateInterval start=$startTimestamp end=$endTimestamp full=true format='sentence'}
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `end` | end of the time interval; current timestamp by default (though either `start` or `end` has to be set) |
-| `format` | output format, either `default`, `sentence`, or `plain`; defaults to `default`, see `wcf\util\DateUtil::FORMAT_*` constants |
-| `full` | if `true`, full difference in minutes is shown; if `false`, only the longest time interval is shown; `false` by default |
-| `start` | start of the time interval; current timestamp by default (though either `start` or `end` has to be set) |
-
-
-## `encodeJS`
-
-`encodeJS` encodes a string to be used as a single-quoted string in JavaScript by replacing `\\` with `\\\\`, `'` with `\'`, linebreaks with `\n`, and `/` with `\/`.
-
-```smarty
-<script>
- var foo = '{@$foo|encodeJS}';
-</script>
-```
-
-
-## `encodeJSON`
-
-`encodeJSON` encodes a JSON string to be used as a single-quoted string in JavaScript by replacing `\\` with `\\\\`, `'` with `'`, linebreaks with `\n`, and `/` with `\/`.
-Additionally, `htmlspecialchars` is applied to the string.
-
-```smarty
-'{@$foo|encodeJSON}'
-```
-
-
-## `escapeCDATA`
-
-`escapeCDATA` encodes a string to be used in a `CDATA` element by replacing `]]>` with `]]]]><![CDATA[>`.
-
-```smarty
-<![CDATA[{@$foo|encodeCDATA}]]>
-```
-
-
-## `event`
-
-`event` provides extension points in templates that [template listeners](package_pip_template-listener.html) can use.
-
-```smarty
-{event name='foo'}
-```
-
-
-## `fetch`
-
-`fetch` fetches the contents of a file using `file_get_contents`.
-
-```smarty
-{fetch file='foo.html'} {* prints the contents of `foo.html` *}
-
-{fetch file='bar.html' assign=bar} {* assigns the contents of `foo.html` to `$bar`; does not print the contents *}
-```
-
-
-## `filesizeBinary`
-
-`filesizeBinary` formats the filesize using binary filesize (in bytes).
-
-```smarty
-{$filesize|filesizeBinary}
-```
-
-
-## `filesize`
-
-`filesize` formats the filesize using filesize (in bytes).
-
-```smarty
-{$filesize|filesize}
-```
-
-
-## `hascontent`
-
-In many cases, conditional statements can be used to determine if a certain section of a template is shown:
-
-```smarty
-{if $foo === 'bar'}
- only shown if $foo is bar
-{/if}
-```
-
-In some situations, however, such conditional statements are not sufficient.
-One prominent example is a template event:
-
-```smarty
-{if $foo === 'bar'}
- <ul>
- {if $foo === 'bar'}
- <li>Bar</li>
- {/if}
-
- {event name='listItems'}
- </li>
-{/if}
-```
-
-In this example, if `$foo !== 'bar'`, the list will not be shown, regardless of the additional template code provided by template listeners.
-In such a situation, `hascontent` has to be used:
-
-```smarty
-{hascontent}
- <ul>
- {content}
- {if $foo === 'bar'}
- <li>Bar</li>
- {/if}
-
- {event name='listItems'}
- {/content}
- </ul>
-{/hascontent}
-```
-
-If the part of the template wrapped in the `content` tags has any (trimmed) content, the part of the template wrapped by `hascontent` tags is shown (including the part wrapped by the `content` tags), otherwise nothing is shown.
-Thus, this construct avoids an empty list compared to the `if` solution above.
-
-Like `foreach`, `hascontent` also supports an `else` part:
-
-```smarty
-{hascontent}
- <ul>
- {content}
- {* … *}
- {/content}
- </ul>
-{hascontentelse}
- no list
-{/hascontent}
-```
-
-
-## `htmlCheckboxes`
-
-`htmlCheckboxes` generates a list of HTML checkboxes.
-
-```smarty
-{htmlCheckboxes name=foo options=$fooOptions selected=$currentFoo}
-
-{htmlCheckboxes name=bar output=$barLabels values=$barValues selected=$currentBar}
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| <span class="label label-info">5.2+</span> `disabled` | if `true`, all checkboxes are disabled |
-| `disableEncoding` | if `true`, the values are not passed through `wcf\util\StringUtil::encodeHTML()`; `false` by default |
-| `name` | `name` attribute of the `input` checkbox element |
-| `output` | array used as keys and values for `options` if present; not present by default |
-| `options` | array selectable options with the key used as `value` attribute and the value as the checkbox label |
-| `selected` | current selected value(s) |
-| `separator` | separator between the different checkboxes in the generated output; empty string by default |
-| `values` | array with values used in combination with `output`, where `output` is only used as keys for `options` |
-
-
-## `htmlOptions`
-
-`htmlOptions` generates an `select` HTML element.
-
-```smarty
-{htmlOptions name='foo' options=$options selected=$selected}
-
-<select name="bar">
- <option value=""{if !$selected} selected{/if}>{lang}foo.bar.default{/lang}</option>
- {htmlOptions options=$options selected=$selected} {* no `name` attribute *}
-</select>
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `disableEncoding` | if `true`, the values are not passed through `wcf\util\StringUtil::encodeHTML()`; `false` by default |
-| `object` | optional instance of `wcf\data\DatabaseObjectList` that provides the selectable options (overwrites `options` attribute internally) |
-| `name` | `name` attribute of the `select` element; if not present, only the <strong>contents</strong> of the `select` element are printed |
-| `output` | array used as keys and values for `options` if present; not present by default |
-| `values` | array with values used in combination with `output`, where `output` is only used as keys for `options` |
-| `options` | array selectable options with the key used as `value` attribute and the value as the option label; if a value is an array, an `optgroup` is generated with the array key as the `optgroup` label |
-| `selected` | current selected value(s) |
-
-All additional attributes are added as attributes of the `select` HTML element.
-
-
-## `implode`
-
-`implodes` transforms an array into a string and prints it.
-
-```smarty
-{implode from=$array key=key item=item glue=";"}{$key}: {$value}{/implode}
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `from` | array with the imploded values |
-| `glue` | separator between the different array values; `', '` by default |
-| `item` | template variable name where the current array value is stored during the iteration |
-| `key` | optional template variable name where the current array key is stored during the iteration |
-
-
-## <span class="label label-info">5.2+</span> `ipSearch`
-
-`ipSearch` generates a link to search for an IP address.
-
-```smarty
-{"127.0.0.1"|ipSearch}
-```
-
-
-## <span class="label label-info">3.0+</span> `js`
-
-`js` generates script tags based on whether `ENABLE_DEBUG_MODE` and `VISITOR_USE_TINY_BUILD` are enabled.
-
-```smarty
-{js application='wbb' file='WBB'} {* generates 'http://example.com/js/WBB.js' *}
-
-{js application='wcf' file='WCF.Like' bundle='WCF.Combined'}
- {* generates 'http://example.com/wcf/js/WCF.Like.js' if ENABLE_DEBUG_MODE=1 *}
- {* generates 'http://example.com/wcf/js/WCF.Combined.min.js' if ENABLE_DEBUG_MODE=0 *}
-
-{js application='wcf' lib='jquery'}
- {* generates 'http://example.com/wcf/js/3rdParty/jquery.js' *}
-
-{js application='wcf' lib='jquery-ui' file='awesomeWidget'}
- {* generates 'http://example.com/wcf/js/3rdParty/jquery-ui/awesomeWidget.js' *}
-
-{js application='wcf' file='WCF.Like' bundle='WCF.Combined' hasTiny=true}
- {* generates 'http://example.com/wcf/js/WCF.Like.js' if ENABLE_DEBUG_MODE=1 *}
- {* generates 'http://example.com/wcf/js/WCF.Combined.min.js' (ENABLE_DEBUG_MODE=0 *}
- {* generates 'http://example.com/wcf/js/WCF.Combined.tiny.min.js' if ENABLE_DEBUG_MODE=0 and VISITOR_USE_TINY_BUILD=1 *}
-```
-
-
-## <span class="label label-info">5.3+</span> `jslang`
-
-`jslang` works like [`lang`](#lang) with the difference that the resulting string is automatically passed through [`encodeJS`](#encodejs).
-
-```smarty
-require(['Language', /* … */], function(Language, /* … */) {
- Language.addObject({
- 'app.foo.bar': '{jslang}app.foo.bar{/jslang}',
- });
-
- // …
-});
-```
-
-
-## `lang`
-
-`lang` replaces a language items with its value.
-
-```smarty
-{lang}foo.bar.baz{/lang}
-
-{lang __literal=true}foo.bar.baz{/lang}
-
-{lang foo='baz'}foo.bar.baz{/lang}
-
-{lang}foo.bar.baz.{$action}{/lang}
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `__encode` | if `true`, the output will be passed through `StringUtil::encodeHTML()` |
-| `__literal` | if `true`, template variables will not resolved but printed as they are in the language item; `false` by default |
-| `__optional` | if `true` and the language item does not exist, an empty string is printed; `false` by default |
-
-All additional attributes are available when parsing the language item.
-
-
-## `language`
-
-`language` replaces a language items with its value.
-If the template variable `__language` exists, this language object will be used instead of `WCF::getLanguage()`.
-This modifier is useful when assigning the value directly to a variable.
-
-```smarty
-{$languageItem|language}
-
-{assign var=foo value=$languageItem|language}
-```
-
-
-## `link`
-
-`link` generates internal links using `LinkHandler`.
-
-```smarty
-<a href="{link controller='FooList' application='bar'}param1=2¶m2=A{/link}">Foo</a>
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `application` | abbreviation of the application the controller belongs to; `wcf` by default |
-| `controller` | name of the controller; if not present, the landing page is linked in the frontend and the index page in the ACP |
-| `encode` | if `true`, the generated link is passed through `wcf\util\StringUtil::encodeHTML()`; `true` by default |
-| `isEmail` | sets `encode=false` and forces links to link to the frontend |
-
-Additional attributes are passed to `LinkHandler::getLink()`.
-
-
-## `newlineToBreak`
-
-`newlineToBreak` transforms newlines into HTML `<br>` elements after encoding the content via `wcf\util\StringUtil::encodeHTML()`.
-
-```smarty
-{$foo|newlineToBreak}
-```
-
-
-## <span class="label label-info">3.0+</span> `page`
-
-`page` generates an internal link to a CMS page.
-
-```smarty
-{page}com.woltlab.wcf.CookiePolicy{/page}
-
-{page pageID=1}{/page}
-
-{page language='de'}com.woltlab.wcf.CookiePolicy{/page}
-
-{page languageID=2}com.woltlab.wcf.CookiePolicy{/page}
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `pageID` | unique id of the page (cannot be used together with a page identifier as value) |
-| `languageID` | id of the page language (cannot be used together with `language`) |
-| `language` | language code of the page language (cannot be used together with `languageID`) |
-
-
-## `pages`
-
-`pages` generates a pagination.
-
-```smarty
-{pages controller='FooList' link="pageNo=%d" print=true assign=pagesLinks} {* prints pagination *}
-
-{@$pagesLinks} {* prints same pagination again *}
-```
-
-| Attribute | Description |
-|-----------|-------------|
-| `assign` | optional name of the template variable the pagination is assigned to |
-| `controller` | controller name of the generated links |
-| `link` | additional link parameter where `%d` will be replaced with the relevant page number |
-| `pages` | maximum number of of pages; by default, the template variable `$pages` is used |
-| `print` | if `false` and `assign=true`, the pagination is not printed |
-| `application`, `id`, `object`, `title` | additional parameters passed to `LinkHandler::getLink()` to generate page links |
-
-
-## `plainTime`
-
-`plainTime` formats a timestamp to include year, month, day, hour, and minutes.
-The exact formatting depends on the current language (via the language items `wcf.date.dateTimeFormat`, `wcf.date.dateFormat`, and `wcf.date.timeFormat`).
-
-```smarty
-{$timestamp|plainTime}
-```
-
-
-## <span class="label label-info">5.3+</span> `plural`
-
-`plural` allows to easily select the correct plural form of a phrase based on a given `value`.
-The pluralization logic follows the [Unicode Language Plural Rules](https://unicode-org.github.io/cldr-staging/charts/37/supplemental/language_plural_rules.html) for cardinal numbers.
-
-The `#` placeholder within the resulting phrase is replaced by the `value`.
-It is automatically formatted using `StringUtil::formatNumeric`.
-
-
-
-English:
-
-Note the use of `1` if the number (`#`) is not used within the phrase and the use of `one` otherwise.
-They are equivalent for English, but following this rule generalizes better to other languages, helping the translator.
-```smarty
-{assign var=numberOfWorlds value=2}
-<h1>Hello {plural value=$numberOfWorlds 1='World' other='Worlds'}!</h1>
-<p>There {plural value=$numberOfWorlds 1='is one world' other='are # worlds'}!</p>
-<p>There {plural value=$numberOfWorlds one='is # world' other='are # worlds'}!</p>
-```
-
-German:
-```smarty
-{assign var=numberOfWorlds value=2}
-<h1>Hallo {plural value=$numberOfWorlds 1='Welt' other='Welten'}!</h1>
-<p>Es gibt {plural value=$numberOfWorlds 1='eine Welt' other='# Welten'}!</p>
-<p>Es gibt {plural value=$numberOfWorlds one='# Welt' other='# Welten'}!</p>
-```
-
-Romanian:
-
-Note the additional use of `few` which is not required in English or German.
-```smarty
-{assign var=numberOfWorlds value=2}
-<h1>Salut {plural value=$numberOfWorlds 1='lume' other='lumi'}!</h1>
-<p>Există {plural value=$numberOfWorlds 1='o lume' few='# lumi' other='# de lumi'}!</p>
-<p>Există {plural value=$numberOfWorlds one='# lume' few='# lumi' other='# de lumi'}!</p>
-```
-
-Russian:
-
-Note the difference between `1` (exactly `1`) and `one` (ending in `1`, except ending in `11`).
-```smarty
-{assign var=numberOfWorlds value=2}
-<h1>Привет {plural value=$numberOfWorld 1='мир' other='миры'}!</h1>
-<p>Есть {plural value=$numberOfWorlds 1='мир' one='# мир' few='# мира' many='# миров' other='# миров'}!</p>
-```
-
-
-| Attribute | Description |
-|-----------|-------------|
-| value | The value that is used to select the proper phrase. |
-| other | The phrase that is used when no other selector matches. |
-| Any Category Name | The phrase that is used when `value` belongs to the named category. Available categories depend on the language. |
-| Any Integer | The phrase that is used when `value` is that exact integer. |
-
-## `prepend`
-
-If a string should be prepended to the value of a variable, `prepend` can be used:
-
-```smarty
-{assign var=templateVariable value='newValue'}
-
-{$templateVariable} {* prints 'newValue *}
-
-{prepend var=templateVariable value='2'}
-
-{$templateVariable} {* now prints '2newValue' *}
-```
-
-If the variables does not exist yet, `prepend` creates a new one with the given value.
-If `prepend` is used on an array as the variable, the value is prepended to all elements of the array.
-
-
-## `shortUnit`
-
-`shortUnit` shortens numbers larger than 1000 by using unit suffixes:
-
-```smarty
-{10000|shortUnit} {* prints 10k *}
-{5400000|shortUnit} {* prints 5.4M *}
-```
-
-
-## `smallpages`
-
-`smallpages` generates a smaller version of `pages` by using adding the `small` CSS class to the generated `<nav>` element and only showing 7 instead of 9 links.
-
-
-## `tableWordwrap`
-
-`tableWordwrap` inserts zero width spaces every 30 characters in words longer than 30 characters.
-
-```smarty
-{$foo|tableWordwrap}
-```
-
-
-## `time`
-
-`time` generates an HTML `time` elements based on a timestamp that shows a relative time or the absolute time if the timestamp more than six days ago.
-
-```smarty
-{$timestamp|time} {* prints a '<time>' element *}
-```
-
-
-## `truncate`
-
-`truncate` truncates a long string into a shorter one:
-
-```smarty
-{$foo|truncate:35}
-
-{$foo|truncate:35:'_':true}
-```
-
-
-| Parameter Number | Description |
-|-----------|-------------|
-| 0 | truncated string |
-| 1 | truncated length; `80` by default |
-| 2 | ellipsis symbol; `wcf\util\StringUtil::HELLIP` by default |
-| 3 | if `true`, words can be broken up in the middle; `false` by default |
-
-
-## <span class="label label-info">5.3+</span> `user`
-
-`user` generates links to user profiles.
-The mandatory `object` parameter requires an instances of `UserProfile`.
-The optional `type` parameter is responsible for what the generated link contains:
-
-- `type='default'` (also applies if no `type` is given) outputs the formatted username relying on the “User Marking” setting of the relevant user group.
- Additionally, the user popover card will be shown when hovering over the generated link.
-- `type='plain'` outputs the username without additional formatting.
-- `type='avatar(\d+)'` outputs the user’s avatar in the specified size, i.e., `avatar48` outputs the avatar with a width and height of 48 pixels.
-
-The last special attribute is `append` whose contents are appended to the `href` attribute of the generated anchor element.
-
-All of the other attributes matching `~^[a-z]+([A-z]+)+$~`, except for `href` which may not be added, are added as attributes to the anchor element.
-
-Examples:
-
-```smarty
-{user object=$user}
-```
-
-generates
-
-```smarty
-<a href="{$user->getLink()}" data-object-id="{$user->userID}" class="userLink">{@$user->getFormattedUsername()}</a>
-```
-
-and
-
-```smarty
-{user object=$user type='avatar48' foo='bar'}
-```
-
-generates
-
-```smarty
-<a href="{$user->getLink()}" foo="bar">{@$object->getAvatar()->getImageTag(48)}</a>
-```
+++ /dev/null
----
-title: Templates
-sidebar: sidebar
-permalink: view_templates.html
-folder: view
-parent: view
----
-
-Templates are responsible for the output a user sees when requesting a page (while the PHP code is responsible for providing the data that will be shown).
-Templates are text files with `.tpl` as the file extension.
-WoltLab Suite Core compiles the template files once into a PHP file that is executed when a user requests the page.
-In subsequent request, as the PHP file containing the compiled template already exists, compiling the template is not necessary anymore.
-
-
-## Template Types and Conventions
-
-WoltLab Suite Core supports two types of templates:
-frontend templates (or simply *templates*) and backend templates (*ACP templates*).
-Each type of template is only available in its respective domain, thus frontend templates cannot be included or used in the ACP and vice versa.
-
-For pages and forms, the name of the template matches the unqualified name of the PHP class except for the `Page` or `Form` suffix:
-
-- `RegisterForm.class.php` → `register.tpl`
-- `UserPage.class.php` → `user.tpl`
-
-If you follow this convention, WoltLab Suite Core will automatically determine the template name so that you do not have to explicitly set it.
-
-{% include callout.html content="For forms that handle creating and editing objects, in general, there are two form classes: `FooAddForm` and `FooEditForm`. WoltLab Suite Core, however, generally only uses one template `fooAdd.tpl` and the template variable `$action` to distinguish between creating a new object (`$action = 'add'`) and editing an existing object (`$action = 'edit'`) as the differences between templates for adding and editing an object are minimal." type="info" %}
-
-
-
-## Installing Templates
-
-Templates and ACP templates are installed by two different package installation plugins:
-the [template PIP](package_pip_template.html) and the [ACP template PIP](package_pip_acp-template.html).
-More information about installing templates can be found on those pages.
-
-
-## Base Templates
-
-### Frontend
-
-```smarty
-{include file='header'}
-
-{* content *}
-
-{include file='footer'}
-```
-
-### Backend
-
-```smarty
-{include file='header' pageTitle='foo.bar.baz'}
-
-<header class="contentHeader">
- <div class="contentHeaderTitle">
- <h1 class="contentTitle">Title</h1>
- </div>
-
- <nav class="contentHeaderNavigation">
- <ul>
- {* your default content header navigation buttons *}
-
- {event name='contentHeaderNavigation'}
- </ul>
- </nav>
-</header>
-
-{* content *}
-
-{include file='footer'}
-```
-
-`foo.bar.baz` is the language item that contains the title of the page.
-
-
-## Common Template Components
-
-### Forms
-
-{% include callout.html content="For new forms, use the new [form builder API](php_api_form_builder.html) introduced with WoltLab Suite 5.2." type="info" %}
-
-```smarty
-<form method="post" action="{link controller='FooBar'}{/link}">
- <div class="section">
- <dl{if $errorField == 'baz'} class="formError"{/if}>
- <dt><label for="baz">{lang}foo.bar.baz{/lang}</label></dt>
- <dd>
- <input type="text" id="baz" name="baz" value="{$baz}" class="long" required autofocus>
- {if $errorField == 'baz'}
- <small class="innerError">
- {if $errorType == 'empty'}
- {lang}wcf.global.form.error.empty{/lang}
- {else}
- {lang}foo.bar.baz.error.{@$errorType}{/lang}
- {/if}
- </small>
- {/if}
- </dd>
- </dl>
-
- <dl>
- <dt><label for="bar">{lang}foo.bar.bar{/lang}</label></dt>
- <dd>
- <textarea name="bar" id="bar" cols="40" rows="10">{$bar}</textarea>
- {if $errorField == 'bar'}
- <small class="innerError">{lang}foo.bar.bar.error.{@$errorType}{/lang}</small>
- {/if}
- </dd>
- </dl>
-
- {* other fields *}
-
- {event name='dataFields'}
- </div>
-
- {* other sections *}
-
- {event name='sections'}
-
- <div class="formSubmit">
- <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
- {csrfToken}
- </div>
-</form>
-```
-
-### Tab Menus
-
-```smarty
-<div class="section tabMenuContainer">
- <nav class="tabMenu">
- <ul>
- <li><a href="{@$__wcf->getAnchor('tab1')}">Tab 1</a></li>
- <li><a href="{@$__wcf->getAnchor('tab2')}">Tab 2</a></li>
-
- {event name='tabMenuTabs'}
- </ul>
- </nav>
-
- <div id="tab1" class="tabMenuContent">
- <div class="section">
- {* contents of first tab *}
- </div>
- </div>
-
- <div id="tab2" class="tabMenuContainer tabMenuContent">
- <nav class="menu">
- <ul>
- <li><a href="{@$__wcf->getAnchor('tab2A')}">Tab 2A</a></li>
- <li><a href="{@$__wcf->getAnchor('tab2B')}">Tab 2B</a></li>
-
- {event name='tabMenuTab2Subtabs'}
- </ul>
- </nav>
-
- <div id="tab2A" class="tabMenuContent">
- <div class="section">
- {* contents of first subtab for second tab *}
- </div>
- </div>
-
- <div id="tab2B" class="tabMenuContent">
- <div class="section">
- {* contents of second subtab for second tab *}
- </div>
- </div>
-
- {event name='tabMenuTab2Contents'}
- </div>
-
- {event name='tabMenuContents'}
-</div>
-```
-
-
-## Template Scripting
-
-### Template Variables
-
-Template variables can be assigned via `WCF::getTPL()->assign('foo', 'bar')` and accessed in templates via `$foo`:
-
-- `{$foo}` will result in the contents of `$foo` to be passed to `StringUtil::encodeHTML()` before being printed.
-- `{#$foo}` will result in the contents of `$foo` to be passed to `StringUtil::formatNumeric()` before being printed.
- Thus, this method is relevant when printing numbers and having them formatted correctly according the the user’s language.
-- `{@$foo}` will result in the contents of `$foo` to be printed directly.
- In general, this method should not be used for user-generated input.
-
-Multiple template variables can be assigned by passing an array:
-
-```php
-WCF::getTPL()->assign([
- 'foo' => 'bar',
- 'baz' => false
-]);
-```
-
-#### Modifiers
-
-If you want to call a function on a variable, you can use the modifier syntax:
-`{@$foo|trim}`, for example, results in the trimmed contents of `$foo` to be printed.
-
-#### System Template Variable
-
-The template variable `$tpl` is automatically assigned and is an array containing different data:
-
-- `$tpl[get]` contains `$_GET`.
-- `$tpl[post]` contains `$_POST`.
-- `$tpl[cookie]` contains `$_COOKIE`.
-- `$tpl[server]` contains `$_SERVER`.
-- `$tpl[env]` contains `$_ENV`.
-- `$tpl[now]` contains `TIME_NOW` (current timestamp).
-
-Furthermore, the following template variables are also automatically assigned:
-
-- `$__wcf` contains the `WCF` object (or `WCFACP` object in the backend).
-
-### Comments
-
-Comments are wrapped in `{*` and `*}` and can span multiple lines:
-
-```smarty
-{* some
- comment *}
-```
-
-{% include callout.html content="The template compiler discards the comments, so that they not included in the compiled template." type="info" %}
-
-### Conditions
-
-Conditions follow a similar syntax to PHP code:
-
-```smarty
-{if $foo === 'bar'}
- foo is bar
-{elseif $foo === 'baz'}
- foo is baz
-{else}
- foo is neither bar nor baz
-{/if}
-```
-
-The supported operators in conditions are `===`, `!==`, `==`, `!=`, `<=`, `<`, `>=`, `>`, `||`, `&&`, `!`, and `=`.
-
-More examples:
-
-````smarty
-{if $bar|isset}…{/if}
-
-{if $bar|count > 3 && $bar|count < 100}…{/if}
-````
-
-### Foreach Loops
-
-Foreach loops allow to iterate over arrays or iterable objects:
-
-```smarty
-<ul>
- {foreach from=$array key=key item=value}
- <li>{$key}: {$value}</li>
- {/foreach}
-</ul>
-```
-
-While the `from` attribute containing the iterated structure and the `item` attribute containg the current value are mandatory, the `key` attribute is optional.
-If the foreach loop has a name assigned to it via the `name` attribute, the `$tpl` template variable provides additional data about the loop:
-
-```smarty
-<ul>
- {foreach from=$array key=key item=value name=foo}
- {if $tpl[foreach][foo][first]}
- something special for the first iteration
- {elseif $tpl[foreach][foo][last]}
- something special for the last iteration
- {/if}
-
- <li>iteration {#$tpl[foreach][foo][iteration]+1} out of {#$tpl[foreach][foo][total]} {$key}: {$value}</li>
- {/foreach}
-</ul>
-```
-
-In contrast to PHP’s foreach loop, templates also support `foreachelse`:
-
-```smarty
-{foreach from=$array item=value}
- …
-{foreachelse}
- there is nothing to iterate over
-{/foreach}
-```
-
-### Including Other Templates
-
-To include template named `foo` from the same domain (frontend/backend), you can use
-
-```smarty
-{include file='foo'}
-```
-
-If the template belongs to an application, you have to specify that application using the `application` attribute:
-
-```smarty
-{include file='foo' application='app'}
-```
-
-Additional template variables can be passed to the included template as additional attributes:
-
-```smarty
-{include file='foo' application='app' var1='foo1' var2='foo2'}
-```
-
-### Template Plugins
-
-An overview of all available template plugins can be found [here](view_template-plugins.html).