A full `package.json` that includes WoltLab Suite, TypeScript, eslint and Prettier could look like the following.
-```json
-{
- "devDependencies": {
- "@typescript-eslint/eslint-plugin": "^4.6.1",
- "@typescript-eslint/parser": "^4.6.1",
- "eslint": "^7.12.1",
- "eslint-config-prettier": "^6.15.0",
- "prettier": "^2.1.2",
- "tslib": "^2.0.3",
- "typescript": "^4.1.3"
- },
- "dependencies": {
- "@woltlab/wcf": "https://github.com/WoltLab/WCF.git#master"
- }
-}
-```
+{jinja{ codebox(
+ "json",
+ "typescript/package.json",
+ "package.json"
+) }}
After installing the types using npm, you will also need to configure `tsconfig.json` to take the types into account.
To do so, you will need to add them to the `compilerOptions.paths` option.
A complete `tsconfig.json` file that matches the configuration of WoltLab Suite could look like the following.
-```json
-{
- "include": [
- "node_modules/@woltlab/wcf/global.d.ts",
- "ts/**/*"
- ],
- "compilerOptions": {
- "target": "es2017",
- "module": "amd",
- "rootDir": "ts/",
- "outDir": "files/js/",
- "lib": [
- "dom",
- "es2017"
- ],
- "strictNullChecks": true,
- "moduleResolution": "node",
- "esModuleInterop": true,
- "noImplicitThis": true,
- "strictBindCallApply": true,
- "baseUrl": ".",
- "paths": {
- "*": [
- "node_modules/@woltlab/wcf/ts/*"
- ]
- },
- "importHelpers": true
- }
-}
-```
+{jinja{ codebox(
+ "json",
+ "typescript/tsconfig.json",
+ "tsconfig.json"
+) }}
After this initial set-up, you would place your TypeScript source files into the `ts/` folder of your project.
The generated JavaScript target files will be placed into `files/js/` and thus will be installed by the [file PIP](../package/pip/file.md).
The current configuration of these tools is as follows.
It is recommended to re-use this configuration as is.
-### .prettierrc
-
-```yml
-trailingComma: all
-printWidth: 120
-```
-
-### .eslintrc.js
-
-```javascript
-module.exports = {
- root: true,
- parser: "@typescript-eslint/parser",
- parserOptions: {
- tsconfigRootDir: __dirname,
- project: ["./tsconfig.json"]
- },
- plugins: ["@typescript-eslint"],
- extends: [
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended",
- "plugin:@typescript-eslint/recommended-requiring-type-checking",
- "prettier",
- "prettier/@typescript-eslint"
- ],
- rules: {
- "@typescript-eslint/ban-types": [
- "error", {
- types: {
- "object": false
- },
- extendDefaults: true
- }
- ],
- "@typescript-eslint/no-explicit-any": 0,
- "@typescript-eslint/no-non-null-assertion": 0,
- "@typescript-eslint/no-unsafe-assignment": 0,
- "@typescript-eslint/no-unsafe-call": 0,
- "@typescript-eslint/no-unsafe-member-access": 0,
- "@typescript-eslint/no-unsafe-return": 0,
- "@typescript-eslint/no-unused-vars": [
- "error", {
- "argsIgnorePattern": "^_"
- }
- ]
- }
-};
-```
-
-### .eslintignore
+{jinja{ codebox(
+ "yml",
+ "typescript/.prettierrc",
+ ".prettierrc"
+) }}
-```gitignore
-**/*.js
-```
+{jinja{ codebox(
+ "javascript",
+ "typescript/.eslintrc.js",
+ ".eslintrc.js"
+) }}
-### .gitattributes
+{jinja{ codebox(
+ "gitignore",
+ "typescript/.eslintignore",
+ ".eslintignore"
+) }}
This `.gitattributes` configuration will automatically collapse the generated JavaScript target files in GitHub’s Diff view.
You will not need it if you do not use git or GitHub.
-```gitattributes
-files/js/**/*.js linguist-generated
-```
+{jinja{ codebox(
+ "gitattributes",
+ "typescript/.gitattributes",
+ ".gitattributes"
+) }}
## Writing a simple module
WoltLab Suite’s modules can be imported using the standard ECMAScript module import syntax by specifying the full module name.
The public API of the module can also be exported using the standard ECMAScript module export syntax.
-```typescript
-import * as Language from "WoltLabSuite/Core/Language";
-
-export function run() {
- alert(Language.get("wcf.foo.bar"));
-}
-```
+{jinja{ codebox(
+ "typescript",
+ "typescript/Example.ts",
+ "ts/Example.ts"
+) }}
This simple example module will compile to plain JavaScript that is compatible with the AMD loader that is used by WoltLab Suite.
-```javascript
-define(["require", "exports", "tslib", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Language) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.run = void 0;
- Language = tslib_1.__importStar(Language);
- function run() {
- alert(Language.get("wcf.foo.bar"));
- }
- exports.run = run;
-});
-```
+{jinja{ codebox(
+ "javascript",
+ "typescript/Example.js",
+ "files/js/Example.js"
+) }}
Within templates it can be consumed as follows.
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/package.xml",
+ "package.xml"
+) }}
## Elements
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/aclOption.xml",
+ "aclOption.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/acpMenu.xml",
+ "acpMenu.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/acpSearchProvider.xml",
+ "acpSearchProvider.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/bbcode.xml",
+ "bbcode.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/box.xml",
+ "box.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/clipboardAction.xml",
+ "clipboardAction.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/coreObject.xml",
+ "coreObject.xml"
+) }}
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).
## 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>
-```
-
+{jinja{ codebox(
+ "xml",
+ "package/pip/cronjob.xml",
+ "cronjob.xml"
+) }}
## 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>
-
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/eventListener.xml",
+ "eventListener.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/en.xml",
+ "language/en.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/mediaProvider.xml",
+ "mediaProvider.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/menuItem.xml",
+ "menuItem.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/menu.xml",
+ "menu.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/objectTypeDefinition.xml",
+ "objectTypeDefinition.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/objectType.xml",
+ "objectType.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/option.xml",
+ "option.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/page.xml",
+ "page.xml"
+) }}
\ No newline at end of file
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/packageInstallationPlugin.xml",
+ "packageInstallationPlugin.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/smiley.xml",
+ "smiley.xml"
+) }}
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;
-```
+{jinja{ codebox(
+ "sql",
+ "package/pip/install.sql",
+ "install.sql"
+) }}
\ No newline at end of file
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/templateListener.xml",
+ "templateListener.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/userMenu.xml",
+ "userMenu.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/userNotificationEvent.xml",
+ "userNotificationEvent.xml"
+) }}
## 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>
-```
+{jinja{ codebox(
+ "xml",
+ "package/pip/userProfileMenu.xml",
+ "userProfileMenu.xml"
+) }}
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;
- }
-}
-```
+{jinja{ codebox(
+"php",
+"php/api/caches/ExampleCacheBuilder.class.php",
+"files/lib/system/cache/builder/ExampleCacheBuilder.class.php"
+) }}
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
## 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;
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/caches/UserRuntimeCache.class.php",
+ "files/lib/system/cache/runtime/UserRuntimeCache.class.php"
+) }}
## 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();
- }
-}
-
-```
+{jinja{ codebox(
+ "php",
+ "php/api/cronjobs/LastActivityCronjob.class.php",
+ "files/lib/system/cronjob/LastActivityCronjob.class.php"
+) }}
## `ICronjob` Interface
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;
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/events/ExampleComponent.class.php",
+ "files/lib/system/example/ExampleComponent.class.php"
+) }}
where an event with event name `getVar` is fired in the `getVar()` method.
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;
- }
-}
-
-```
+{jinja{ codebox(
+ "php",
+ "php/api/events/ExampleEventListener.class.php",
+ "files/lib/system/event/listener/ExampleEventListener.class.php"
+) }}
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()`.
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'];
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/events/ExampleParser1.class.php",
+ "files/lib/system/example/ExampleParser.class.php"
+) }}
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;
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/events/ExampleParserEventListener.class.php",
+ "files/lib/system/event/listener/ExampleParserEventListener.class.php"
+) }}
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'];
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/events/ExampleParser2.class.php",
+ "files/lib/system/example/ExampleParser.class.php"
+) }}
## Advanced Example: Additional Form Field
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');
- }
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/events/ExampleAddFormListener.class.php",
+ "files/lib/system/event/listener/ExampleAddFormListener.class.php"
+) }}
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.
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.md)):
-```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
+{jinja{ codebox(
+ "xml",
+ "php/api/events/eventListener.xml",
+ "eventListener.xml"
+) }}
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');
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/sitemaps/UserSitemapObject.class.php",
+ "files/lib/system/sitemap/object/UserSitemapObject.class.php"
+) }}
Next, the sitemap object must be registered as an object type:
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>
-```
+{jinja{ codebox(
+ "xml",
+ "php/api/user_notifications/objectType.xml",
+ "objectType.xml"
+) }}
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;
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/user_notifications/FooUserNotificationObjectType.class.php",
+ "files/lib/system/user/notification/object/type/FooUserNotificationObjectType.class.php"
+) }}
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;
+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.
-/**
- * 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;
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/user_notifications/FooUserNotificationObject.class.php",
+ "files/lib/system/user/notification/object/FooUserNotificationObject.class.php"
+) }}
- 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.
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.md).
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>
-```
+{jinja{ codebox(
+ "xml",
+ "php/api/user_notifications/userNotificationEvent.xml",
+ "userNotificationEvent.xml"
+) }}
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);
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/api/user_notifications/FooUserNotificationEvent.class.php",
+ "files/lib/system/user/notification/event/FooUserNotificationEvent.class.php"
+) }}
- 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.
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);
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/code-style/Box.class.php",
+ "files/lib/data/box/Box.class.php"
+) }}
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 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 {}
-```
+{jinja{ codebox(
+ "php",
+ "php/database-objects/Example.class.php",
+ "files/lib/data/example/Example.class.php"
+) }}
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`.
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;
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/database-objects/ViewableExample.class.php",
+ "files/lib/data/example/ViewableExample.class.php"
+) }}
It is mandatory to set the static `$baseClass` property to the name of the decorated class.
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;
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/database-objects/ExampleEditor.class.php",
+ "files/lib/data/example/ExampleEditor.class.php"
+) }}
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.
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;
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/database-objects/ExampleList.class.php",
+ "files/lib/data/example/ExampleList.class.php"
+) }}
The following code listing illustrates loading a large set of examples and iterating over the list to retrieve the objects.
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;
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/database-objects/ViewableExampleList.class.php",
+ "files/lib/data/example/ViewableExampleList.class.php"
+) }}
## AbstractDatabaseObjectAction
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;
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/database-objects/ExampleAction.class.php",
+ "files/lib/data/example/ExampleAction.class.php"
+) }}
### Executing an Action
### 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';
- }
-}
-```
+{jinja{ codebox(
+ "php",
+ "php/gdpr/MyUserExportGdprActionListener.class.php",
+ "files/lib/system/event/listener/MyUserExportGdprActionListener.class.php"
+) }}
### `$data`
border-left: .2rem solid #448aff;
background-color: rgba(0,184,212,.1);
}
+
+.titledCodeBox .codeBoxTitle {
+ background-color: rgb(225, 225, 225);
+ font-size: .85em;
+ font-weight: bold;
+ padding: 0.5em 1em;
+}
+
+.titledCodeBox .codeBoxTitle code {
+ background: none;
+}
+
+.titledCodeBox .codeBoxTitle + .highlighttable {
+ margin-top: 0;
+}
The first file for our package is the `install_com.woltlab.wcf.people.php` file used to create such a database table during package installation:
-```sql
---8<-- "tutorial/tutorial-series/part-1/files/acp/database/install_com.woltlab.wcf.people.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/acp/database/install_com.woltlab.wcf.people.php",
+ "files/acp/database/install_com.woltlab.wcf.people.php"
+) }}
### Database Object
In our PHP code, each person will be represented by an object of the following class:
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php",
+ "files/lib/data/person/Person.class.php"
+) }}
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.
#### `PersonAction`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php",
+ "files/lib/data/person/PersonAction.class.php"
+) }}
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.
#### `PersonEditor`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php",
+ "files/lib/data/person/PersonEditor.class.php"
+) }}
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`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php",
+ "files/lib/data/person/PersonList.class.php"
+) }}
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.
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.
-```xml
---8<-- "tutorial/tutorial-series/part-1/acpMenu.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-1/acpMenu.xml",
+ "acpMenu.xml"
+) }}
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.
#### `PersonListPage`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php",
+ "files/lib/data/person/PersonListPage.class.php"
+) }}
As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
#### `personList.tpl`
-```smarty
---8<-- "tutorial/tutorial-series/part-1/acptemplates/personList.tpl"
-```
+{jinja{ codebox(
+ "smarty",
+ "tutorial/tutorial-series/part-1/acptemplates/personList.tpl",
+ "acptemplates/personList.tpl"
+) }}
We will go piece by piece through the template code:
#### `PersonAddForm`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php",
+ "files/lib/acp/form/PersonAddForm.class.php"
+) }}
The properties here consist of three types:
the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the [`$objectEditLinkController` property](../../migration/wsc52/php.md#addform), which is used to generate a link to edit the newly created person after submitting the form, and finally `$formAction` and `$objectActionClass` required by the [PHP form builder API](../../php/api/form_builder/overview.md) used to generate the form.
#### `personAdd.tpl`
-```smarty
---8<-- "tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl"
-```
+{jinja{ codebox(
+ "smarty",
+ "tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl",
+ "acptemplates/personAdd.tpl"
+) }}
We will now only concentrate on the new parts compared to `personList.tpl`:
#### `PersonEditForm`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php",
+ "files/lib/acp/form/PersonEditForm.class.php"
+) }}
In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
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.md):
-```xml
---8<-- "tutorial/tutorial-series/part-1/page.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-1/page.xml",
+ "page.xml"
+) }}
For more information about what each of the elements means, please refer to the [page package installation plugin page](../../package/pip/page.md).
Next, we register the menu item using the [menuItem package installation plugin](../../package/pip/menu-item.md):
-```xml
---8<-- "tutorial/tutorial-series/part-1/menuItem.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-1/menuItem.xml",
+ "menuItem.xml"
+) }}
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.
#### `PersonListPage`
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php",
+ "files/lib/page/PersonListPage.class.php"
+) }}
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.
#### `personList.tpl`
-```smarty
---8<-- "tutorial/tutorial-series/part-1/templates/personList.tpl"
-```
+{jinja{ codebox(
+ "smarty",
+ "tutorial/tutorial-series/part-1/templates/personList.tpl",
+ "templates/personList.tpl"
+) }}
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.
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.md):
-```xml
---8<-- "tutorial/tutorial-series/part-1/userGroupOption.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-1/userGroupOption.xml",
+ "userGroupOption.xml"
+) }}
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`.
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.md).
-```xml
---8<-- "tutorial/tutorial-series/part-1/package.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-1/package.xml",
+ "package.xml"
+) }}
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) `5.4.0 Alpha 1`.
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 [`database` package installation plugin](../../package/pip/database.md):
-```sql
---8<-- "tutorial/tutorial-series/part-2/files/acp/database/install_com.woltlab.wcf.people.birthday.php"
-```
+{jinja{ codebox(
+ "sql",
+ "tutorial/tutorial-series/part-2/files/acp/database/install_com.woltlab.wcf.people.birthday.php",
+ "files/acp/database/install_com.woltlab.wcf.people.birthday.php"
+) }}
If we have a [`Person` object](part_1.md#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`.
To set the birthday of a person, we only have to add another form field with an event listener:
-```php
---8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php",
+ "files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php"
+) }}
registered via
The language item `wcf.person.birthday` used in the label is the only new one for this package:
-```sql
---8<-- "tutorial/tutorial-series/part-2/language/de.xml"
-```
+{jinja{ codebox(
+ "sql",
+ "tutorial/tutorial-series/part-2/language/de.xml",
+ "language/de.xml"
+) }}
-```sql
---8<-- "tutorial/tutorial-series/part-2/language/en.xml"
-```
+{jinja{ codebox(
+ "sql",
+ "tutorial/tutorial-series/part-2/language/en.xml",
+ "language/en.xml"
+) }}
## Adding Birthday Table Column in ACP
The first part is a very simple class:
-```php
---8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php",
+ "files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php"
+) }}
!!! info "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."
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:
-```smarty
---8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl"
-```
+{jinja{ codebox(
+ "smarty",
+ "tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl",
+ "templates/__personListBirthdaySortField.tpl"
+) }}
!!! info "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."
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:
-```smarty
---8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl"
-```
+{jinja{ codebox(
+ "smarty",
+ "tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl",
+ "templates/__personListBirthday.tpl"
+) }}
## `templateListener.xml`
The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
-```xml
---8<-- "tutorial/tutorial-series/part-2/templateListener.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-2/templateListener.xml",
+ "templateListener.xml"
+) }}
In cases where a template is used, we simply use the `include` syntax to load the template.
There are two event listeners that make `birthday` a valid sort field in the ACP and the front end, respectively, and the third event listener takes care of setting the birthday.
-```xml
---8<-- "tutorial/tutorial-series/part-2/eventListener.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-2/eventListener.xml",
+ "eventListener.xml"
+) }}
## `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>`):
-```xml
---8<-- "tutorial/tutorial-series/part-2/package.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-2/package.xml",
+ "package.xml"
+) }}
---
To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](../../php/api/caches_runtime-caches.md) for people:
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php",
+ "files/lib/system/cache/runtime/PersonRuntimeCache.class.php"
+) }}
## 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):
-```xml
---8<-- "tutorial/tutorial-series/part-3/objectType.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-3/objectType.xml",
+ "objectType.xml"
+) }}
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):
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php",
+ "files/lib/system/comment/manager/PersonCommentManager.class.php"
+) }}
- 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.md#user-group-options).
### `PersonPage`
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php",
+ "files/lib/page/PersonPage.class.php"
+) }}
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.
### `person.tpl`
-```tpl
---8<-- "tutorial/tutorial-series/part-3/templates/person.tpl"
-```
+{jinja{ codebox(
+ "tpl",
+ "tutorial/tutorial-series/part-3/templates/person.tpl",
+ "templates/person.tpl"
+) }}
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.
### `page.xml`
-```xml
---8<-- "tutorial/tutorial-series/part-3/page.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-3/page.xml",
+ "page.xml"
+) }}
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:
### `PersonPageHandler`
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php",
+ "files/lib/system/page/handler/PersonPageHandler.class.php"
+) }}
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.
The `com.woltlab.wcf.boxController` object type definition requires the provided class to implement `wcf\system\box\IBoxController`:
-```php
---8<-- "tutorial/tutorial-series/part-4/files/lib/system/box/PersonListBoxController.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-4/files/lib/system/box/PersonListBoxController.class.php",
+ "files/lib/system/box/PersonListBoxController.class.php"
+) }}
By extending `AbstractDatabaseObjectListBoxController`, we only have to provide minimal data ourself and rely mostly on the default implementation provided by `AbstractDatabaseObjectListBoxController`:
The first step for condition support is to register a object type definition for the relevant conditions requiring the `IObjectListCondition` interface:
-```xml
---8<-- "tutorial/tutorial-series/part-4/objectTypeDefinition.xml"
-```
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-4/objectTypeDefinition.xml",
+ "objectTypeDefinition.xml"
+) }}
Next, we register the specific conditions for filtering by the first and last name using this object type condition:
`PersonFirstNameTextPropertyCondition` and `PersonLastNameTextPropertyCondition` only differ minimally so that we only focus on `PersonFirstNameTextPropertyCondition` here, which relies on the default implementation `AbstractObjectTextPropertyCondition` and only requires specifying different object properties:
-```php
---8<-- "tutorial/tutorial-series/part-4/files/lib/system/condition/person/PersonFirstNameTextPropertyCondition.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-4/files/lib/system/condition/person/PersonFirstNameTextPropertyCondition.class.php",
+ "files/lib/system/condition/person/PersonFirstNameTextPropertyCondition.class.php"
+) }}
1. `$className` contains the class name of the relevant database object from which the class name of the database object list is derived and `$propertyName` is the name of the database object's property that contains the value used for filtering.
1. By setting `$supportsMultipleValues` to `true`, multiple comma-separated values can be specified so that, for example, a box can also only list people with either of two specific first names.
The PHP file with the database layout has been updated as follows:
-```php
---8<-- "tutorial/tutorial-series/part-5/files/acp/database/install_com.woltlab.wcf.people.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-5/files/acp/database/install_com.woltlab.wcf.people.php",
+ "files/acp/database/install_com.woltlab.wcf.people.php"
+) }}
- The number of pieces of information per person is tracked via the new `informationCount` column.
- The `wcf1_person_information` table has been added for the `PersonInformation` model.
The meaning of the different columns is explained in the property documentation part of `PersonInformation`'s documentation (see below).
The two foreign keys ensure that if a person is deleted, all of their information is also deleted, and that if a user is deleted, the `userID` column is set to `NULL`.
-```php
---8<-- "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformation.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformation.class.php",
+ "files/lib/data/person/information/PersonInformation.class.php"
+) }}
`PersonInformation` provides two methods, `canDelete()` and `canEdit()`, to check whether the active user can delete or edit a specific piece of information.
In both cases, it is checked if the current user has created the relevant piece of information to check the user-specific permissions or to fall back to the moderator-specific permissions.
While `PersonInformationEditor` is simply the default implementation and thus not explicitly shown here, `PersonInformationList::readObjects()` caches the relevant ids of the associated people and users who created the pieces of information using runtime caches:
-```php
---8<-- "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationList.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationList.class.php",
+ "files/lib/data/person/information/PersonInformationList.class.php"
+) }}
## Listing and Deleting Person Information
The `person.tpl` template has been updated to include a block for listing the information at the beginning:
-```smarty
---8<-- "tutorial/tutorial-series/part-5/templates/person.tpl"
-```
+{jinja{ codebox(
+ "smarty",
+ "tutorial/tutorial-series/part-5/templates/person.tpl",
+ "templates/person.tpl"
+) }}
To keep things simple here, we reuse the structure and CSS classes used for comments.
Additionally, we always list all pieces of information.
When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form:
-```TypeScript
---8<-- "tutorial/tutorial-series/part-5/ts/WoltLabSuite/Core/Controller/Person.ts"
-```
+{jinja{ codebox(
+ "typescript",
+ "tutorial/tutorial-series/part-5/ts/WoltLabSuite/Core/Controller/Person.ts",
+ "ts/WoltLabSuite/Core/Controller/Person.ts"
+) }}
We use the [`WoltLabSuite/Core/Form/Builder/Dialog` module](https://github.com/WoltLab/WCF/blob/master/ts/WoltLabSuite/Core/Form/Builder/Dialog.ts), which takes care of the internal handling with regard to these dialogs.
We only have to provide some data during for initializing these objects and call the `open()` function after a button has been clicked.
Next, we focus on `PersonInformationAction`, which actually provides the contents of these dialogs and creates and edits the information:
-```php
---8<-- "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationAction.class.php"
-```
+{jinja{ codebox(
+ "php",
+ "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationAction.class.php",
+ "files/lib/data/person/information/PersonInformationAction.class.php"
+) }}
When setting up the `WoltLabSuite/Core/Form/Builder/Dialog` object for adding new pieces of information, we specified `getAddDialog` and `submitAddDialog` as the names of the dialog getter and submit handler.
In addition to these two methods, the matching validation methods `validateGetAddDialog()` and `validateGetAddDialog()` are also added.
Lastly, we present the updated `eventListener.xml` file with new entries for all of these event listeners:
-```xml
---8<-- "tutorial/tutorial-series/part-5/eventListener.xml"
-```
\ No newline at end of file
+{jinja{ codebox(
+ "xml",
+ "tutorial/tutorial-series/part-5/eventListener.xml",
+ "eventListener.xml"
+) }}
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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="en">
+ <category name="wcf.example">
+ <item name="wcf.example.foo"><![CDATA[<strong>Look!</strong>]]></item>
+ </category>
+</language>
\ No newline at end of file
--- /dev/null
+<?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
+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;
\ No newline at end of file
--- /dev/null
+<?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 name="example" />
+ </delete>
+</data>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?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;
+}
\ No newline at end of file
--- /dev/null
+<?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();
+ }
+}
--- /dev/null
+<?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');
+ }
+ }
+}
--- /dev/null
+<?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;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\system\event\listener;
+
+class ExampleEventListener implements IParameterizedEventListener {
+ public function execute($eventObj, $className, $eventName, array &$parameters) {
+ $eventObj->var = 2;
+ }
+}
--- /dev/null
+<?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'];
+ }
+}
\ No newline at end of file
--- /dev/null
+<?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'];
+ }
+}
--- /dev/null
+<?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;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?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
+<?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');
+ }
+}
--- /dev/null
+<?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);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?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;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?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;
+}
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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>
\ No newline at end of file
--- /dev/null
+<?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);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObject;
+
+class Example extends DatabaseObject {}
--- /dev/null
+<?php
+namespace wcf\data\example;
+use wcf\data\AbstractDatabaseObjectAction;
+
+class ExampleAction extends AbstractDatabaseObjectAction {
+ public $className = ExampleEditor::class;
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObjectEditor;
+
+class ExampleEditor extends DatabaseObjectEditor {
+ protected static $baseClass = Example::class;
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\data\example;
+use wcf\data\DatabaseObjectList;
+
+class ExampleList extends DatabaseObjectList {
+ public $className = Example::class;
+}
\ No newline at end of file
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\example;
+
+class ViewableExampleList extends ExampleList {
+ public $decoratorClassName = ViewableExample::class;
+}
\ No newline at end of file
--- /dev/null
+<?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';
+ }
+}
\ No newline at end of file
--- /dev/null
+**/*.js
\ No newline at end of file
--- /dev/null
+module.exports = {
+ root: true,
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: ["./tsconfig.json"]
+ },
+ plugins: ["@typescript-eslint"],
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking",
+ "prettier",
+ "prettier/@typescript-eslint"
+ ],
+ rules: {
+ "@typescript-eslint/ban-types": [
+ "error", {
+ types: {
+ "object": false
+ },
+ extendDefaults: true
+ }
+ ],
+ "@typescript-eslint/no-explicit-any": 0,
+ "@typescript-eslint/no-non-null-assertion": 0,
+ "@typescript-eslint/no-unsafe-assignment": 0,
+ "@typescript-eslint/no-unsafe-call": 0,
+ "@typescript-eslint/no-unsafe-member-access": 0,
+ "@typescript-eslint/no-unsafe-return": 0,
+ "@typescript-eslint/no-unused-vars": [
+ "error", {
+ "argsIgnorePattern": "^_"
+ }
+ ]
+ }
+};
\ No newline at end of file
--- /dev/null
+files/js/**/*.js linguist-generated
\ No newline at end of file
--- /dev/null
+trailingComma: all
+printWidth: 120
\ No newline at end of file
--- /dev/null
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Language) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.run = void 0;
+ Language = tslib_1.__importStar(Language);
+ function run() {
+ alert(Language.get("wcf.foo.bar"));
+ }
+ exports.run = run;
+});
\ No newline at end of file
--- /dev/null
+import * as Language from "WoltLabSuite/Core/Language";
+
+export function run() {
+ alert(Language.get("wcf.foo.bar"));
+}
\ No newline at end of file
--- /dev/null
+{
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^4.6.1",
+ "@typescript-eslint/parser": "^4.6.1",
+ "eslint": "^7.12.1",
+ "eslint-config-prettier": "^6.15.0",
+ "prettier": "^2.1.2",
+ "tslib": "^2.0.3",
+ "typescript": "^4.1.3"
+ },
+ "dependencies": {
+ "@woltlab/wcf": "https://github.com/WoltLab/WCF.git#master"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "include": [
+ "node_modules/@woltlab/wcf/global.d.ts",
+ "ts/**/*"
+ ],
+ "compilerOptions": {
+ "target": "es2017",
+ "module": "amd",
+ "rootDir": "ts/",
+ "outDir": "files/js/",
+ "lib": [
+ "dom",
+ "es2017"
+ ],
+ "strictNullChecks": true,
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "noImplicitThis": true,
+ "strictBindCallApply": true,
+ "baseUrl": ".",
+ "paths": {
+ "*": [
+ "node_modules/@woltlab/wcf/ts/*"
+ ]
+ },
+ "importHelpers": true
+ }
+}
\ No newline at end of file