From d68aabe04dbe6ccadba5a4ae1e05f7c0ddb94a17 Mon Sep 17 00:00:00 2001 From: WoltLab GmbH Date: Fri, 23 Apr 2021 13:50:11 +0000 Subject: [PATCH] Deployed 14b5e91 to 5.4 with MkDocs 1.1.2 and mike 1.0.0 --- 5.4/javascript/typescript/index.html | 190 +++---- 5.4/package/package-xml/index.html | 8 +- 5.4/package/pip/acl-option/index.html | 11 +- 5.4/package/pip/acp-menu/index.html | 7 +- .../pip/acp-search-provider/index.html | 7 +- 5.4/package/pip/bbcode/index.html | 51 +- 5.4/package/pip/box/index.html | 7 +- 5.4/package/pip/clipboard-action/index.html | 7 +- 5.4/package/pip/core-object/index.html | 8 +- 5.4/package/pip/cronjob/index.html | 7 +- 5.4/package/pip/event-listener/index.html | 7 +- 5.4/package/pip/language/index.html | 9 +- 5.4/package/pip/media-provider/index.html | 45 +- 5.4/package/pip/menu-item/index.html | 7 +- 5.4/package/pip/menu/index.html | 7 +- .../pip/object-type-definition/index.html | 7 +- 5.4/package/pip/object-type/index.html | 7 +- 5.4/package/pip/option/index.html | 9 +- 5.4/package/pip/page/index.html | 7 +- 5.4/package/pip/pip/index.html | 7 +- 5.4/package/pip/smiley/index.html | 7 +- 5.4/package/pip/sql/index.html | 7 +- 5.4/package/pip/template-listener/index.html | 7 +- 5.4/package/pip/user-menu/index.html | 7 +- .../pip/user-notification-event/index.html | 7 +- 5.4/package/pip/user-profile-menu/index.html | 7 +- .../api/caches_persistent-caches/index.html | 39 +- 5.4/php/api/caches_runtime-caches/index.html | 56 +- 5.4/php/api/cronjobs/index.html | 18 +- 5.4/php/api/events/index.html | 304 ++--------- 5.4/php/api/sitemaps/index.html | 83 +-- 5.4/php/api/user_notifications/index.html | 511 ++---------------- 5.4/php/apps/index.html | 2 +- 5.4/php/code-style/index.html | 53 +- 5.4/php/database-objects/index.html | 126 ++--- 5.4/php/gdpr/index.html | 40 +- 5.4/search/search_index.json | 2 +- 5.4/sitemap.xml.gz | Bin 906 -> 906 bytes 5.4/stylesheets/extra.css | 15 + 5.4/tutorial/series/part_1/index.html | 122 ++++- 5.4/tutorial/series/part_2/index.html | 62 ++- 5.4/tutorial/series/part_3/index.html | 44 +- 5.4/tutorial/series/part_4/index.html | 20 +- 5.4/tutorial/series/part_5/index.html | 43 +- 44 files changed, 659 insertions(+), 1338 deletions(-) diff --git a/5.4/javascript/typescript/index.html b/5.4/javascript/typescript/index.html index d2a64b02..2240c47e 100644 --- a/5.4/javascript/typescript/index.html +++ b/5.4/javascript/typescript/index.html @@ -751,40 +751,6 @@ Additional Tools - -
  • @@ -2003,40 +1969,6 @@ Additional Tools - -
  • @@ -2067,7 +1999,9 @@

    Consuming WoltLab Suite’s Types#

    To consume the types of WoltLab Suite, you will need to install the @woltlab/wcf npm package using a git URL that refers to the appropriate branch of WoltLab/WCF.

    A full package.json that includes WoltLab Suite, TypeScript, eslint and Prettier could look like the following.

    -
     1
    +
    +
    package.json
    +
     1
      2
      3
      4
    @@ -2096,10 +2030,14 @@
     }
     
    +
    +

    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.

    -
     1
    +
    +
    tsconfig.json
    +
     1
      2
      3
      4
    @@ -2156,20 +2094,26 @@ A complete tsconfig.json file that matches the configuration of Wol
     }
     
    +
    +

    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.

    Additional Tools#

    WoltLab Suite uses additional tools to ensure the high quality and a consistent code style of the TypeScript modules. The current configuration of these tools is as follows. It is recommended to re-use this configuration as is.

    -

    .prettierrc#

    -
    1
    +
    +
    .prettierrc
    +
    1
     2
    trailingComma: all
     printWidth: 120
     
    -

    .eslintrc.js#

    -
     1
    +
    + +
    +
    .eslintrc.js
    +
     1
      2
      3
      4
    @@ -2206,60 +2150,70 @@ printWidth: 120
     35
     36
     37
    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
    -      }
    +    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"
         ],
    -    "@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": "^_"
    -      }
    -    ]
    -  }
    +    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#

    -
    1
    **/*.js
    +
    + +
    +
    .eslintignore
    +
    1
    **/*.js
     
    -

    .gitattributes#

    +
    +

    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.

    -
    1
    files/js/**/*.js linguist-generated
    +
    +
    .gitattributes
    +
    1
    files/js/**/*.js linguist-generated
     
    +
    +

    Writing a simple module#

    After completing this initial set-up you can start writing your first TypeScript module. The TypeScript compiler can be launched in Watch Mode by running npx tsc -w.

    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.

    -
    1
    +
    +
    ts/Example.ts
    +
    1
     2
     3
     4
    @@ -2270,8 +2224,12 @@ The public API of the module can also be exported using the standard ECMAScript
     }
     
    +
    +

    This simple example module will compile to plain JavaScript that is compatible with the AMD loader that is used by WoltLab Suite.

    -
     1
    +
    +
    files/js/Example.js
    +
     1
      2
      3
      4
    @@ -2292,6 +2250,8 @@ The public API of the module can also be exported using the standard ECMAScript
     });
     
    +
    +

    Within templates it can be consumed as follows.

    1
     2
    @@ -2312,7 +2272,7 @@ The public API of the module can also be exported using the standard ECMAScript
     
    - Last update: 2021-04-13 + Last update: 2021-04-23
    diff --git a/5.4/package/package-xml/index.html b/5.4/package/package-xml/index.html index 5d54ac8a..4acce158 100644 --- a/5.4/package/package-xml/index.html +++ b/5.4/package/package-xml/index.html @@ -2389,7 +2389,9 @@

    The package.xml is the core component of every package. It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version.

    Example#

    -
     1
    +
    +
    package.xml
    +
     1
      2
      3
      4
    @@ -2444,6 +2446,8 @@ It provides the meta data (e.g. package name, description, author) and the instr
     </package>
     
    +
    +

    Elements#

    <package>#

    The root node of every package.xml it contains the reference to the namespace and the location of the XML Schema Definition (XSD).

    @@ -2628,7 +2632,7 @@ Instead of using a dummy <instruction> that idempotently upda
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/acl-option/index.html b/5.4/package/pip/acl-option/index.html index 1a6d7bf6..0e7a1648 100644 --- a/5.4/package/pip/acl-option/index.html +++ b/5.4/package/pip/acl-option/index.html @@ -2077,7 +2077,9 @@

    <objecttype>#

    The name of the acl object type (of the object type definition com.woltlab.wcf.acl).

    Example#

    -
     1
    +
    +
    aclOption.xml
    +
     1
      2
      3
      4
    @@ -2135,15 +2137,16 @@
     
         <delete>
             <optioncategory name="old.example">
    -           <objecttype>com.example.wcf.example</objecttype>
    +            <objecttype>com.example.wcf.example</objecttype>
             </optioncategory>
             <option name="canDoSomethingWithExample">
    -           <objecttype>com.example.wcf.example</objecttype>
    +            <objecttype>com.example.wcf.example</objecttype>
             </option>
         </delete>
     </data>
     
    +
    @@ -2152,7 +2155,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/acp-menu/index.html b/5.4/package/pip/acp-menu/index.html index ef913044..22be36d4 100644 --- a/5.4/package/pip/acp-menu/index.html +++ b/5.4/package/pip/acp-menu/index.html @@ -2121,7 +2121,9 @@ the full external link otherwise.

    Optional

    The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the tab to be shown.

    Example#

    -
     1
    +
    +
    acpMenu.xml
    +
     1
      2
      3
      4
    @@ -2166,6 +2168,7 @@ the full external link otherwise.

    </data>
    +
    @@ -2174,7 +2177,7 @@ the full external link otherwise.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/acp-search-provider/index.html b/5.4/package/pip/acp-search-provider/index.html index 7139c8a6..42801c07 100644 --- a/5.4/package/pip/acp-search-provider/index.html +++ b/5.4/package/pip/acp-search-provider/index.html @@ -2034,7 +2034,9 @@ the class has to implement the wcf\system\search\acp\IACPSearchResultProvi

    Optional

    Determines at which position of the search result list the provided results are shown.

    Example#

    -
    1
    +
    +
    acpSearchProvider.xml
    +
    1
     2
     3
     4
    @@ -2053,6 +2055,7 @@ the class has to implement the wcf\system\search\acp\IACPSearchResultProvi
     </data>
     
    +
    @@ -2061,7 +2064,7 @@ the class has to implement the wcf\system\search\acp\IACPSearchResultProvi
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/bbcode/index.html b/5.4/package/pip/bbcode/index.html index cfdf14a4..76c06636 100644 --- a/5.4/package/pip/bbcode/index.html +++ b/5.4/package/pip/bbcode/index.html @@ -2235,7 +2235,9 @@ The name attribute is a 0-indexed integer.

    Specifies whether the text content of the BBCode should become this attribute's value.

    Example#

    -
     1
    +
    +
    bbcode.xml
    +
     1
      2
      3
      4
    @@ -2256,30 +2258,31 @@ The name attribute is a 0-indexed integer.

    19 20 21 -22
    <?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>
    +22
    <?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>
     
    +
    @@ -2288,7 +2291,7 @@ The name attribute is a 0-indexed integer.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/box/index.html b/5.4/package/pip/box/index.html index b334e83b..0f5c6a61 100644 --- a/5.4/package/pip/box/index.html +++ b/5.4/package/pip/box/index.html @@ -2276,7 +2276,9 @@

    <content>#

    The content that should be used to populate the box, only used and required if the boxType equals text, html and tpl.

    Example#

    -
     1
    +
    +
    box.xml
    +
     1
      2
      3
      4
    @@ -2335,6 +2337,7 @@
     </data>
     
    +
    @@ -2343,7 +2346,7 @@
    - Last update: 2021-04-20 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/clipboard-action/index.html b/5.4/package/pip/clipboard-action/index.html index 74df7a6f..247e78b4 100644 --- a/5.4/package/pip/clipboard-action/index.html +++ b/5.4/package/pip/clipboard-action/index.html @@ -2050,7 +2050,9 @@ The class has to implement the wcf\system\clipboard\action\IClipboardActio

    Optional

    Determines at which position of the clipboard action list the action is shown.

    Example#

    -
     1
    +
    +
    clipboardAction.xml
    +
     1
      2
      3
      4
    @@ -2103,6 +2105,7 @@ The class has to implement the wcf\system\clipboard\action\IClipboardActio
     </data>
     
    +
    @@ -2111,7 +2114,7 @@ The class has to implement the wcf\system\clipboard\action\IClipboardActio
    - Last update: 2021-01-24 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/core-object/index.html b/5.4/package/pip/core-object/index.html index ac9f6404..51edfff0 100644 --- a/5.4/package/pip/core-object/index.html +++ b/5.4/package/pip/core-object/index.html @@ -2016,7 +2016,9 @@

    <objectname>#

    The fully qualified class name of the class.

    Example#

    -
    1
    +
    +
    coreObject.xml
    +
    1
     2
     3
     4
    @@ -2033,6 +2035,8 @@
     </data>
     
    +
    +

    This object can be accessed in templates via $__wcf->getExampleHandler() (in general: the method name begins with get and ends with the unqualified class name).

    @@ -2042,7 +2046,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/cronjob/index.html b/5.4/package/pip/cronjob/index.html index 4d8b99a9..469cd419 100644 --- a/5.4/package/pip/cronjob/index.html +++ b/5.4/package/pip/cronjob/index.html @@ -2104,7 +2104,9 @@ They correspond to the fields in crontab(5) of a cron daemon and ac

    <options>#

    The options element can contain a comma-separated list of options of which at least one needs to be enabled for the template listener to be executed.

    Example#

    -
     1
    +
    +
    cronjob.xml
    +
     1
      2
      3
      4
    @@ -2139,6 +2141,7 @@ They correspond to the fields in crontab(5) of a cron daemon and ac
     </data>
     
    +
    @@ -2147,7 +2150,7 @@ They correspond to the fields in crontab(5) of a cron daemon and ac
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/event-listener/index.html b/5.4/package/pip/event-listener/index.html index 5266d72d..53bdbec9 100644 --- a/5.4/package/pip/event-listener/index.html +++ b/5.4/package/pip/event-listener/index.html @@ -2144,7 +2144,9 @@ If the nice value of two event listeners is equal, they are sorted by the listen

    <permissions>#

    The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the event listener to be executed.

    Example#

    -
     1
    +
    +
    eventListener.xml
    +
     1
      2
      3
      4
    @@ -2189,6 +2191,7 @@ If the nice value of two event listeners is equal, they are sorted by the listen
     </data>
     
    +
    @@ -2197,7 +2200,7 @@ If the nice value of two event listeners is equal, they are sorted by the listen
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/language/index.html b/5.4/package/pip/language/index.html index 6d5fd48c..bac06143 100644 --- a/5.4/package/pip/language/index.html +++ b/5.4/package/pip/language/index.html @@ -2054,19 +2054,22 @@

    The text content of the <item> node is the value of the language item. Language items that are not in the wcf.global category support template scripting.

    Example#

    -
    1
    +
    +
    language/en.xml
    +
    1
     2
     3
     4
     5
     6
    <?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">
    +<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>
     
    +
    @@ -2075,7 +2078,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/media-provider/index.html b/5.4/package/pip/media-provider/index.html index 638ab884..74b9eb56 100644 --- a/5.4/package/pip/media-provider/index.html +++ b/5.4/package/pip/media-provider/index.html @@ -2074,7 +2074,9 @@

    Replacement HTML that gets populated using the captured matches in <regex>, variables are accessed as {$VariableName}. For example, the capture group (?P<ID>...) is accessed using {$ID}.

    Example#

    -
     1
    +
    +
    mediaProvider.xml
    +
     1
      2
      3
      4
    @@ -2097,28 +2099,29 @@
     21
     22
    <?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>
    +    <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>
     
    +
    @@ -2127,7 +2130,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/menu-item/index.html b/5.4/package/pip/menu-item/index.html index 08fc7b95..83eac848 100644 --- a/5.4/package/pip/menu-item/index.html +++ b/5.4/package/pip/menu-item/index.html @@ -2051,7 +2051,9 @@

    <page>#

    The page that the link should point to, requires the internal identifier set by creating a page through the page.xml.

    Example#

    -
     1
    +
    +
    menuItem.xml
    +
     1
      2
      3
      4
    @@ -2082,6 +2084,7 @@
     </data>
     
    +
    @@ -2090,7 +2093,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/menu/index.html b/5.4/package/pip/menu/index.html index 6818c40a..43b8cba2 100644 --- a/5.4/package/pip/menu/index.html +++ b/5.4/package/pip/menu/index.html @@ -2042,7 +2042,9 @@
  • cssClassName
  • Example#

    -
     1
    +
    +
    menu.xml
    +
     1
      2
      3
      4
    @@ -2083,6 +2085,7 @@
     </data>
     
    +
    @@ -2091,7 +2094,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/object-type-definition/index.html b/5.4/package/pip/object-type-definition/index.html index b7bd0b55..7cdc9ce0 100644 --- a/5.4/package/pip/object-type-definition/index.html +++ b/5.4/package/pip/object-type-definition/index.html @@ -2024,7 +2024,9 @@ Posts are then registered as an object type, implementing the “taggable conten

    Optional

    The name of the PHP interface objectTypes have to implement.

    Example#

    -
    1
    +
    +
    objectTypeDefinition.xml
    +
    1
     2
     3
     4
    @@ -2043,6 +2045,7 @@ Posts are then registered as an object type, implementing the “taggable conten
     </data>
     
    +
    @@ -2051,7 +2054,7 @@ Posts are then registered as an object type, implementing the “taggable conten
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/object-type/index.html b/5.4/package/pip/object-type/index.html index 9425a58f..b1d945e7 100644 --- a/5.4/package/pip/object-type/index.html +++ b/5.4/package/pip/object-type/index.html @@ -2052,7 +2052,9 @@ the class has to implement the <interfacename> interface of t

    Additional fields may be defined for specific definitions of object types. Refer to the documentation of these for further explanation.

    Example#

    -
     1
    +
    +
    objectType.xml
    +
     1
      2
      3
      4
    @@ -2075,6 +2077,7 @@ Refer to the documentation of these for further explanation.

    </data>
    +
    @@ -2083,7 +2086,7 @@ Refer to the documentation of these for further explanation.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/option/index.html b/5.4/package/pip/option/index.html index cfded9ac..9d2e821e 100644 --- a/5.4/package/pip/option/index.html +++ b/5.4/package/pip/option/index.html @@ -2362,7 +2362,9 @@ Descriptions are only relevant for categories whose parent has a parent itself,

    If you install an option named module_example, you have to provide the language item wcf.acp.option.module_example, which is used as a label for setting the option value. If you want to provide an optional description of the option, you have to provide the language item wcf.acp.option.module_example.description.

    Example#

    -
     1
    +
    +
    option.xml
    +
     1
      2
      3
      4
    @@ -2432,7 +2434,7 @@ If you want to provide an optional description of the option, you have to provid
                     <optiontype>select</optiontype>
                     <defaultvalue>DESC</defaultvalue>
                     <selectoptions>ASC:wcf.global.sortOrder.ascending
    -DESC:wcf.global.sortOrder.descending</selectoptions>
    +                    DESC:wcf.global.sortOrder.descending</selectoptions>
                 </option>
             </options>
         </import>
    @@ -2443,6 +2445,7 @@ DESC:wcf.global.sortOrder.descending</selectoptions></data>
     
    +
    @@ -2451,7 +2454,7 @@ DESC:wcf.global.sortOrder.descending</selectoptions> - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/page/index.html b/5.4/package/pip/page/index.html index 3ce233e5..cfb751c2 100644 --- a/5.4/package/pip/page/index.html +++ b/5.4/package/pip/page/index.html @@ -2262,7 +2262,9 @@ page would be presented with a permission denied message.

    <content>#

    The content that should be used to populate the page, only used and required if the pageType equals text, html and tpl.

    Example#

    -
     1
    +
    +
    page.xml
    +
     1
      2
      3
      4
    @@ -2311,6 +2313,7 @@ page would be presented with a permission denied message.

    </data>
    +
    @@ -2319,7 +2322,7 @@ page would be presented with a permission denied message.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/pip/index.html b/5.4/package/pip/pip/index.html index f0865cd4..446eb62d 100644 --- a/5.4/package/pip/pip/index.html +++ b/5.4/package/pip/pip/index.html @@ -1991,7 +1991,9 @@

    The package installation plugin’s class file must be installed into the wcf application and must not include classes outside the \wcf\* hierarchy to allow for proper uninstallation!

    Example#

    -
    1
    +
    +
    packageInstallationPlugin.xml
    +
    1
     2
     3
     4
    @@ -2010,6 +2012,7 @@
     </data>
     
    +
    @@ -2018,7 +2021,7 @@
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/smiley/index.html b/5.4/package/pip/smiley/index.html index e4907ddd..bf23fd95 100644 --- a/5.4/package/pip/smiley/index.html +++ b/5.4/package/pip/smiley/index.html @@ -2071,7 +2071,9 @@ Aliases must be separated by a line feed character (\n, U+000A).

    Optional

    Determines at which position of the smiley list the smiley is shown.

    Example#

    -
     1
    +
    +
    smiley.xml
    +
     1
      2
      3
      4
    @@ -2096,6 +2098,7 @@ Aliases must be separated by a line feed character (\n, U+000A).

    </data>
    +
    @@ -2104,7 +2107,7 @@ Aliases must be separated by a line feed character (\n, U+000A).

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/sql/index.html b/5.4/package/pip/sql/index.html index c23df833..9e280aaa 100644 --- a/5.4/package/pip/sql/index.html +++ b/5.4/package/pip/sql/index.html @@ -2142,7 +2142,9 @@ If you really need triggers, you should consider adding them by custom SQL queri

    Example content:

    -
     1
    +
    +
    install.sql
    +
     1
      2
      3
      4
    @@ -2163,6 +2165,7 @@ If you really need triggers, you should consider adding them by custom SQL queri
     ALTER TABLE wcf1_foo_bar ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
     
    +
    @@ -2171,7 +2174,7 @@ If you really need triggers, you should consider adding them by custom SQL queri
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/template-listener/index.html b/5.4/package/pip/template-listener/index.html index f9e4a1d2..09698748 100644 --- a/5.4/package/pip/template-listener/index.html +++ b/5.4/package/pip/template-listener/index.html @@ -2131,7 +2131,9 @@ If the nice value of two template listeners is equal, the order is undefined.

    Optional

    The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the template listener to be executed.

    Example#

    -
     1
    +
    +
    templateListener.xml
    +
     1
      2
      3
      4
    @@ -2162,6 +2164,7 @@ If the nice value of two template listeners is equal, the order is undefined.

    </data>
    +
    @@ -2170,7 +2173,7 @@ If the nice value of two template listeners is equal, the order is undefined.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/user-menu/index.html b/5.4/package/pip/user-menu/index.html index 85161394..b5b6e92b 100644 --- a/5.4/package/pip/user-menu/index.html +++ b/5.4/package/pip/user-menu/index.html @@ -2138,7 +2138,9 @@ the full external link otherwise.

    The name of the class providing the user menu item’s behaviour, the class has to implement the wcf\system\menu\user\IUserMenuItemProvider interface.

    Example#

    -
     1
    +
    +
    userMenu.xml
    +
     1
      2
      3
      4
    @@ -2183,6 +2185,7 @@ the class has to implement the wcf\system\menu\user\IUserMenuItemProvider<
     </data>
     
    +
    @@ -2191,7 +2194,7 @@ the class has to implement the wcf\system\menu\user\IUserMenuItemProvider<
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/user-notification-event/index.html b/5.4/package/pip/user-notification-event/index.html index bcb4a359..f68a12ba 100644 --- a/5.4/package/pip/user-notification-event/index.html +++ b/5.4/package/pip/user-notification-event/index.html @@ -2106,7 +2106,9 @@ Defines whether this type of email notifications is enabled by default.

    Optional

    The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the notification type to be available.

    Example#

    - For the table body\u2019s column, we need to make sure that the birthday is only show if it is actually set: 1 Adding Birthday in Front End # In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person\u2019s \u201cstatistics\u201d. 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: templates/__personListBirthdaySortField.tpl 1 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. Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable. To show the birthday, we use the following template code for the personStatistics template event, which again makes sure that the birthday is only shown if it is actually set: templates/__personListBirthday.tpl 1 2 3 4 { if $person -> birthday }
    { lang } wcf.person.birthday { /lang }
    { @ $person -> birthday | strtotime | date }
    { /if } templateListener.xml # The following code shows the templateListener.xml file used to install all mentioned template listeners: templateListener.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 columnHeads admin {lang}wcf.person.birthday{/lang}]]> personList columns admin {if $person->birthday}{@$person->birthday|strtotime|date}{/if}]]> personList personStatistics user personList sortField user personList In cases where a template is used, we simply use the include syntax to load the template. eventListener.xml # 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. eventListener.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 admin wcf\\acp\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener admin wcf\\acp\\form\\PersonAddForm createForm wcf\\system\\event\\listener\\BirthdayPersonAddFormListener 1 user wcf\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener 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 ): package.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 WoltLab Suite Core Tutorial: People (Birthday) Adds a birthday field to the people management system as part of a tutorial to create packages. 5.4.0 2021-04-16 WoltLab GmbH http://www.woltlab.com com.woltlab.wcf com.woltlab.wcf.people com.woltlab.wcf acp/database/install_com.woltlab.wcf.people.birthday.php This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people. The complete source code of this part can be found on GitHub .","title":"Part 2"},{"location":"tutorial/series/part_2/#part-2-event-and-template-listeners","text":"In the first part of this tutorial series, we have created the base structure of our people management package. In further parts, we will use the package of the first part as a basis to directly add new features. In order to explain how event listeners and template works, however, we will not directly adding a new feature to the package by altering it in this part, but we will assume that somebody else created the package and that we want to extend it the \u201ccorrect\u201d way by creating a plugin. The goal of the small plugin that will be created in this part is to add the birthday of the managed people. As in the first part, we will not bother with careful validation of the entered date but just make sure that it is a valid date.","title":"Part 2: Event and Template Listeners"},{"location":"tutorial/series/part_2/#package-functionality","text":"The package should provide the following possibilities/functions: List person\u2019s birthday (if set) in people list in the ACP Sort people list by birthday in the ACP Add or remove birthday when adding or editing person List person\u2019s birthday (if set) in people list in the front end Sort people list by birthday in the front end","title":"Package Functionality"},{"location":"tutorial/series/part_2/#used-components","text":"We will use the following package installation plugins: database package installation plugin , eventListener package installation plugin , file package installation plugin , language package installation plugin , template package installation plugin , templateListener package installation plugin . For more information about the event system, please refer to the dedicated page on events .","title":"Used Components"},{"location":"tutorial/series/part_2/#package-structure","text":"The package will have the following file structure: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u251c\u2500\u2500 eventListener.xml \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.birthday.php \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 system \u2502 \u2514\u2500\u2500 event \u2502 \u2514\u2500\u2500 listener \u2502 \u251c\u2500\u2500 BirthdayPersonAddFormListener.class.php \u2502 \u2514\u2500\u2500 BirthdaySortFieldPersonListPageListener.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 templateListener.xml \u2514\u2500\u2500 templates \u251c\u2500\u2500 __personListBirthday.tpl \u2514\u2500\u2500 __personListBirthdaySortField.tpl","title":"Package Structure"},{"location":"tutorial/series/part_2/#extending-person-model","text":"The existing model of a person only contains the person\u2019s 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 : files/acp/database/install_com.woltlab.wcf.people.birthday.php 1 2 3 4 5 6 7 8 9 10 11 columns ([ DateDatabaseTableColumn :: create ( 'birthday' ), ]), ]; If we have a Person object , 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 .","title":"Extending Person Model"},{"location":"tutorial/series/part_2/#setting-birthday-in-acp","text":"To set the birthday of a person, we only have to add another form field with an event listener: files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class BirthdayPersonAddFormListener extends AbstractEventListener { /** * @see AbstractFormBuilderForm::createForm() */ protected function onCreateForm ( PersonAddForm $form ) : void { /** @var FormContainer $dataContainer */ $dataContainer = $form -> form -> getNodeById ( 'data' ); $dataContainer -> appendChild ( DateFormField :: create ( 'birthday' ) -> label ( 'wcf.person.birthday' ) -> saveValueFormat ( 'Y-m-d' ) -> nullable () ); } } registered via 1 2 3 4 5 6 7 admin wcf\\acp\\form\\PersonAddForm createForm wcf\\system\\event\\listener\\BirthdayPersonAddFormListener 1 in eventListener.xml , see below . As BirthdayPersonAddFormListener extends AbstractEventListener and as the name of relevant event is createForm , AbstractEventListener internally automatically calls onCreateForm() with the event object as the parameter. It is important to set 1 so that the event listener is also executed for PersonEditForm , which extends PersonAddForm . The language item wcf.person.birthday used in the label is the only new one for this package: language/de.xml 1 2 3 4 5 6 < 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/5.4/language.xsd\" languagecode = \"de\" > < category name = \"wcf.person\" > < item name = \"wcf.person.birthday\" > language/en.xml 1 2 3 4 5 6 < 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/5.4/language.xsd\" languagecode = \"en\" > < category name = \"wcf.person\" > < item name = \"wcf.person.birthday\" > ","title":"Setting Birthday in ACP"},{"location":"tutorial/series/part_2/#adding-birthday-table-column-in-acp","text":"To add a birthday column to the person list page in the ACP, we need three parts: an event listener that makes the birthday database table column a valid sort field, a template listener that adds the birthday column to the table\u2019s head, and a template listener that adds the birthday column to the table\u2019s rows. The first part is a very simple class: files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class BirthdaySortFieldPersonListPageListener extends AbstractEventListener { /** * @see SortablePage::validateSortField() */ public function onValidateSortField ( SortablePage $page ) : void { $page -> validSortFields [] = 'birthday' ; } } 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. As the relevant template codes are only one line each, we will simply put them directly in the templateListener.xml file that will be shown later on . The code for the table head is similar to the other th elements: 1 For the table body\u2019s column, we need to make sure that the birthday is only show if it is actually set: 1 ","title":"Adding Birthday Table Column in ACP"},{"location":"tutorial/series/part_2/#adding-birthday-in-front-end","text":"In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person\u2019s \u201cstatistics\u201d. 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: templates/__personListBirthdaySortField.tpl 1 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. Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable. To show the birthday, we use the following template code for the personStatistics template event, which again makes sure that the birthday is only shown if it is actually set: templates/__personListBirthday.tpl 1 2 3 4 { if $person -> birthday }
    { lang } wcf.person.birthday { /lang }
    { @ $person -> birthday | strtotime | date }
    { /if }","title":"Adding Birthday in Front End"},{"location":"tutorial/series/part_2/#templatelistenerxml","text":"The following code shows the templateListener.xml file used to install all mentioned template listeners: templateListener.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 columnHeads admin {lang}wcf.person.birthday{/lang}]]> personList columns admin {if $person->birthday}{@$person->birthday|strtotime|date}{/if}]]> personList personStatistics user personList sortField user personList In cases where a template is used, we simply use the include syntax to load the template.","title":"templateListener.xml"},{"location":"tutorial/series/part_2/#eventlistenerxml","text":"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. eventListener.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 admin wcf\\acp\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener admin wcf\\acp\\form\\PersonAddForm createForm wcf\\system\\event\\listener\\BirthdayPersonAddFormListener 1 user wcf\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener ","title":"eventListener.xml"},{"location":"tutorial/series/part_2/#packagexml","text":"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 ): package.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 WoltLab Suite Core Tutorial: People (Birthday) Adds a birthday field to the people management system as part of a tutorial to create packages. 5.4.0 2021-04-16 WoltLab GmbH http://www.woltlab.com com.woltlab.wcf com.woltlab.wcf.people com.woltlab.wcf acp/database/install_com.woltlab.wcf.people.birthday.php This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people. The complete source code of this part can be found on GitHub .","title":"package.xml"},{"location":"tutorial/series/part_3/","text":"Part 3: Person Page and Comments # In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details. To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite\u2019s reusable comment functionality. Package Functionality # In addition to the existing functions from part 1 , the package will provide the following possibilities/functions after this part of the tutorial: Details page for each person linked in the front end person list Comment on people on their respective page (can be disabled per person) User online location for person details page with name and link to person details page Create menu items linking to specific person details pages Used Components # In addition to the components used in part 1 , we will use the objectType package installation plugin , use the comment API , create a runtime cache , and create a page handler. Package Structure # The complete package will have the following file structure (including the files from part 1 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u251c\u2500\u2500 acpMenu.xml \u251c\u2500\u2500 acptemplates \u2502 \u251c\u2500\u2500 personAdd.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u2514\u2500\u2500 lib \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u251c\u2500\u2500 form \u2502 \u2502 \u2502 \u251c\u2500\u2500 PersonAddForm.class.php \u2502 \u2502 \u2502 \u2514\u2500\u2500 PersonEditForm.class.php \u2502 \u2502 \u2514\u2500\u2500 page \u2502 \u2502 \u2514\u2500\u2500 PersonListPage.class.php \u2502 \u251c\u2500\u2500 data \u2502 \u2502 \u2514\u2500\u2500 person \u2502 \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonAction.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonEditor.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonList.class.php \u2502 \u251c\u2500\u2500 page \u2502 \u2502 \u251c\u2500\u2500 PersonListPage.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonPage.class.php \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 cache \u2502 \u2502 \u2514\u2500\u2500 runtime \u2502 \u2502 \u2514\u2500\u2500 PersonRuntimeCache.class.php \u2502 \u251c\u2500\u2500 comment \u2502 \u2502 \u2514\u2500\u2500 manager \u2502 \u2502 \u2514\u2500\u2500 PersonCommentManager.class.php \u2502 \u2514\u2500\u2500 page \u2502 \u2514\u2500\u2500 handler \u2502 \u2514\u2500\u2500 PersonPageHandler.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 menuItem.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 page.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u2514\u2500\u2500 userGroupOption.xml We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new Person::getLink() method and new language items have been added. For all changes, please refer to the source code on GitHub . Runtime Cache # To reduce the number of database queries when different APIs require person objects, we implement a runtime cache for people: files/lib/system/cache/runtime/PersonRuntimeCache.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 * @package WoltLabSuite\\Core\\System\\Cache\\Runtime * * @method Person[] getCachedObjects() * @method Person getObject($objectID) * @method Person[] getObjects(array $objectIDs) */ class PersonRuntimeCache extends AbstractRuntimeCache { /** * @inheritDoc */ protected $listClassName = PersonList :: class ; } 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 : objectType.xml 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.person.personComment com.woltlab.wcf.comment.commentableContent wcf\\system\\comment\\manager\\PersonCommentManager The PersonCommentManager class extended ICommentManager \u2019s default implementation AbstractCommentManager : files/lib/system/comment/manager/PersonCommentManager.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 * @package WoltLabSuite\\Core\\System\\Comment\\Manager */ class PersonCommentManager extends AbstractCommentManager { /** * @inheritDoc */ protected $permissionAdd = 'user.person.canAddComment' ; /** * @inheritDoc */ protected $permissionAddWithoutModeration = 'user.person.canAddCommentWithoutModeration' ; /** * @inheritDoc */ protected $permissionCanModerate = 'mod.person.canModerateComment' ; /** * @inheritDoc */ protected $permissionDelete = 'user.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionEdit = 'user.person.canEditComment' ; /** * @inheritDoc */ protected $permissionModDelete = 'mod.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionModEdit = 'mod.person.canEditComment' ; /** * @inheritDoc */ public function getLink ( $objectTypeID , $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * @inheritDoc */ public function isAccessible ( $objectID , $validateWritePermission = false ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function getTitle ( $objectTypeID , $objectID , $isResponse = false ) { if ( $isResponse ) { return WCF :: getLanguage () -> get ( 'wcf.person.commentResponse' ); } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.person.comment' ); } /** * @inheritDoc */ public function updateCounter ( $objectID , $value ) { ( new PersonEditor ( new Person ( $objectID ))) -> updateCounters ([ 'comments' => $value ]); } } First, the system is told the names of the permissions via the $permission* properties. More information about comment permissions can be found here . The getLink() method returns the link to the person with the passed comment id. As in isAccessible() , PersonRuntimeCache is used to potentially save database queries. The isAccessible() method checks if the active user can access the relevant person. As we do not have any special restrictions for accessing people, we only need to check if the person exists. The getTitle() method returns the title used for comments and responses, which is just a generic language item in this case. The updateCounter() updates the comments\u2019 counter of the person. We have added a new comments database table column to the wcf1_person database table in order to keep track on the number of comments. Additionally, we have added a new enableComments database table column to the wcf1_person database table whose value can be set when creating or editing a person in the ACP. With this option, comments on individual people can be disabled. Liking comments is already built-in and only requires some extra code in the PersonPage class for showing the likes of pre-loaded comments. Person Page # PersonPage # files/lib/page/PersonPage.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 * @package WoltLabSuite\\Core\\Page */ class PersonPage extends AbstractPage { /** * list of comments * @var StructuredCommentList */ public $commentList ; /** * person comment manager object * @var PersonCommentManager */ public $commentManager ; /** * id of the person comment object type * @var integer */ public $commentObjectTypeID = 0 ; /** * shown person * @var Person */ public $person ; /** * id of the shown person * @var integer */ public $personID = 0 ; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'commentCanAdd' => WCF :: getSession () -> getPermission ( 'user.person.canAddComment' ), 'commentList' => $this -> commentList , 'commentObjectTypeID' => $this -> commentObjectTypeID , 'lastCommentTime' => $this -> commentList ? $this -> commentList -> getMinCommentTime () : 0 , 'likeData' => MODULE_LIKE && $this -> commentList ? $this -> commentList -> getLikeData () : [], 'person' => $this -> person , ]); } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( $this -> person -> enableComments ) { $this -> commentObjectTypeID = CommentHandler :: getInstance () -> getObjectTypeID ( 'com.woltlab.wcf.person.personComment' ); $this -> commentManager = CommentHandler :: getInstance () -> getObjectType ( $this -> commentObjectTypeID ) -> getProcessor (); $this -> commentList = CommentHandler :: getInstance () -> getCommentList ( $this -> commentManager , $this -> commentObjectTypeID , $this -> person -> personID ); } } /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) { $this -> personID = \\intval ( $_REQUEST [ 'id' ]); } $this -> person = new Person ( $this -> personID ); if ( ! $this -> person -> personID ) { throw new IllegalLinkException (); } } } The PersonPage class is similar to the PersonEditForm in the ACP in that it reads the id of the requested person from the request data and validates the id in readParameters() . The rest of the code only handles fetching the list of comments on the requested person. In readData() , this list is fetched using CommentHandler::getCommentList() if comments are enabled for the person. The assignVariables() method assigns some additional template variables like $commentCanAdd , which is 1 if the active person can add comments and is 0 otherwise, $lastCommentTime , which contains the UNIX timestamp of the last comment, and $likeData , which contains data related to the likes for the disabled comments. person.tpl # templates/person.tpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 {capture assign='pageTitle'}{$person} - {lang}wcf.person.list{/lang}{/capture} {capture assign='contentTitle'}{$person}{/capture} {include file='header'} {if $person->enableComments} {if $commentList|count || $commentCanAdd}

    {lang}wcf.person.comments{/lang} {if $person->comments}{#$person->comments}{/if}

    {include file='__commentJavaScript' commentContainerID='personCommentList'}
      personID}\" {* *}data-object-type-id=\"{@$commentObjectTypeID}\" {* *}data-comments=\"{if $person->comments}{@$commentList->countObjects()}{else}0{/if}\" {* *}data-last-comment-time=\"{@$lastCommentTime}\" {* *}> {include file='commentListAddComment' wysiwygSelector='personCommentListAddComment'} {include file='commentList'}
    {/if} {/if}
    {hascontent} {/hascontent}
    {include file='footer'} For now, the person template is still very empty and only shows the comments in the content area. The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container ul#personCommentList element for the comments shown by commentList template. The ul#personCommentList elements has five additional data- attributes required by the JavaScript API for comments for loading more comments or creating new ones. The commentListAddComment template adds the WYSIWYG support. The attribute wysiwygSelector should be the id of the comment list personCommentList with an additional AddComment suffix. page.xml # page.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 system wcf\\page\\PersonListPage Personen-Liste Person List Personen People system wcf\\page\\PersonPage wcf\\system\\page\\handler\\PersonPageHandler Person Person 1 com.woltlab.wcf.people.PersonList 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: It has a element with a class name as value. This aspect will be discussed in more detail in the next section. There are no elements because, both, the title and the content of the page are dynamically generated in the template. The tells the system that this page requires an object id to properly work, in this case a valid person id. This page has a page, the person list page. In general, the details page for any type of object that is listed on a different page has the list page as its parent. PersonPageHandler # files/lib/system/page/handler/PersonPageHandler.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 * @package WoltLabSuite\\Core\\System\\Page\\Handler */ class PersonPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler { use TOnlineLocationPageHandler ; /** * @inheritDoc */ public function getLink ( $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * Returns the textual description if a user is currently online viewing this page. * * @see IOnlineLocationPageHandler::getOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data * @return string */ public function getOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID === null ) { return '' ; } $person = PersonRuntimeCache :: getInstance () -> getObject ( $user -> pageObjectID ); if ( $person === null ) { return '' ; } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.page.onlineLocation.' . $page -> identifier , [ 'person' => $person ]); } /** * @inheritDoc */ public function isValid ( $objectID = null ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function lookup ( $searchString ) { $conditionBuilder = new PreparedStatementConditionBuilder ( false , 'OR' ); $conditionBuilder -> add ( 'person.firstName LIKE ?' , [ '%' . $searchString . '%' ]); $conditionBuilder -> add ( 'person.lastName LIKE ?' , [ '%' . $searchString . '%' ]); $personList = new PersonList (); $personList -> getConditionBuilder () -> add ( $conditionBuilder , $conditionBuilder -> getParameters ()); $personList -> readObjects (); $results = []; foreach ( $personList as $person ) { $results [] = [ 'image' => 'fa-user' , 'link' => $person -> getLink (), 'objectID' => $person -> personID , 'title' => $person -> getTitle (), ]; } return $results ; } /** * Prepares fetching all necessary data for the textual description if a user is currently online * viewing this page. * * @see IOnlineLocationPageHandler::prepareOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data */ public function prepareOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID !== null ) { PersonRuntimeCache :: getInstance () -> cacheObjectID ( $user -> pageObjectID ); } } } Like any page handler, the PersonPageHandler class has to implement the IMenuPageHandler interface, which should be done by extending the AbstractMenuPageHandler class. As we want administrators to link to specific people in menus, for example, we have to also implement the ILookupPageHandler interface by extending the AbstractLookupPageHandler class. For the ILookupPageHandler interface, we need to implement three methods: getLink($objectID) returns the link to the person page with the given id. In this case, we simply delegate this method call to the Person object returned by PersonRuntimeCache::getObject() . isValid($objectID) returns true if the person with the given id exists, otherwise false . Here, we use PersonRuntimeCache::getObject() again and check if the return value is null , which is the case for non-existing people. lookup($searchString) is used when setting up an internal link and when searching for the linked person. This method simply searches the first and last name of the people and returns an array with the person data. While the link , the objectID , and the title element are self-explanatory, the image element can either contain an HTML tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with fa- ). Additionally, the class also implements IOnlineLocationPageHandler which is used to determine the online location of users. To ensure upwards-compatibility if the IOnlineLocationPageHandler interface changes, the TOnlineLocationPageHandler trait is used. The IOnlineLocationPageHandler interface requires two methods to be implemented: getOnlineLocation(Page $page, UserOnline $user) returns the textual description of the online location. The language item for the user online locations should use the pattern wcf.page.onlineLocation.{page identifier} . prepareOnlineLocation(Page $page, UserOnline $user) is called for each user online before the getOnlineLocation() calls. In this case, calling prepareOnlineLocation() first enables us to add all relevant person ids to the person runtime cache so that for all getOnlineLocation() calls combined, only one database query is necessary to fetch all person objects. This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person. The complete source code of this part can be found on GitHub .","title":"Part 3"},{"location":"tutorial/series/part_3/#part-3-person-page-and-comments","text":"In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details. To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite\u2019s reusable comment functionality.","title":"Part 3: Person Page and Comments"},{"location":"tutorial/series/part_3/#package-functionality","text":"In addition to the existing functions from part 1 , the package will provide the following possibilities/functions after this part of the tutorial: Details page for each person linked in the front end person list Comment on people on their respective page (can be disabled per person) User online location for person details page with name and link to person details page Create menu items linking to specific person details pages","title":"Package Functionality"},{"location":"tutorial/series/part_3/#used-components","text":"In addition to the components used in part 1 , we will use the objectType package installation plugin , use the comment API , create a runtime cache , and create a page handler.","title":"Used Components"},{"location":"tutorial/series/part_3/#package-structure","text":"The complete package will have the following file structure (including the files from part 1 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u251c\u2500\u2500 acpMenu.xml \u251c\u2500\u2500 acptemplates \u2502 \u251c\u2500\u2500 personAdd.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u2514\u2500\u2500 lib \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u251c\u2500\u2500 form \u2502 \u2502 \u2502 \u251c\u2500\u2500 PersonAddForm.class.php \u2502 \u2502 \u2502 \u2514\u2500\u2500 PersonEditForm.class.php \u2502 \u2502 \u2514\u2500\u2500 page \u2502 \u2502 \u2514\u2500\u2500 PersonListPage.class.php \u2502 \u251c\u2500\u2500 data \u2502 \u2502 \u2514\u2500\u2500 person \u2502 \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonAction.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonEditor.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonList.class.php \u2502 \u251c\u2500\u2500 page \u2502 \u2502 \u251c\u2500\u2500 PersonListPage.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonPage.class.php \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 cache \u2502 \u2502 \u2514\u2500\u2500 runtime \u2502 \u2502 \u2514\u2500\u2500 PersonRuntimeCache.class.php \u2502 \u251c\u2500\u2500 comment \u2502 \u2502 \u2514\u2500\u2500 manager \u2502 \u2502 \u2514\u2500\u2500 PersonCommentManager.class.php \u2502 \u2514\u2500\u2500 page \u2502 \u2514\u2500\u2500 handler \u2502 \u2514\u2500\u2500 PersonPageHandler.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 menuItem.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 page.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u2514\u2500\u2500 userGroupOption.xml We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new Person::getLink() method and new language items have been added. For all changes, please refer to the source code on GitHub .","title":"Package Structure"},{"location":"tutorial/series/part_3/#runtime-cache","text":"To reduce the number of database queries when different APIs require person objects, we implement a runtime cache for people: files/lib/system/cache/runtime/PersonRuntimeCache.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 * @package WoltLabSuite\\Core\\System\\Cache\\Runtime * * @method Person[] getCachedObjects() * @method Person getObject($objectID) * @method Person[] getObjects(array $objectIDs) */ class PersonRuntimeCache extends AbstractRuntimeCache { /** * @inheritDoc */ protected $listClassName = PersonList :: class ; }","title":"Runtime Cache"},{"location":"tutorial/series/part_3/#comments","text":"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 : objectType.xml 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.person.personComment com.woltlab.wcf.comment.commentableContent wcf\\system\\comment\\manager\\PersonCommentManager The PersonCommentManager class extended ICommentManager \u2019s default implementation AbstractCommentManager : files/lib/system/comment/manager/PersonCommentManager.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 * @package WoltLabSuite\\Core\\System\\Comment\\Manager */ class PersonCommentManager extends AbstractCommentManager { /** * @inheritDoc */ protected $permissionAdd = 'user.person.canAddComment' ; /** * @inheritDoc */ protected $permissionAddWithoutModeration = 'user.person.canAddCommentWithoutModeration' ; /** * @inheritDoc */ protected $permissionCanModerate = 'mod.person.canModerateComment' ; /** * @inheritDoc */ protected $permissionDelete = 'user.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionEdit = 'user.person.canEditComment' ; /** * @inheritDoc */ protected $permissionModDelete = 'mod.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionModEdit = 'mod.person.canEditComment' ; /** * @inheritDoc */ public function getLink ( $objectTypeID , $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * @inheritDoc */ public function isAccessible ( $objectID , $validateWritePermission = false ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function getTitle ( $objectTypeID , $objectID , $isResponse = false ) { if ( $isResponse ) { return WCF :: getLanguage () -> get ( 'wcf.person.commentResponse' ); } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.person.comment' ); } /** * @inheritDoc */ public function updateCounter ( $objectID , $value ) { ( new PersonEditor ( new Person ( $objectID ))) -> updateCounters ([ 'comments' => $value ]); } } First, the system is told the names of the permissions via the $permission* properties. More information about comment permissions can be found here . The getLink() method returns the link to the person with the passed comment id. As in isAccessible() , PersonRuntimeCache is used to potentially save database queries. The isAccessible() method checks if the active user can access the relevant person. As we do not have any special restrictions for accessing people, we only need to check if the person exists. The getTitle() method returns the title used for comments and responses, which is just a generic language item in this case. The updateCounter() updates the comments\u2019 counter of the person. We have added a new comments database table column to the wcf1_person database table in order to keep track on the number of comments. Additionally, we have added a new enableComments database table column to the wcf1_person database table whose value can be set when creating or editing a person in the ACP. With this option, comments on individual people can be disabled. Liking comments is already built-in and only requires some extra code in the PersonPage class for showing the likes of pre-loaded comments.","title":"Comments"},{"location":"tutorial/series/part_3/#person-page","text":"","title":"Person Page"},{"location":"tutorial/series/part_3/#personpage","text":"files/lib/page/PersonPage.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 * @package WoltLabSuite\\Core\\Page */ class PersonPage extends AbstractPage { /** * list of comments * @var StructuredCommentList */ public $commentList ; /** * person comment manager object * @var PersonCommentManager */ public $commentManager ; /** * id of the person comment object type * @var integer */ public $commentObjectTypeID = 0 ; /** * shown person * @var Person */ public $person ; /** * id of the shown person * @var integer */ public $personID = 0 ; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'commentCanAdd' => WCF :: getSession () -> getPermission ( 'user.person.canAddComment' ), 'commentList' => $this -> commentList , 'commentObjectTypeID' => $this -> commentObjectTypeID , 'lastCommentTime' => $this -> commentList ? $this -> commentList -> getMinCommentTime () : 0 , 'likeData' => MODULE_LIKE && $this -> commentList ? $this -> commentList -> getLikeData () : [], 'person' => $this -> person , ]); } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( $this -> person -> enableComments ) { $this -> commentObjectTypeID = CommentHandler :: getInstance () -> getObjectTypeID ( 'com.woltlab.wcf.person.personComment' ); $this -> commentManager = CommentHandler :: getInstance () -> getObjectType ( $this -> commentObjectTypeID ) -> getProcessor (); $this -> commentList = CommentHandler :: getInstance () -> getCommentList ( $this -> commentManager , $this -> commentObjectTypeID , $this -> person -> personID ); } } /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) { $this -> personID = \\intval ( $_REQUEST [ 'id' ]); } $this -> person = new Person ( $this -> personID ); if ( ! $this -> person -> personID ) { throw new IllegalLinkException (); } } } The PersonPage class is similar to the PersonEditForm in the ACP in that it reads the id of the requested person from the request data and validates the id in readParameters() . The rest of the code only handles fetching the list of comments on the requested person. In readData() , this list is fetched using CommentHandler::getCommentList() if comments are enabled for the person. The assignVariables() method assigns some additional template variables like $commentCanAdd , which is 1 if the active person can add comments and is 0 otherwise, $lastCommentTime , which contains the UNIX timestamp of the last comment, and $likeData , which contains data related to the likes for the disabled comments.","title":"PersonPage"},{"location":"tutorial/series/part_3/#persontpl","text":"templates/person.tpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 {capture assign='pageTitle'}{$person} - {lang}wcf.person.list{/lang}{/capture} {capture assign='contentTitle'}{$person}{/capture} {include file='header'} {if $person->enableComments} {if $commentList|count || $commentCanAdd}

    {lang}wcf.person.comments{/lang} {if $person->comments}{#$person->comments}{/if}

    {include file='__commentJavaScript' commentContainerID='personCommentList'}
      personID}\" {* *}data-object-type-id=\"{@$commentObjectTypeID}\" {* *}data-comments=\"{if $person->comments}{@$commentList->countObjects()}{else}0{/if}\" {* *}data-last-comment-time=\"{@$lastCommentTime}\" {* *}> {include file='commentListAddComment' wysiwygSelector='personCommentListAddComment'} {include file='commentList'}
    {/if} {/if}
    {hascontent} {/hascontent}
    {include file='footer'} For now, the person template is still very empty and only shows the comments in the content area. The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container ul#personCommentList element for the comments shown by commentList template. The ul#personCommentList elements has five additional data- attributes required by the JavaScript API for comments for loading more comments or creating new ones. The commentListAddComment template adds the WYSIWYG support. The attribute wysiwygSelector should be the id of the comment list personCommentList with an additional AddComment suffix.","title":"person.tpl"},{"location":"tutorial/series/part_3/#pagexml","text":"page.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 system wcf\\page\\PersonListPage Personen-Liste Person List Personen People system wcf\\page\\PersonPage wcf\\system\\page\\handler\\PersonPageHandler Person Person 1 com.woltlab.wcf.people.PersonList 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: It has a element with a class name as value. This aspect will be discussed in more detail in the next section. There are no elements because, both, the title and the content of the page are dynamically generated in the template. The tells the system that this page requires an object id to properly work, in this case a valid person id. This page has a page, the person list page. In general, the details page for any type of object that is listed on a different page has the list page as its parent.","title":"page.xml"},{"location":"tutorial/series/part_3/#personpagehandler","text":"files/lib/system/page/handler/PersonPageHandler.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 * @package WoltLabSuite\\Core\\System\\Page\\Handler */ class PersonPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler { use TOnlineLocationPageHandler ; /** * @inheritDoc */ public function getLink ( $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * Returns the textual description if a user is currently online viewing this page. * * @see IOnlineLocationPageHandler::getOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data * @return string */ public function getOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID === null ) { return '' ; } $person = PersonRuntimeCache :: getInstance () -> getObject ( $user -> pageObjectID ); if ( $person === null ) { return '' ; } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.page.onlineLocation.' . $page -> identifier , [ 'person' => $person ]); } /** * @inheritDoc */ public function isValid ( $objectID = null ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function lookup ( $searchString ) { $conditionBuilder = new PreparedStatementConditionBuilder ( false , 'OR' ); $conditionBuilder -> add ( 'person.firstName LIKE ?' , [ '%' . $searchString . '%' ]); $conditionBuilder -> add ( 'person.lastName LIKE ?' , [ '%' . $searchString . '%' ]); $personList = new PersonList (); $personList -> getConditionBuilder () -> add ( $conditionBuilder , $conditionBuilder -> getParameters ()); $personList -> readObjects (); $results = []; foreach ( $personList as $person ) { $results [] = [ 'image' => 'fa-user' , 'link' => $person -> getLink (), 'objectID' => $person -> personID , 'title' => $person -> getTitle (), ]; } return $results ; } /** * Prepares fetching all necessary data for the textual description if a user is currently online * viewing this page. * * @see IOnlineLocationPageHandler::prepareOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data */ public function prepareOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID !== null ) { PersonRuntimeCache :: getInstance () -> cacheObjectID ( $user -> pageObjectID ); } } } Like any page handler, the PersonPageHandler class has to implement the IMenuPageHandler interface, which should be done by extending the AbstractMenuPageHandler class. As we want administrators to link to specific people in menus, for example, we have to also implement the ILookupPageHandler interface by extending the AbstractLookupPageHandler class. For the ILookupPageHandler interface, we need to implement three methods: getLink($objectID) returns the link to the person page with the given id. In this case, we simply delegate this method call to the Person object returned by PersonRuntimeCache::getObject() . isValid($objectID) returns true if the person with the given id exists, otherwise false . Here, we use PersonRuntimeCache::getObject() again and check if the return value is null , which is the case for non-existing people. lookup($searchString) is used when setting up an internal link and when searching for the linked person. This method simply searches the first and last name of the people and returns an array with the person data. While the link , the objectID , and the title element are self-explanatory, the image element can either contain an HTML tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with fa- ). Additionally, the class also implements IOnlineLocationPageHandler which is used to determine the online location of users. To ensure upwards-compatibility if the IOnlineLocationPageHandler interface changes, the TOnlineLocationPageHandler trait is used. The IOnlineLocationPageHandler interface requires two methods to be implemented: getOnlineLocation(Page $page, UserOnline $user) returns the textual description of the online location. The language item for the user online locations should use the pattern wcf.page.onlineLocation.{page identifier} . prepareOnlineLocation(Page $page, UserOnline $user) is called for each user online before the getOnlineLocation() calls. In this case, calling prepareOnlineLocation() first enables us to add all relevant person ids to the person runtime cache so that for all getOnlineLocation() calls combined, only one database query is necessary to fetch all person objects. This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person. The complete source code of this part can be found on GitHub .","title":"PersonPageHandler"},{"location":"tutorial/series/part_4/","text":"Part 4: Box and Box Conditions # In this part of our tutorial series, we add support for creating boxes listing people. Package Functionality # In addition to the existing functions from part 3 , the package will provide the following functionality after this part of the tutorial: Creating boxes dynamically listing people Filtering the people listed in boxes using conditions Used Components # In addition to the components used in previous parts, we will use the objectTypeDefinition package installation plugin and use the box and condition APIs. To pre-install a specific person list box, we refer to the documentation of the box package installation plugin . Package Structure # The complete package will have the following file structure ( excluding unchanged files from part 3 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u251c\u2500\u2500 files \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 box \u2502 \u2502 \u2514\u2500\u2500 PersonListBoxController.class.php \u2502 \u2514\u2500\u2500 condition \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 PersonFirstNameTextPropertyCondition.class.php \u2502 \u2514\u2500\u2500 PersonLastNameTextPropertyCondition.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 objectTypeDefinition.xml \u2514\u2500\u2500 templates \u2514\u2500\u2500 boxPersonList.tpl For all changes, please refer to the source code on GitHub . Box Controller # In addition to static boxes with fixed contents, administrators are able to create dynamic boxes with contents from the database. In our case here, we want administrators to be able to create boxes listing people. To do so, we first have to register a new object type for this person list box controller for the object type definition com.woltlab.wcf.boxController : 1 2 3 4 5 com.woltlab.wcf.personList com.woltlab.wcf.boxController wcf\\system\\box\\PersonListBoxController The com.woltlab.wcf.boxController object type definition requires the provided class to implement wcf\\system\\box\\IBoxController : files/lib/system/box/PersonListBoxController.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 * @package WoltLabSuite\\Core\\System\\Box */ class PersonListBoxController extends AbstractDatabaseObjectListBoxController { /** * @inheritDoc */ protected $conditionDefinition = 'com.woltlab.wcf.box.personList.condition' ; /** * @inheritDoc */ public $defaultLimit = 5 ; /** * @inheritDoc */ protected $sortFieldLanguageItemPrefix = 'wcf.person' ; /** * @inheritDoc */ protected static $supportedPositions = [ 'sidebarLeft' , 'sidebarRight' , ]; /** * @inheritDoc */ public $validSortFields = [ 'firstName' , 'lastName' , 'comments' , ]; /** * @inheritDoc */ public function getObjectList () { return new PersonList (); } /** * @inheritDoc */ protected function getTemplate () { return WCF :: getTPL () -> fetch ( 'boxPersonList' , 'wcf' , [ 'boxPersonList' => $this -> objectList , 'boxSortField' => $this -> sortField , 'boxPosition' => $this -> box -> position , ], true ); } } By extending AbstractDatabaseObjectListBoxController , we only have to provide minimal data ourself and rely mostly on the default implementation provided by AbstractDatabaseObjectListBoxController : As we will support conditions for the listed people, we have to set the relevant condition definition via $conditionDefinition . AbstractDatabaseObjectListBoxController already supports restricting the number of listed objects. To do so, you only have to specify the default number of listed objects via $defaultLimit . AbstractDatabaseObjectListBoxController also supports setting the sort order of the listed objects. You have to provide the supported sort fields via $validSortFields and specify the prefix used for the language items of the sort fields via $sortFieldLanguageItemPrefix so that for every $validSortField in $validSortFields , the language item {$sortFieldLanguageItemPrefix}.{$validSortField} must exist. The box system supports different positions . Each box controller specifies the positions it supports via $supportedPositions . To keep the implementation simple here as different positions might require different output in the template, we restrict ourselves to sidebars. getObjectList() returns an instance of DatabaseObjectList that is used to read the listed objects. getObjectList() itself must not call readObjects() , as AbstractDatabaseObjectListBoxController takes care of calling the method after adding the conditions and setting the sort order. getTemplate() returns the contents of the box relying on the boxPersonList template here: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
      { foreach from = $boxPersonList item = boxPerson }
    • { anchor object = $boxPerson }

      { capture assign = '__boxPersonDescription' }{ lang __optional = true } wcf.person.boxList.description. { $boxSortField }{ /lang }{ /capture } { if $__boxPersonDescription } { @ $__boxPersonDescription } { /if }
    • { /foreach }
    The template relies on a .sidebarItemList element, which is generally used for sidebar listings. (If different box positions were supported, we either have to generate different output by considering the value of $boxPosition in the template or by using different templates in getTemplate() .) One specific piece of code is the $__boxPersonDescription variable, which supports an optional description below the person's name relying on the optional language item wcf.person.boxList.description.{$boxSortField} . We only add one such language item when sorting the people by comments: In such a case, the number of comments will be shown. (When sorting by first and last name, there are no additional useful information that could be shown here, though the plugin from part 2 adding support for birthdays might also show the birthday when sorting by first or last name.) Lastly, we also provide the language item wcf.acp.box.boxController.com.woltlab.wcf.personList , which is used in the list of available box controllers. Conditions # The condition system can be used to generally filter a list of objects. In our case, the box system supports conditions to filter the objects shown in a specific box. Admittedly, our current person implementation only contains minimal data so that filtering might not make the most sense here but it will still show how to use the condition system for boxes. We will support filtering the people by their first and last name so that, for example, a box can be created listing all people with a specific first name. The first step for condition support is to register a object type definition for the relevant conditions requiring the IObjectListCondition interface: objectTypeDefinition.xml 1 2 3 4 5 6 7 8 9 com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\IObjectListCondition Next, we register the specific conditions for filtering by the first and last name using this object type condition: 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.people.firstName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonFirstNameTextPropertyCondition com.woltlab.wcf.people.lastName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonLastNameTextPropertyCondition 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: files/lib/system/condition/person/PersonFirstNameTextPropertyCondition.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 * @package WoltLabSuite\\Core\\System\\Condition */ class PersonFirstNameTextPropertyCondition extends AbstractObjectTextPropertyCondition { /** * @inheritDoc */ protected $className = Person :: class ; /** * @inheritDoc */ protected $description = 'wcf.person.condition.firstName.description' ; /** * @inheritDoc */ protected $fieldName = 'personFirstName' ; /** * @inheritDoc */ protected $label = 'wcf.person.firstName' ; /** * @inheritDoc */ protected $propertyName = 'firstName' ; /** * @inheritDoc */ protected $supportsMultipleValues = true ; } $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. 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. $description (optional), $fieldName , and $label are used in the output of the form field. (The implementation here is specific for AbstractObjectTextPropertyCondition . The wcf\\system\\condition namespace also contains several other default condition implementations.)","title":"Part 4"},{"location":"tutorial/series/part_4/#part-4-box-and-box-conditions","text":"In this part of our tutorial series, we add support for creating boxes listing people.","title":"Part 4: Box and Box Conditions"},{"location":"tutorial/series/part_4/#package-functionality","text":"In addition to the existing functions from part 3 , the package will provide the following functionality after this part of the tutorial: Creating boxes dynamically listing people Filtering the people listed in boxes using conditions","title":"Package Functionality"},{"location":"tutorial/series/part_4/#used-components","text":"In addition to the components used in previous parts, we will use the objectTypeDefinition package installation plugin and use the box and condition APIs. To pre-install a specific person list box, we refer to the documentation of the box package installation plugin .","title":"Used Components"},{"location":"tutorial/series/part_4/#package-structure","text":"The complete package will have the following file structure ( excluding unchanged files from part 3 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u251c\u2500\u2500 files \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 box \u2502 \u2502 \u2514\u2500\u2500 PersonListBoxController.class.php \u2502 \u2514\u2500\u2500 condition \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 PersonFirstNameTextPropertyCondition.class.php \u2502 \u2514\u2500\u2500 PersonLastNameTextPropertyCondition.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 objectTypeDefinition.xml \u2514\u2500\u2500 templates \u2514\u2500\u2500 boxPersonList.tpl For all changes, please refer to the source code on GitHub .","title":"Package Structure"},{"location":"tutorial/series/part_4/#box-controller","text":"In addition to static boxes with fixed contents, administrators are able to create dynamic boxes with contents from the database. In our case here, we want administrators to be able to create boxes listing people. To do so, we first have to register a new object type for this person list box controller for the object type definition com.woltlab.wcf.boxController : 1 2 3 4 5 com.woltlab.wcf.personList com.woltlab.wcf.boxController wcf\\system\\box\\PersonListBoxController The com.woltlab.wcf.boxController object type definition requires the provided class to implement wcf\\system\\box\\IBoxController : files/lib/system/box/PersonListBoxController.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 * @package WoltLabSuite\\Core\\System\\Box */ class PersonListBoxController extends AbstractDatabaseObjectListBoxController { /** * @inheritDoc */ protected $conditionDefinition = 'com.woltlab.wcf.box.personList.condition' ; /** * @inheritDoc */ public $defaultLimit = 5 ; /** * @inheritDoc */ protected $sortFieldLanguageItemPrefix = 'wcf.person' ; /** * @inheritDoc */ protected static $supportedPositions = [ 'sidebarLeft' , 'sidebarRight' , ]; /** * @inheritDoc */ public $validSortFields = [ 'firstName' , 'lastName' , 'comments' , ]; /** * @inheritDoc */ public function getObjectList () { return new PersonList (); } /** * @inheritDoc */ protected function getTemplate () { return WCF :: getTPL () -> fetch ( 'boxPersonList' , 'wcf' , [ 'boxPersonList' => $this -> objectList , 'boxSortField' => $this -> sortField , 'boxPosition' => $this -> box -> position , ], true ); } } By extending AbstractDatabaseObjectListBoxController , we only have to provide minimal data ourself and rely mostly on the default implementation provided by AbstractDatabaseObjectListBoxController : As we will support conditions for the listed people, we have to set the relevant condition definition via $conditionDefinition . AbstractDatabaseObjectListBoxController already supports restricting the number of listed objects. To do so, you only have to specify the default number of listed objects via $defaultLimit . AbstractDatabaseObjectListBoxController also supports setting the sort order of the listed objects. You have to provide the supported sort fields via $validSortFields and specify the prefix used for the language items of the sort fields via $sortFieldLanguageItemPrefix so that for every $validSortField in $validSortFields , the language item {$sortFieldLanguageItemPrefix}.{$validSortField} must exist. The box system supports different positions . Each box controller specifies the positions it supports via $supportedPositions . To keep the implementation simple here as different positions might require different output in the template, we restrict ourselves to sidebars. getObjectList() returns an instance of DatabaseObjectList that is used to read the listed objects. getObjectList() itself must not call readObjects() , as AbstractDatabaseObjectListBoxController takes care of calling the method after adding the conditions and setting the sort order. getTemplate() returns the contents of the box relying on the boxPersonList template here: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
      { foreach from = $boxPersonList item = boxPerson }
    • { anchor object = $boxPerson }

      { capture assign = '__boxPersonDescription' }{ lang __optional = true } wcf.person.boxList.description. { $boxSortField }{ /lang }{ /capture } { if $__boxPersonDescription } { @ $__boxPersonDescription } { /if }
    • { /foreach }
    The template relies on a .sidebarItemList element, which is generally used for sidebar listings. (If different box positions were supported, we either have to generate different output by considering the value of $boxPosition in the template or by using different templates in getTemplate() .) One specific piece of code is the $__boxPersonDescription variable, which supports an optional description below the person's name relying on the optional language item wcf.person.boxList.description.{$boxSortField} . We only add one such language item when sorting the people by comments: In such a case, the number of comments will be shown. (When sorting by first and last name, there are no additional useful information that could be shown here, though the plugin from part 2 adding support for birthdays might also show the birthday when sorting by first or last name.) Lastly, we also provide the language item wcf.acp.box.boxController.com.woltlab.wcf.personList , which is used in the list of available box controllers.","title":"Box Controller"},{"location":"tutorial/series/part_4/#conditions","text":"The condition system can be used to generally filter a list of objects. In our case, the box system supports conditions to filter the objects shown in a specific box. Admittedly, our current person implementation only contains minimal data so that filtering might not make the most sense here but it will still show how to use the condition system for boxes. We will support filtering the people by their first and last name so that, for example, a box can be created listing all people with a specific first name. The first step for condition support is to register a object type definition for the relevant conditions requiring the IObjectListCondition interface: objectTypeDefinition.xml 1 2 3 4 5 6 7 8 9 com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\IObjectListCondition Next, we register the specific conditions for filtering by the first and last name using this object type condition: 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.people.firstName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonFirstNameTextPropertyCondition com.woltlab.wcf.people.lastName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonLastNameTextPropertyCondition 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: files/lib/system/condition/person/PersonFirstNameTextPropertyCondition.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 * @package WoltLabSuite\\Core\\System\\Condition */ class PersonFirstNameTextPropertyCondition extends AbstractObjectTextPropertyCondition { /** * @inheritDoc */ protected $className = Person :: class ; /** * @inheritDoc */ protected $description = 'wcf.person.condition.firstName.description' ; /** * @inheritDoc */ protected $fieldName = 'personFirstName' ; /** * @inheritDoc */ protected $label = 'wcf.person.firstName' ; /** * @inheritDoc */ protected $propertyName = 'firstName' ; /** * @inheritDoc */ protected $supportsMultipleValues = true ; } $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. 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. $description (optional), $fieldName , and $label are used in the output of the form field. (The implementation here is specific for AbstractObjectTextPropertyCondition . The wcf\\system\\condition namespace also contains several other default condition implementations.)","title":"Conditions"},{"location":"tutorial/series/part_5/","text":"Part 5: Person Information # This part of our tutorial series lays the foundation for future parts in which we will be using additional APIs, which we have not used in this series yet. To make use of those APIs, we need content generated by users in the frontend. Package Functionality # In addition to the existing functions from part 4 , the package will provide the following functionality after this part of the tutorial: Users are able to add information on the people in the frontend. Users are able to edit and delete the pieces of information they added. Moderators are able to edit and delete all pieces of information. Used Components # In addition to the components used in previous parts, we will use the form builder API to create forms shown in dialogs instead of dedicated pages and we will, for the first time, add TypeScript code . Package Structure # The package will have the following file structure excluding unchanged files from previous parts: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u251c\u2500\u2500 js \u2502 \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2502 \u2514\u2500\u2500 Core \u2502 \u2502 \u2514\u2500\u2500 Controller \u2502 \u2502 \u2514\u2500\u2500 Person.js \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 data \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2514\u2500\u2500 information \u2502 \u251c\u2500\u2500 PersonInformation.class.php \u2502 \u251c\u2500\u2500 PersonInformationAction.class.php \u2502 \u251c\u2500\u2500 PersonInformationEditor.class.php \u2502 \u2514\u2500\u2500 PersonInformationList.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 ts \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2514\u2500\u2500 Core \u2502 \u2514\u2500\u2500 Controller \u2502 \u2514\u2500\u2500 Person.ts \u2514\u2500\u2500 userGroupOption.xml For all changes, please refer to the source code on GitHub . Miscellaneous # Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on: Several new user group options and the relevant language items have been added related to creating, editing, and deleting information: mod.person.canEditInformation and mod.person.canDeleteInformation are moderative permissions to edit and delete any piece of information, regardless of who created it. user.person.canAddInformation is the permission for users to add new pieces of information. user.person.canEditInformation and user.person.canDeleteInformation are the user permissions to edit and the piece of information they created. The actual information text will be entered via a WYSIWYG editor, which requires an object type of the definition com.woltlab.wcf.message : com.woltlab.wcf.people.information . personList.tpl has been adjusted to show the number of pieces of information in the person statistics section. We have not updated the person list box to also support sorting by the number of pieces of information added for each person. Person Information Model # The PHP file with the database layout has been updated as follows: files/acp/database/install_com.woltlab.wcf.people.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 columns ([ ObjectIdDatabaseTableColumn :: create ( 'personID' ), NotNullVarchar255DatabaseTableColumn :: create ( 'firstName' ), NotNullVarchar255DatabaseTableColumn :: create ( 'lastName' ), NotNullInt10DatabaseTableColumn :: create ( 'informationCount' ) -> defaultValue ( 0 ), SmallintDatabaseTableColumn :: create ( 'comments' ) -> length ( 5 ) -> notNull () -> defaultValue ( 0 ), DefaultTrueBooleanDatabaseTableColumn :: create ( 'enableComments' ), ]), DatabaseTable :: create ( 'wcf1_person_information' ) -> columns ([ ObjectIdDatabaseTableColumn :: create ( 'informationID' ), NotNullInt10DatabaseTableColumn :: create ( 'personID' ), TextDatabaseTableColumn :: create ( 'information' ), IntDatabaseTableColumn :: create ( 'userID' ) -> length ( 10 ), NotNullVarchar255DatabaseTableColumn :: create ( 'username' ), VarcharDatabaseTableColumn :: create ( 'ipAddress' ) -> length ( 39 ) -> notNull ( true ) -> defaultValue ( '' ), NotNullInt10DatabaseTableColumn :: create ( 'time' ), ]) -> foreignKeys ([ DatabaseTableForeignKey :: create () -> columns ([ 'personID' ]) -> referencedTable ( 'wcf1_person' ) -> referencedColumns ([ 'personID' ]) -> onDelete ( 'CASCADE' ), DatabaseTableForeignKey :: create () -> columns ([ 'userID' ]) -> referencedTable ( 'wcf1_user' ) -> referencedColumns ([ 'userID' ]) -> onDelete ( 'SET NULL' ), ]), ]; 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 . files/lib/data/person/information/PersonInformation.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @property-read int $informationID unique id of the information * @property-read int $personID id of the person the information belongs to * @property-read string $information information text * @property-read int|null $userID id of the user who added the information or `null` if the user no longer exists * @property-read string $username name of the user who added the information * @property-read int $time timestamp at which the information was created */ class PersonInformation extends DatabaseObject { /** * Returns `true` if the active user can delete this piece of information and `false` otherwise. */ public function canDelete () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canDeleteInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canDeleteInformation' ); } /** * Returns `true` if the active user can edit this piece of information and `false` otherwise. */ public function canEdit () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canEditInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canEditInformation' ); } /** * Returns the formatted information. */ public function getFormattedInformation () : string { $processor = new HtmlOutputProcessor (); $processor -> process ( $this -> information , 'com.woltlab.wcf.people.information' , $this -> informationID ); return $processor -> getHtml (); } /** * Returns the person the information belongs to. */ public function getPerson () : Person { return PersonRuntimeCache :: getInstance () -> getObject ( $this -> personID ); } /** * Returns the user profile of the user who added the information. */ public function getUserProfile () : UserProfile { if ( $this -> userID ) { return UserProfileRuntimeCache :: getInstance () -> getObject ( $this -> userID ); } else { return UserProfile :: getGuestUserProfile ( $this -> username ); } } } 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. There also two getter methods for the person, the piece of information belongs to ( getPerson() ), and for the user profile of the user who created the information ( getUserProfile() ). In both cases, we use runtime caches, though in getUserProfile() , we also have to consider the case of the user who created the information being deleted, i.e. userID being null . For such a case, we also save the name of the user who created the information in username , so that we can return a guest user profile object in this case. The most interesting method is getFormattedInformation() , which returns the HTML code of the information text meant for output. To generate such an output, HtmlOutputProcessor::process() is used and here is where we first use the associated message object type com.woltlab.wcf.people.information mentioned before . 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: files/lib/data/person/information/PersonInformationList.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 * @package WoltLabSuite\\Core\\Data\\PersonInformation * * @method PersonInformation current() * @method PersonInformation[] getObjects() * @method PersonInformation|null search($objectID) * @property PersonInformation[] $objects */ class PersonInformationList extends DatabaseObjectList { public function readObjects () { parent :: readObjects (); UserProfileRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_filter ( \\array_column ( $this -> objects , 'userID' )))); PersonRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_column ( $this -> objects , 'personID' ))); } } Listing and Deleting Person Information # The person.tpl template has been updated to include a block for listing the information at the beginning: templates/person.tpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 { capture assign = 'pageTitle' }{ $person } - { lang } wcf.person.list { /lang }{ /capture } { capture assign = 'contentTitle' }{ $person }{ /capture } { include file = 'header' } { if $person -> informationCount || $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }

    { lang } wcf.person.information.list { /lang } { if $person -> informationCount } { # $person -> informationCount } { /if }

      { if $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }
    • { /if } { foreach from = $person -> getInformation () item = $information }
    • getObjectID () } \">
      getUserProfileHandler ()-> isIgnoredUser ( $information -> userID ) } ignoredUserContent { /if } \"> { user object = $information -> getUserProfile () type = 'avatar48' ariaHidden = 'true' tabindex = '-1' }

      { if $information -> userID } { user object = $information -> getUserProfile () } { else } { $information -> username } { /if } { @ $information -> time | time }

      getObjectID () } \"> { @ $information -> getFormattedInformation () }
    • { /foreach }
    { /if } { if $person -> enableComments } { if $commentList | count || $commentCanAdd }

    { lang } wcf.person.comments { /lang } { if $person -> comments } { # $person -> comments } { /if }

    { include file = '__commentJavaScript' commentContainerID = 'personCommentList' }
      personID } \" { * * } data-object-type-id=\" { @ $commentObjectTypeID } \" { * * } data-comments=\" { if $person -> comments }{ @ $commentList -> countObjects () }{ else } 0 { /if } \" { * * } data-last-comment-time=\" { @ $lastCommentTime } \" { * * } > { include file = 'commentListAddComment' wysiwygSelector = 'personCommentListAddComment' } { include file = 'commentList' }
    { /if } { /if }
    { hascontent } { /hascontent }
    { include file = 'footer' } To keep things simple here, we reuse the structure and CSS classes used for comments. Additionally, we always list all pieces of information. If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript. First, we note the jsObjectActionContainer class in combination with the data-object-action-class-name attribute, which are needed for the delete button for each piece of information, as explained here . In PersonInformationAction , we have overridden the default implementations of validateDelete() and delete() which are called after clicking on a delete button. In validateDelete() , we call PersonInformation::canDelete() on all pieces of information to be deleted for proper permission validation, and in delete() , we update the informationCount values of the people the deleted pieces of information belong to (see below). The button to add a new piece of information, #personInformationAddButton , and the buttons to edit existing pieces of information, .jsEditInformation , are controlled with JavaScript code initialized at the very end of the template. Lastly, in create() we provide default values for the time , userID , username , and ipAddress for cases like here when creating a new piece of information, where do not explicitly provide this data. Additionally, we extract the information text from the information_htmlInputProcessor parameter provided by the associated WYSIWYG form field and update the number of pieces of information created for the relevant person. Creating and Editing Person Information # To create new pieces of information or editing existing ones, we do not add new form controllers but instead use dialogs generated by the form builder API so that the user does not have to leave the person page. When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form: ts/WoltLabSuite/Core/Controller/Person.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 /** * Provides the JavaScript code for the person page. * * @author Matthias Schmidt * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Controller/Person */ import FormBuilderDialog from \"WoltLabSuite/Core/Form/Builder/Dialog\" ; import * as Language from \"WoltLabSuite/Core/Language\" ; import * as UiNotification from \"WoltLabSuite/Core/Ui/Notification\" ; let addDialog : FormBuilderDialog ; const editDialogs = new Map < string , FormBuilderDialog > (); interface EditReturnValues { formattedInformation : string ; informationID : number ; } interface Options { canAddInformation : true ; } /** * Opens the edit dialog after clicking on the edit button for a piece of information. */ function editInformation ( event : Event ) : void { event . preventDefault (); const currentTarget = event . currentTarget as HTMLElement ; const information = currentTarget . closest ( \".jsObjectActionObject\" ) as HTMLElement ; const informationId = information . dataset . objectId ! ; if ( ! editDialogs . has ( informationId )) { editDialogs . set ( informationId , new FormBuilderDialog ( `personInformationEditDialog ${ informationId } ` , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getEditDialog\" , { actionParameters : { informationID : informationId , }, dialog : { title : Language.get ( \"wcf.person.information.edit\" ), }, submitActionName : \"submitEditDialog\" , successCallback ( returnValues : EditReturnValues ) { document . getElementById ( `personInformation ${ returnValues . informationID } ` ) ! . innerHTML = returnValues . formattedInformation ; UiNotification . show ( Language . get ( \"wcf.person.information.edit.success\" )); }, }, ), ); } editDialogs . get ( informationId ) ! . open (); } /** * Initializes the JavaScript code for the person page. */ export function init ( personId : number , options : Options ) : void { if ( options . canAddInformation ) { // Initialize the dialog to add new information. addDialog = new FormBuilderDialog ( \"personInformationAddDialog\" , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getAddDialog\" , { actionParameters : { personID : personId , }, dialog : { title : Language.get ( \"wcf.person.information.add\" ), }, submitActionName : \"submitAddDialog\" , successCallback () { UiNotification . show ( Language . get ( \"wcf.person.information.add.success\" ), () => window . location . reload ()); }, }, ); document . getElementById ( \"personInformationAddButton\" ) ! . addEventListener ( \"click\" , ( event ) => { event . preventDefault (); addDialog . open (); }); } document . querySelectorAll ( \".jsEditInformation\" ) . forEach (( el ) => el . addEventListener ( \"click\" , ( ev ) => editInformation ( ev ))); } We use the WoltLabSuite/Core/Form/Builder/Dialog module , 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. Explanation of the initialization arguments for WoltLabSuite/Core/Form/Builder/Dialog used here: The first argument is the id of the dialog used to identify it. The second argument is the PHP class name which provides the contents of the dialog's form and handles the data after the form is submitted. The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form. The fourth argument contains additional options: actionParameters are additional parameters send during each AJAX request. Here, we either pass the id of the person for who a new piece of information is added or the id of the edited piece of information. dialog contains the options for the dialog, see the DialogOptions interface. Here, we only provide the title of the dialog. submitActionName is the name of the method in the referenced PHP class that is called with the form data after submitting the form. successCallback is called after the submit AJAX request was successful. After adding a new piece of information, we reload the page, and after editing an existing piece of information, we update the existing information text with the updated text. (Dynamically inserting a newly added piece of information instead of reloading the page would also be possible, of course, but for this tutorial series, we kept things simple.) Next, we focus on PersonInformationAction , which actually provides the contents of these dialogs and creates and edits the information: files/lib/data/person/information/PersonInformationAction.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @method PersonInformationEditor[] getObjects() * @method PersonInformationEditor getSingleObject() */ class PersonInformationAction extends AbstractDatabaseObjectAction { /** * @var DialogFormDocument */ public $dialog ; /** * @var PersonInformation */ public $information ; /** * @return PersonInformation */ public function create () { if ( ! isset ( $this -> parameters [ 'data' ][ 'time' ])) { $this -> parameters [ 'data' ][ 'time' ] = TIME_NOW ; } if ( ! isset ( $this -> parameters [ 'data' ][ 'userID' ])) { $this -> parameters [ 'data' ][ 'userID' ] = WCF :: getUser () -> userID ; $this -> parameters [ 'data' ][ 'username' ] = WCF :: getUser () -> username ; } if ( LOG_IP_ADDRESS ) { if ( ! isset ( $this -> parameters [ 'data' ][ 'ipAddress' ])) { $this -> parameters [ 'data' ][ 'ipAddress' ] = WCF :: getSession () -> ipAddress ; } } else { unset ( $this -> parameters [ 'data' ][ 'ipAddress' ]); } if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } /** @var PersonInformation $information */ $information = parent :: create (); ( new PersonAction ([ $information -> personID ], 'update' , [ 'counters' => [ 'informationCount' => 1 , ], ])) -> executeAction (); return $information ; } /** * @inheritDoc */ public function update () { if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } parent :: update (); } /** * @inheritDoc */ public function validateDelete () { if ( empty ( $this -> objects )) { $this -> readObjects (); if ( empty ( $this -> objects )) { throw new UserInputException ( 'objectIDs' ); } } foreach ( $this -> getObjects () as $informationEditor ) { if ( ! $informationEditor -> canDelete ()) { throw new PermissionDeniedException (); } } } /** * @inheritDoc */ public function delete () { $deleteCount = parent :: delete (); if ( ! $deleteCount ) { return $deleteCount ; } $counterUpdates = []; foreach ( $this -> getObjects () as $informationEditor ) { if ( ! isset ( $counterUpdates [ $informationEditor -> personID ])) { $counterUpdates [ $informationEditor -> personID ] = 0 ; } $counterUpdates [ $informationEditor -> personID ] -- ; } WCF :: getDB () -> beginTransaction (); foreach ( $counterUpdates as $personID => $counterUpdate ) { ( new PersonEditor ( PersonRuntimeCache :: getInstance () -> getObject ( $personID ))) -> updateCounters ([ 'informationCount' => $counterUpdate , ]); } WCF :: getDB () -> commitTransaction (); return $deleteCount ; } /** * Validates the `getAddDialog` action. */ public function validateGetAddDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'personID' ); if ( PersonRuntimeCache :: getInstance () -> getObject ( $this -> parameters [ 'personID' ]) === null ) { throw new UserInputException ( 'personID' ); } } /** * Returns the data to show the dialog to add a new piece of information on a person. * * @return string[] */ public function getAddDialog () : array { $this -> buildDialog (); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitAddDialog` action. */ public function validateSubmitAddDialog () : void { $this -> validateGetAddDialog (); $this -> buildDialog (); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Creates a new piece of information on a person after submitting the dialog. * * @return string[] */ public function submitAddDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([], 'create' , \\array_merge ( $this -> dialog -> getData (), [ 'data' => [ 'personID' => $this -> parameters [ 'personID' ], ], ]))) -> executeAction (); return []; } /** * Validates the `getEditDialog` action. */ public function validateGetEditDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'informationID' ); $this -> information = new PersonInformation ( $this -> parameters [ 'informationID' ]); if ( ! $this -> information -> getObjectID ()) { throw new UserInputException ( 'informationID' ); } if ( ! $this -> information -> canEdit ()) { throw new IllegalLinkException (); } } /** * Returns the data to show the dialog to edit a piece of information on a person. * * @return string[] */ public function getEditDialog () : array { $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information ); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitEditDialog` action. */ public function validateSubmitEditDialog () : void { $this -> validateGetEditDialog (); $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information , false ); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Updates a piece of information on a person after submitting the edit dialog. * * @return string[] */ public function submitEditDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([ $this -> information ], 'update' , $this -> dialog -> getData ())) -> executeAction (); // Reload the information with the updated data. $information = new PersonInformation ( $this -> information -> getObjectID ()); return [ 'formattedInformation' => $information -> getFormattedInformation (), 'informationID' => $this -> information -> getObjectID (), ]; } /** * Builds the dialog to create or edit person information. */ protected function buildDialog () : void { if ( $this -> dialog !== null ) { return ; } $this -> dialog = DialogFormDocument :: create ( 'personInformationAddDialog' ) -> appendChild ( WysiwygFormContainer :: create ( 'information' ) -> messageObjectType ( 'com.woltlab.wcf.people.information' ) -> required () ); EventHandler :: getInstance () -> fireAction ( $this , 'buildDialog' ); $this -> dialog -> build (); } } 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. As the forms for adding and editing pieces of information have the same structure, this form is created in buildDialog() using a DialogFormDocument object, which is intended for forms in dialogs. We fire an event in buildDialog() so that plugins are able to easily extend the dialog with additional data. validateGetAddDialog() checks if the user has the permission to create new pieces of information and if a valid id for the person, the information will belong to, is given. The method configured in the WoltLabSuite/Core/Form/Builder/Dialog object returning the dialog is expected to return two values: the id of the form ( formId ) and the contents of form shown in the dialog ( dialog ). This data is returned by getAddDialog using the dialog build previously by buildDialog() . After the form is submitted, validateSubmitAddDialog() has to do the same basic validation as validateGetAddDialog() so that validateGetAddDialog() is simply called. Additionally, the form data is read and validated. In submitAddDialog() , we first check if there have been any validation errors: If any error occured during validation, we return the same data as in getAddDialog() so that the dialog is shown again with the erroneous fields marked as such. Otherwise, if the validation succeeded, the form data is used to create the new piece of information. In addition to the form data, we manually add the id of the person to whom the information belongs to. Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog. As we will simply be reloading the page, no such data is returned. An alternative to reloading to the page would be dynamically inserting the new piece of information in the list so that we would have to return the rendered list item for the new piece of information. The process for getting and submitting the dialog to edit existing pieces of information is similar to the process for adding new pieces of information. Instead of the id of the person, however, we now pass the id of the edited piece of information and in submitEditDialog() , we update the edited information instead of creating a new one like in submitAddDialog() . After editing a piece of information, we do not reload the page but dynamically update the text of the information in the TypeScript code so that we return the updated rendered information text and id of the edited pieced of information in submitAddDialog() . Username and IP Address Event Listeners # As we store the name of the user who create a new piece of information and store their IP address, we have to add event listeners to properly handle the following scenarios: If the user is renamed, the value of username stored with the person information has to be updated, which can be achieved by a simple event listener that only has to specify the name of relevant database table if AbstractUserActionRenameListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserActionRenameListener extends AbstractUserActionRenameListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 2. If users are merged, all pieces of information need to be assigned to the target user of the merging. Again, we only have to specify the name of relevant database table if AbstractUserMergeListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserMergeListener extends AbstractUserMergeListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 3. If the option to prune stored ip addresses after a certain period of time is enabled, we also have to prune them in the person information database table. Here we also only have to specify the name of the relevant database table and provide the mapping from the ipAddress column to the time column: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonPruneIpAddressesCronjobListener extends AbstractEventListener { protected function onExecute ( PruneIpAddressesCronjob $cronjob ) : void { $cronjob -> columns [ 'wcf' . WCF_N . '_person_information' ][ 'ipAddress' ] = 'time' ; } } 4. The ip addresses in the person information database table also have to be considered for the user data export which can also be done with minimal effort by providing the name of the relevant database table: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserExportGdprListener extends AbstractEventListener { protected function onExport ( UserExportGdprAction $action ) : void { $action -> ipAddresses [ 'com.woltlab.wcf.people' ] = [ 'wcf' . WCF_N . '_person_information' ]; } } Lastly, we present the updated eventListener.xml file with new entries for all of these event listeners: eventListener.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 wcf\\data\\user\\UserAction rename wcf\\system\\event\\listener\\PersonUserActionRenameListener all wcf\\acp\\form\\UserMergeForm save wcf\\system\\event\\listener\\PersonUserMergeListener admin wcf\\system\\cronjob\\PruneIpAddressesCronjob execute wcf\\system\\event\\listener\\PersonPruneIpAddressesCronjobListener all wcf\\acp\\action\\UserExportGdprAction export wcf\\system\\event\\listener\\PersonUserExportGdprListener admin ","title":"Part 5"},{"location":"tutorial/series/part_5/#part-5-person-information","text":"This part of our tutorial series lays the foundation for future parts in which we will be using additional APIs, which we have not used in this series yet. To make use of those APIs, we need content generated by users in the frontend.","title":"Part 5: Person Information"},{"location":"tutorial/series/part_5/#package-functionality","text":"In addition to the existing functions from part 4 , the package will provide the following functionality after this part of the tutorial: Users are able to add information on the people in the frontend. Users are able to edit and delete the pieces of information they added. Moderators are able to edit and delete all pieces of information.","title":"Package Functionality"},{"location":"tutorial/series/part_5/#used-components","text":"In addition to the components used in previous parts, we will use the form builder API to create forms shown in dialogs instead of dedicated pages and we will, for the first time, add TypeScript code .","title":"Used Components"},{"location":"tutorial/series/part_5/#package-structure","text":"The package will have the following file structure excluding unchanged files from previous parts: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u251c\u2500\u2500 js \u2502 \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2502 \u2514\u2500\u2500 Core \u2502 \u2502 \u2514\u2500\u2500 Controller \u2502 \u2502 \u2514\u2500\u2500 Person.js \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 data \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2514\u2500\u2500 information \u2502 \u251c\u2500\u2500 PersonInformation.class.php \u2502 \u251c\u2500\u2500 PersonInformationAction.class.php \u2502 \u251c\u2500\u2500 PersonInformationEditor.class.php \u2502 \u2514\u2500\u2500 PersonInformationList.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 ts \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2514\u2500\u2500 Core \u2502 \u2514\u2500\u2500 Controller \u2502 \u2514\u2500\u2500 Person.ts \u2514\u2500\u2500 userGroupOption.xml For all changes, please refer to the source code on GitHub .","title":"Package Structure"},{"location":"tutorial/series/part_5/#miscellaneous","text":"Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on: Several new user group options and the relevant language items have been added related to creating, editing, and deleting information: mod.person.canEditInformation and mod.person.canDeleteInformation are moderative permissions to edit and delete any piece of information, regardless of who created it. user.person.canAddInformation is the permission for users to add new pieces of information. user.person.canEditInformation and user.person.canDeleteInformation are the user permissions to edit and the piece of information they created. The actual information text will be entered via a WYSIWYG editor, which requires an object type of the definition com.woltlab.wcf.message : com.woltlab.wcf.people.information . personList.tpl has been adjusted to show the number of pieces of information in the person statistics section. We have not updated the person list box to also support sorting by the number of pieces of information added for each person.","title":"Miscellaneous"},{"location":"tutorial/series/part_5/#person-information-model","text":"The PHP file with the database layout has been updated as follows: files/acp/database/install_com.woltlab.wcf.people.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 columns ([ ObjectIdDatabaseTableColumn :: create ( 'personID' ), NotNullVarchar255DatabaseTableColumn :: create ( 'firstName' ), NotNullVarchar255DatabaseTableColumn :: create ( 'lastName' ), NotNullInt10DatabaseTableColumn :: create ( 'informationCount' ) -> defaultValue ( 0 ), SmallintDatabaseTableColumn :: create ( 'comments' ) -> length ( 5 ) -> notNull () -> defaultValue ( 0 ), DefaultTrueBooleanDatabaseTableColumn :: create ( 'enableComments' ), ]), DatabaseTable :: create ( 'wcf1_person_information' ) -> columns ([ ObjectIdDatabaseTableColumn :: create ( 'informationID' ), NotNullInt10DatabaseTableColumn :: create ( 'personID' ), TextDatabaseTableColumn :: create ( 'information' ), IntDatabaseTableColumn :: create ( 'userID' ) -> length ( 10 ), NotNullVarchar255DatabaseTableColumn :: create ( 'username' ), VarcharDatabaseTableColumn :: create ( 'ipAddress' ) -> length ( 39 ) -> notNull ( true ) -> defaultValue ( '' ), NotNullInt10DatabaseTableColumn :: create ( 'time' ), ]) -> foreignKeys ([ DatabaseTableForeignKey :: create () -> columns ([ 'personID' ]) -> referencedTable ( 'wcf1_person' ) -> referencedColumns ([ 'personID' ]) -> onDelete ( 'CASCADE' ), DatabaseTableForeignKey :: create () -> columns ([ 'userID' ]) -> referencedTable ( 'wcf1_user' ) -> referencedColumns ([ 'userID' ]) -> onDelete ( 'SET NULL' ), ]), ]; 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 . files/lib/data/person/information/PersonInformation.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @property-read int $informationID unique id of the information * @property-read int $personID id of the person the information belongs to * @property-read string $information information text * @property-read int|null $userID id of the user who added the information or `null` if the user no longer exists * @property-read string $username name of the user who added the information * @property-read int $time timestamp at which the information was created */ class PersonInformation extends DatabaseObject { /** * Returns `true` if the active user can delete this piece of information and `false` otherwise. */ public function canDelete () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canDeleteInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canDeleteInformation' ); } /** * Returns `true` if the active user can edit this piece of information and `false` otherwise. */ public function canEdit () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canEditInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canEditInformation' ); } /** * Returns the formatted information. */ public function getFormattedInformation () : string { $processor = new HtmlOutputProcessor (); $processor -> process ( $this -> information , 'com.woltlab.wcf.people.information' , $this -> informationID ); return $processor -> getHtml (); } /** * Returns the person the information belongs to. */ public function getPerson () : Person { return PersonRuntimeCache :: getInstance () -> getObject ( $this -> personID ); } /** * Returns the user profile of the user who added the information. */ public function getUserProfile () : UserProfile { if ( $this -> userID ) { return UserProfileRuntimeCache :: getInstance () -> getObject ( $this -> userID ); } else { return UserProfile :: getGuestUserProfile ( $this -> username ); } } } 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. There also two getter methods for the person, the piece of information belongs to ( getPerson() ), and for the user profile of the user who created the information ( getUserProfile() ). In both cases, we use runtime caches, though in getUserProfile() , we also have to consider the case of the user who created the information being deleted, i.e. userID being null . For such a case, we also save the name of the user who created the information in username , so that we can return a guest user profile object in this case. The most interesting method is getFormattedInformation() , which returns the HTML code of the information text meant for output. To generate such an output, HtmlOutputProcessor::process() is used and here is where we first use the associated message object type com.woltlab.wcf.people.information mentioned before . 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: files/lib/data/person/information/PersonInformationList.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 * @package WoltLabSuite\\Core\\Data\\PersonInformation * * @method PersonInformation current() * @method PersonInformation[] getObjects() * @method PersonInformation|null search($objectID) * @property PersonInformation[] $objects */ class PersonInformationList extends DatabaseObjectList { public function readObjects () { parent :: readObjects (); UserProfileRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_filter ( \\array_column ( $this -> objects , 'userID' )))); PersonRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_column ( $this -> objects , 'personID' ))); } }","title":"Person Information Model"},{"location":"tutorial/series/part_5/#listing-and-deleting-person-information","text":"The person.tpl template has been updated to include a block for listing the information at the beginning: templates/person.tpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 { capture assign = 'pageTitle' }{ $person } - { lang } wcf.person.list { /lang }{ /capture } { capture assign = 'contentTitle' }{ $person }{ /capture } { include file = 'header' } { if $person -> informationCount || $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }

    { lang } wcf.person.information.list { /lang } { if $person -> informationCount } { # $person -> informationCount } { /if }

      { if $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }
    • { /if } { foreach from = $person -> getInformation () item = $information }
    • getObjectID () } \">
      getUserProfileHandler ()-> isIgnoredUser ( $information -> userID ) } ignoredUserContent { /if } \"> { user object = $information -> getUserProfile () type = 'avatar48' ariaHidden = 'true' tabindex = '-1' }

      { if $information -> userID } { user object = $information -> getUserProfile () } { else } { $information -> username } { /if } { @ $information -> time | time }

      getObjectID () } \"> { @ $information -> getFormattedInformation () }
    • { /foreach }
    { /if } { if $person -> enableComments } { if $commentList | count || $commentCanAdd }

    { lang } wcf.person.comments { /lang } { if $person -> comments } { # $person -> comments } { /if }

    { include file = '__commentJavaScript' commentContainerID = 'personCommentList' }
      personID } \" { * * } data-object-type-id=\" { @ $commentObjectTypeID } \" { * * } data-comments=\" { if $person -> comments }{ @ $commentList -> countObjects () }{ else } 0 { /if } \" { * * } data-last-comment-time=\" { @ $lastCommentTime } \" { * * } > { include file = 'commentListAddComment' wysiwygSelector = 'personCommentListAddComment' } { include file = 'commentList' }
    { /if } { /if }
    { hascontent } { /hascontent }
    { include file = 'footer' } To keep things simple here, we reuse the structure and CSS classes used for comments. Additionally, we always list all pieces of information. If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript. First, we note the jsObjectActionContainer class in combination with the data-object-action-class-name attribute, which are needed for the delete button for each piece of information, as explained here . In PersonInformationAction , we have overridden the default implementations of validateDelete() and delete() which are called after clicking on a delete button. In validateDelete() , we call PersonInformation::canDelete() on all pieces of information to be deleted for proper permission validation, and in delete() , we update the informationCount values of the people the deleted pieces of information belong to (see below). The button to add a new piece of information, #personInformationAddButton , and the buttons to edit existing pieces of information, .jsEditInformation , are controlled with JavaScript code initialized at the very end of the template. Lastly, in create() we provide default values for the time , userID , username , and ipAddress for cases like here when creating a new piece of information, where do not explicitly provide this data. Additionally, we extract the information text from the information_htmlInputProcessor parameter provided by the associated WYSIWYG form field and update the number of pieces of information created for the relevant person.","title":"Listing and Deleting Person Information"},{"location":"tutorial/series/part_5/#creating-and-editing-person-information","text":"To create new pieces of information or editing existing ones, we do not add new form controllers but instead use dialogs generated by the form builder API so that the user does not have to leave the person page. When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form: ts/WoltLabSuite/Core/Controller/Person.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 /** * Provides the JavaScript code for the person page. * * @author Matthias Schmidt * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Controller/Person */ import FormBuilderDialog from \"WoltLabSuite/Core/Form/Builder/Dialog\" ; import * as Language from \"WoltLabSuite/Core/Language\" ; import * as UiNotification from \"WoltLabSuite/Core/Ui/Notification\" ; let addDialog : FormBuilderDialog ; const editDialogs = new Map < string , FormBuilderDialog > (); interface EditReturnValues { formattedInformation : string ; informationID : number ; } interface Options { canAddInformation : true ; } /** * Opens the edit dialog after clicking on the edit button for a piece of information. */ function editInformation ( event : Event ) : void { event . preventDefault (); const currentTarget = event . currentTarget as HTMLElement ; const information = currentTarget . closest ( \".jsObjectActionObject\" ) as HTMLElement ; const informationId = information . dataset . objectId ! ; if ( ! editDialogs . has ( informationId )) { editDialogs . set ( informationId , new FormBuilderDialog ( `personInformationEditDialog ${ informationId } ` , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getEditDialog\" , { actionParameters : { informationID : informationId , }, dialog : { title : Language.get ( \"wcf.person.information.edit\" ), }, submitActionName : \"submitEditDialog\" , successCallback ( returnValues : EditReturnValues ) { document . getElementById ( `personInformation ${ returnValues . informationID } ` ) ! . innerHTML = returnValues . formattedInformation ; UiNotification . show ( Language . get ( \"wcf.person.information.edit.success\" )); }, }, ), ); } editDialogs . get ( informationId ) ! . open (); } /** * Initializes the JavaScript code for the person page. */ export function init ( personId : number , options : Options ) : void { if ( options . canAddInformation ) { // Initialize the dialog to add new information. addDialog = new FormBuilderDialog ( \"personInformationAddDialog\" , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getAddDialog\" , { actionParameters : { personID : personId , }, dialog : { title : Language.get ( \"wcf.person.information.add\" ), }, submitActionName : \"submitAddDialog\" , successCallback () { UiNotification . show ( Language . get ( \"wcf.person.information.add.success\" ), () => window . location . reload ()); }, }, ); document . getElementById ( \"personInformationAddButton\" ) ! . addEventListener ( \"click\" , ( event ) => { event . preventDefault (); addDialog . open (); }); } document . querySelectorAll ( \".jsEditInformation\" ) . forEach (( el ) => el . addEventListener ( \"click\" , ( ev ) => editInformation ( ev ))); } We use the WoltLabSuite/Core/Form/Builder/Dialog module , 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. Explanation of the initialization arguments for WoltLabSuite/Core/Form/Builder/Dialog used here: The first argument is the id of the dialog used to identify it. The second argument is the PHP class name which provides the contents of the dialog's form and handles the data after the form is submitted. The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form. The fourth argument contains additional options: actionParameters are additional parameters send during each AJAX request. Here, we either pass the id of the person for who a new piece of information is added or the id of the edited piece of information. dialog contains the options for the dialog, see the DialogOptions interface. Here, we only provide the title of the dialog. submitActionName is the name of the method in the referenced PHP class that is called with the form data after submitting the form. successCallback is called after the submit AJAX request was successful. After adding a new piece of information, we reload the page, and after editing an existing piece of information, we update the existing information text with the updated text. (Dynamically inserting a newly added piece of information instead of reloading the page would also be possible, of course, but for this tutorial series, we kept things simple.) Next, we focus on PersonInformationAction , which actually provides the contents of these dialogs and creates and edits the information: files/lib/data/person/information/PersonInformationAction.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @method PersonInformationEditor[] getObjects() * @method PersonInformationEditor getSingleObject() */ class PersonInformationAction extends AbstractDatabaseObjectAction { /** * @var DialogFormDocument */ public $dialog ; /** * @var PersonInformation */ public $information ; /** * @return PersonInformation */ public function create () { if ( ! isset ( $this -> parameters [ 'data' ][ 'time' ])) { $this -> parameters [ 'data' ][ 'time' ] = TIME_NOW ; } if ( ! isset ( $this -> parameters [ 'data' ][ 'userID' ])) { $this -> parameters [ 'data' ][ 'userID' ] = WCF :: getUser () -> userID ; $this -> parameters [ 'data' ][ 'username' ] = WCF :: getUser () -> username ; } if ( LOG_IP_ADDRESS ) { if ( ! isset ( $this -> parameters [ 'data' ][ 'ipAddress' ])) { $this -> parameters [ 'data' ][ 'ipAddress' ] = WCF :: getSession () -> ipAddress ; } } else { unset ( $this -> parameters [ 'data' ][ 'ipAddress' ]); } if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } /** @var PersonInformation $information */ $information = parent :: create (); ( new PersonAction ([ $information -> personID ], 'update' , [ 'counters' => [ 'informationCount' => 1 , ], ])) -> executeAction (); return $information ; } /** * @inheritDoc */ public function update () { if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } parent :: update (); } /** * @inheritDoc */ public function validateDelete () { if ( empty ( $this -> objects )) { $this -> readObjects (); if ( empty ( $this -> objects )) { throw new UserInputException ( 'objectIDs' ); } } foreach ( $this -> getObjects () as $informationEditor ) { if ( ! $informationEditor -> canDelete ()) { throw new PermissionDeniedException (); } } } /** * @inheritDoc */ public function delete () { $deleteCount = parent :: delete (); if ( ! $deleteCount ) { return $deleteCount ; } $counterUpdates = []; foreach ( $this -> getObjects () as $informationEditor ) { if ( ! isset ( $counterUpdates [ $informationEditor -> personID ])) { $counterUpdates [ $informationEditor -> personID ] = 0 ; } $counterUpdates [ $informationEditor -> personID ] -- ; } WCF :: getDB () -> beginTransaction (); foreach ( $counterUpdates as $personID => $counterUpdate ) { ( new PersonEditor ( PersonRuntimeCache :: getInstance () -> getObject ( $personID ))) -> updateCounters ([ 'informationCount' => $counterUpdate , ]); } WCF :: getDB () -> commitTransaction (); return $deleteCount ; } /** * Validates the `getAddDialog` action. */ public function validateGetAddDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'personID' ); if ( PersonRuntimeCache :: getInstance () -> getObject ( $this -> parameters [ 'personID' ]) === null ) { throw new UserInputException ( 'personID' ); } } /** * Returns the data to show the dialog to add a new piece of information on a person. * * @return string[] */ public function getAddDialog () : array { $this -> buildDialog (); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitAddDialog` action. */ public function validateSubmitAddDialog () : void { $this -> validateGetAddDialog (); $this -> buildDialog (); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Creates a new piece of information on a person after submitting the dialog. * * @return string[] */ public function submitAddDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([], 'create' , \\array_merge ( $this -> dialog -> getData (), [ 'data' => [ 'personID' => $this -> parameters [ 'personID' ], ], ]))) -> executeAction (); return []; } /** * Validates the `getEditDialog` action. */ public function validateGetEditDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'informationID' ); $this -> information = new PersonInformation ( $this -> parameters [ 'informationID' ]); if ( ! $this -> information -> getObjectID ()) { throw new UserInputException ( 'informationID' ); } if ( ! $this -> information -> canEdit ()) { throw new IllegalLinkException (); } } /** * Returns the data to show the dialog to edit a piece of information on a person. * * @return string[] */ public function getEditDialog () : array { $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information ); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitEditDialog` action. */ public function validateSubmitEditDialog () : void { $this -> validateGetEditDialog (); $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information , false ); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Updates a piece of information on a person after submitting the edit dialog. * * @return string[] */ public function submitEditDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([ $this -> information ], 'update' , $this -> dialog -> getData ())) -> executeAction (); // Reload the information with the updated data. $information = new PersonInformation ( $this -> information -> getObjectID ()); return [ 'formattedInformation' => $information -> getFormattedInformation (), 'informationID' => $this -> information -> getObjectID (), ]; } /** * Builds the dialog to create or edit person information. */ protected function buildDialog () : void { if ( $this -> dialog !== null ) { return ; } $this -> dialog = DialogFormDocument :: create ( 'personInformationAddDialog' ) -> appendChild ( WysiwygFormContainer :: create ( 'information' ) -> messageObjectType ( 'com.woltlab.wcf.people.information' ) -> required () ); EventHandler :: getInstance () -> fireAction ( $this , 'buildDialog' ); $this -> dialog -> build (); } } 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. As the forms for adding and editing pieces of information have the same structure, this form is created in buildDialog() using a DialogFormDocument object, which is intended for forms in dialogs. We fire an event in buildDialog() so that plugins are able to easily extend the dialog with additional data. validateGetAddDialog() checks if the user has the permission to create new pieces of information and if a valid id for the person, the information will belong to, is given. The method configured in the WoltLabSuite/Core/Form/Builder/Dialog object returning the dialog is expected to return two values: the id of the form ( formId ) and the contents of form shown in the dialog ( dialog ). This data is returned by getAddDialog using the dialog build previously by buildDialog() . After the form is submitted, validateSubmitAddDialog() has to do the same basic validation as validateGetAddDialog() so that validateGetAddDialog() is simply called. Additionally, the form data is read and validated. In submitAddDialog() , we first check if there have been any validation errors: If any error occured during validation, we return the same data as in getAddDialog() so that the dialog is shown again with the erroneous fields marked as such. Otherwise, if the validation succeeded, the form data is used to create the new piece of information. In addition to the form data, we manually add the id of the person to whom the information belongs to. Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog. As we will simply be reloading the page, no such data is returned. An alternative to reloading to the page would be dynamically inserting the new piece of information in the list so that we would have to return the rendered list item for the new piece of information. The process for getting and submitting the dialog to edit existing pieces of information is similar to the process for adding new pieces of information. Instead of the id of the person, however, we now pass the id of the edited piece of information and in submitEditDialog() , we update the edited information instead of creating a new one like in submitAddDialog() . After editing a piece of information, we do not reload the page but dynamically update the text of the information in the TypeScript code so that we return the updated rendered information text and id of the edited pieced of information in submitAddDialog() .","title":"Creating and Editing Person Information"},{"location":"tutorial/series/part_5/#username-and-ip-address-event-listeners","text":"As we store the name of the user who create a new piece of information and store their IP address, we have to add event listeners to properly handle the following scenarios: If the user is renamed, the value of username stored with the person information has to be updated, which can be achieved by a simple event listener that only has to specify the name of relevant database table if AbstractUserActionRenameListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserActionRenameListener extends AbstractUserActionRenameListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 2. If users are merged, all pieces of information need to be assigned to the target user of the merging. Again, we only have to specify the name of relevant database table if AbstractUserMergeListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserMergeListener extends AbstractUserMergeListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 3. If the option to prune stored ip addresses after a certain period of time is enabled, we also have to prune them in the person information database table. Here we also only have to specify the name of the relevant database table and provide the mapping from the ipAddress column to the time column: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonPruneIpAddressesCronjobListener extends AbstractEventListener { protected function onExecute ( PruneIpAddressesCronjob $cronjob ) : void { $cronjob -> columns [ 'wcf' . WCF_N . '_person_information' ][ 'ipAddress' ] = 'time' ; } } 4. The ip addresses in the person information database table also have to be considered for the user data export which can also be done with minimal effort by providing the name of the relevant database table: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserExportGdprListener extends AbstractEventListener { protected function onExport ( UserExportGdprAction $action ) : void { $action -> ipAddresses [ 'com.woltlab.wcf.people' ] = [ 'wcf' . WCF_N . '_person_information' ]; } } Lastly, we present the updated eventListener.xml file with new entries for all of these event listeners: eventListener.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 wcf\\data\\user\\UserAction rename wcf\\system\\event\\listener\\PersonUserActionRenameListener all wcf\\acp\\form\\UserMergeForm save wcf\\system\\event\\listener\\PersonUserMergeListener admin wcf\\system\\cronjob\\PruneIpAddressesCronjob execute wcf\\system\\event\\listener\\PersonPruneIpAddressesCronjobListener all wcf\\acp\\action\\UserExportGdprAction export wcf\\system\\event\\listener\\PersonUserExportGdprListener admin ","title":"Username and IP Address Event Listeners"},{"location":"view/css/","text":"CSS # SCSS and CSS # SCSS is a scripting language that features a syntax similar to CSS and compiles into native CSS at runtime. It provides many great additions to CSS such as declaration nesting and variables, it is recommended to read the official guide to learn more. You can create .scss files containing only pure CSS code and it will work just fine, you are at no point required to write actual SCSS code. File Location # Please place your style files in a subdirectory of the style/ directory of the target application or the Core's style directory, for example style/layout/pageHeader.scss . Variables # You can access variables with $myVariable , variable interpolation (variables inside strings) is accomplished with #{$myVariable} . Linking images # Images used within a style must be located in the style's image folder. To get the folder name within the CSS the SCSS variable #{$style_image_path} can be used. The value will contain a trailing slash. Media Breakpoints # Media breakpoints instruct the browser to apply different CSS depending on the viewport dimensions, e.g. serving a desktop PC a different view than when viewed on a smartphone. 1 2 3 4 5 6 7 8 9 10 11 12 13 /* red background color for desktop pc */ @include screen-lg { body { background-color : red ; } } /* green background color on smartphones and tablets */ @include screen-md-down { body { background-color : green ; } } Available Breakpoints # Some very large smartphones, for example the Apple iPhone 7 Plus, do match the media query for Tablets (portrait) when viewed in landscape mode. Name Devices @media equivalent screen-xs Smartphones only (max-width: 544px) screen-sm Tablets (portrait) (min-width: 545px) and (max-width: 768px) screen-sm-down Tablets (portrait) and smartphones (max-width: 768px) screen-sm-up Tablets and desktop PC (min-width: 545px) screen-sm-md Tablets only (min-width: 545px) and (max-width: 1024px) screen-md Tablets (landscape) (min-width: 769px) and (max-width: 1024px) screen-md-down Smartphones and tablets (max-width: 1024px) screen-md-up Tablets (landscape) and desktop PC (min-width: 769px) screen-lg Desktop PC (min-width: 1025px) screen-lg-only Desktop PC (min-width: 1025px) and (max-width: 1280px) screen-lg-down Smartphones, tablets, and desktop PC (max-width: 1280px) screen-xl Desktop PC (min-width: 1281px) Asset Preloading # WoltLab Suite\u2019s SCSS compiler supports adding preloading metadata to the CSS. To communicate the preloading intent to the compiler, the --woltlab-suite-preload CSS variable is set to the result of the preload() function: 1 2 3 4 5 6 7 8 9 10 .fooBar { --woltlab-suite-preload : # { preload ( ' #{ $style_image_path } custom/background.png' , $ as : \"image\" , $ crossorigin : false , $ type : \"image/png\" ) } ; background : url ( ' #{ $style_image_path } custom/background.png' ); } The parameters of the preload() function map directly to the preloading properties that are used within the tag and the link: HTTP response header. The above example will result in a similar to the following being added to the generated HTML: 1 Use preloading sparingly for the most important resources where you can be certain that the browser will need them. Unused preloaded resources will unnecessarily waste bandwidth.","title":"CSS"},{"location":"view/css/#css","text":"","title":"CSS"},{"location":"view/css/#scss-and-css","text":"SCSS is a scripting language that features a syntax similar to CSS and compiles into native CSS at runtime. It provides many great additions to CSS such as declaration nesting and variables, it is recommended to read the official guide to learn more. You can create .scss files containing only pure CSS code and it will work just fine, you are at no point required to write actual SCSS code.","title":"SCSS and CSS"},{"location":"view/css/#file-location","text":"Please place your style files in a subdirectory of the style/ directory of the target application or the Core's style directory, for example style/layout/pageHeader.scss .","title":"File Location"},{"location":"view/css/#variables","text":"You can access variables with $myVariable , variable interpolation (variables inside strings) is accomplished with #{$myVariable} .","title":"Variables"},{"location":"view/css/#linking-images","text":"Images used within a style must be located in the style's image folder. To get the folder name within the CSS the SCSS variable #{$style_image_path} can be used. The value will contain a trailing slash.","title":"Linking images"},{"location":"view/css/#media-breakpoints","text":"Media breakpoints instruct the browser to apply different CSS depending on the viewport dimensions, e.g. serving a desktop PC a different view than when viewed on a smartphone. 1 2 3 4 5 6 7 8 9 10 11 12 13 /* red background color for desktop pc */ @include screen-lg { body { background-color : red ; } } /* green background color on smartphones and tablets */ @include screen-md-down { body { background-color : green ; } }","title":"Media Breakpoints"},{"location":"view/css/#available-breakpoints","text":"Some very large smartphones, for example the Apple iPhone 7 Plus, do match the media query for Tablets (portrait) when viewed in landscape mode. Name Devices @media equivalent screen-xs Smartphones only (max-width: 544px) screen-sm Tablets (portrait) (min-width: 545px) and (max-width: 768px) screen-sm-down Tablets (portrait) and smartphones (max-width: 768px) screen-sm-up Tablets and desktop PC (min-width: 545px) screen-sm-md Tablets only (min-width: 545px) and (max-width: 1024px) screen-md Tablets (landscape) (min-width: 769px) and (max-width: 1024px) screen-md-down Smartphones and tablets (max-width: 1024px) screen-md-up Tablets (landscape) and desktop PC (min-width: 769px) screen-lg Desktop PC (min-width: 1025px) screen-lg-only Desktop PC (min-width: 1025px) and (max-width: 1280px) screen-lg-down Smartphones, tablets, and desktop PC (max-width: 1280px) screen-xl Desktop PC (min-width: 1281px)","title":"Available Breakpoints"},{"location":"view/css/#asset-preloading","text":"WoltLab Suite\u2019s SCSS compiler supports adding preloading metadata to the CSS. To communicate the preloading intent to the compiler, the --woltlab-suite-preload CSS variable is set to the result of the preload() function: 1 2 3 4 5 6 7 8 9 10 .fooBar { --woltlab-suite-preload : # { preload ( ' #{ $style_image_path } custom/background.png' , $ as : \"image\" , $ crossorigin : false , $ type : \"image/png\" ) } ; background : url ( ' #{ $style_image_path } custom/background.png' ); } The parameters of the preload() function map directly to the preloading properties that are used within the tag and the link: HTTP response header. The above example will result in a similar to the following being added to the generated HTML: 1 Use preloading sparingly for the most important resources where you can be certain that the browser will need them. Unused preloaded resources will unnecessarily waste bandwidth.","title":"Asset Preloading"},{"location":"view/languages-naming-conventions/","text":"Language Naming Conventions # This page contains general rules for naming language items and for their values. API-specific rules are listed on the relevant API page: Comments Forms # Fields # If you have an application foo and a database object foo\\data\\bar\\Bar with a property baz that can be set via a form field, the name of the corresponding language item has to be foo.bar.baz . If you want to add an additional description below the field, use the language item foo.bar.baz.description . Error Texts # If an error of type {error type} for the previously mentioned form field occurs during validation, you have to use the language item foo.bar.baz.error.{error type} for the language item describing the error. Exception to this rule: There are several general error messages like wcf.global.form.error.empty that have to be used for general errors like an empty field that may not be empty to avoid duplication of the same error message text over and over again in different language items. Naming Conventions # If the entered text does not conform to some special rules, i.e. if the text is invalid, use invalid as error type. If the entered text is required to be unique but is already used for another object, use notUnique as error type. Confirmation messages # If the language item for an action is foo.bar.action , the language item for the confirmation message has to be foo.bar.action.confirmMessage instead of foo.bar.action.sure which is still used by some older language items. Type-Specific Deletion Confirmation Message # German # 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Icon wirklich l\u00f6schen? English # 1 Do you really want delete the {element type}? Example: 1 Do you really want delete the icon? Object-Specific Deletion Confirmation Message # German # 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} {object name} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Artikel {$article->getTitle()} wirklich l\u00f6schen? English # 1 Do you really want to delete the {element type} {object name}? Example: 1 Do you really want to delete the article {$article->getTitle()}? User Group Options # Comments # German # group type action example permission name language item user adding user.foo.canAddComment Kann Kommentare erstellen user deleting user.foo.canDeleteComment Kann eigene Kommentare l\u00f6schen user editing user.foo.canEditComment Kann eigene Kommentare bearbeiten moderator deleting mod.foo.canDeleteComment Kann Kommentare l\u00f6schen moderator editing mod.foo.canEditComment Kann Kommentare bearbeiten moderator moderating mod.foo.canModerateComment Kann Kommentare moderieren English # group type action example permission name language item user adding user.foo.canAddComment Can create comments user deleting user.foo.canDeleteComment Can delete their comments user editing user.foo.canEditComment Can edit their comments moderator deleting mod.foo.canDeleteComment Can delete comments moderator editing mod.foo.canEditComment Can edit comments moderator moderating mod.foo.canModerateComment Can moderate comments","title":"Language Naming Conventions"},{"location":"view/languages-naming-conventions/#language-naming-conventions","text":"This page contains general rules for naming language items and for their values. API-specific rules are listed on the relevant API page: Comments","title":"Language Naming Conventions"},{"location":"view/languages-naming-conventions/#forms","text":"","title":"Forms"},{"location":"view/languages-naming-conventions/#fields","text":"If you have an application foo and a database object foo\\data\\bar\\Bar with a property baz that can be set via a form field, the name of the corresponding language item has to be foo.bar.baz . If you want to add an additional description below the field, use the language item foo.bar.baz.description .","title":"Fields"},{"location":"view/languages-naming-conventions/#error-texts","text":"If an error of type {error type} for the previously mentioned form field occurs during validation, you have to use the language item foo.bar.baz.error.{error type} for the language item describing the error. Exception to this rule: There are several general error messages like wcf.global.form.error.empty that have to be used for general errors like an empty field that may not be empty to avoid duplication of the same error message text over and over again in different language items.","title":"Error Texts"},{"location":"view/languages-naming-conventions/#naming-conventions","text":"If the entered text does not conform to some special rules, i.e. if the text is invalid, use invalid as error type. If the entered text is required to be unique but is already used for another object, use notUnique as error type.","title":"Naming Conventions"},{"location":"view/languages-naming-conventions/#confirmation-messages","text":"If the language item for an action is foo.bar.action , the language item for the confirmation message has to be foo.bar.action.confirmMessage instead of foo.bar.action.sure which is still used by some older language items.","title":"Confirmation messages"},{"location":"view/languages-naming-conventions/#type-specific-deletion-confirmation-message","text":"","title":"Type-Specific Deletion Confirmation Message"},{"location":"view/languages-naming-conventions/#german","text":"1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Icon wirklich l\u00f6schen?","title":"German"},{"location":"view/languages-naming-conventions/#english","text":"1 Do you really want delete the {element type}? Example: 1 Do you really want delete the icon?","title":"English"},{"location":"view/languages-naming-conventions/#object-specific-deletion-confirmation-message","text":"","title":"Object-Specific Deletion Confirmation Message"},{"location":"view/languages-naming-conventions/#german_1","text":"1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} {object name} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Artikel {$article->getTitle()} wirklich l\u00f6schen?","title":"German"},{"location":"view/languages-naming-conventions/#english_1","text":"1 Do you really want to delete the {element type} {object name}? Example: 1 Do you really want to delete the article {$article->getTitle()}?","title":"English"},{"location":"view/languages-naming-conventions/#user-group-options","text":"","title":"User Group Options"},{"location":"view/languages-naming-conventions/#comments","text":"","title":"Comments"},{"location":"view/languages-naming-conventions/#german_2","text":"group type action example permission name language item user adding user.foo.canAddComment Kann Kommentare erstellen user deleting user.foo.canDeleteComment Kann eigene Kommentare l\u00f6schen user editing user.foo.canEditComment Kann eigene Kommentare bearbeiten moderator deleting mod.foo.canDeleteComment Kann Kommentare l\u00f6schen moderator editing mod.foo.canEditComment Kann Kommentare bearbeiten moderator moderating mod.foo.canModerateComment Kann Kommentare moderieren","title":"German"},{"location":"view/languages-naming-conventions/#english_2","text":"group type action example permission name language item user adding user.foo.canAddComment Can create comments user deleting user.foo.canDeleteComment Can delete their comments user editing user.foo.canEditComment Can edit their comments moderator deleting mod.foo.canDeleteComment Can delete comments moderator editing mod.foo.canEditComment Can edit comments moderator moderating mod.foo.canModerateComment Can moderate comments","title":"English"},{"location":"view/languages/","text":"Languages # WoltLab Suite offers full i18n support with its integrated language system, including but not limited to dynamic phrases using template scripting and the built-in support for right-to-left languages. Phrases are deployed using the language package installation plugin, please also read the naming conventions for language items . Special Phrases # wcf.date.dateFormat # Many characters in the format have a special meaning and will be replaced with date fragments. If you want to include a literal character, you'll have to use the backslash \\ as an escape sequence to indicate that the character should be output as-is rather than being replaced. For example, Y-m-d will be output as 2018-03-30 , but \\Y-m-d will result in Y-03-30 . Defaults to M jS Y . The date format without time using PHP's format characters for the date() function. This value is also used inside the JavaScript implementation, where the characters are mapped to an equivalent representation. wcf.date.timeFormat # Defaults to g:i a . The date format that is used to represent a time, but not a date. Please see the explanation on wcf.date.dateFormat to learn more about the format characters. wcf.date.firstDayOfTheWeek # Defaults to 0 . Sets the first day of the week: * 0 - Sunday * 1 - Monday wcf.global.pageDirection - RTL support # Defaults to ltr . Changing this value to rtl will reverse the page direction and enable the right-to-left support for phrases. Additionally, a special version of the stylesheet is loaded that contains all necessary adjustments for the reverse direction.","title":"Languages"},{"location":"view/languages/#languages","text":"WoltLab Suite offers full i18n support with its integrated language system, including but not limited to dynamic phrases using template scripting and the built-in support for right-to-left languages. Phrases are deployed using the language package installation plugin, please also read the naming conventions for language items .","title":"Languages"},{"location":"view/languages/#special-phrases","text":"","title":"Special Phrases"},{"location":"view/languages/#wcfdatedateformat","text":"Many characters in the format have a special meaning and will be replaced with date fragments. If you want to include a literal character, you'll have to use the backslash \\ as an escape sequence to indicate that the character should be output as-is rather than being replaced. For example, Y-m-d will be output as 2018-03-30 , but \\Y-m-d will result in Y-03-30 . Defaults to M jS Y . The date format without time using PHP's format characters for the date() function. This value is also used inside the JavaScript implementation, where the characters are mapped to an equivalent representation.","title":"wcf.date.dateFormat"},{"location":"view/languages/#wcfdatetimeformat","text":"Defaults to g:i a . The date format that is used to represent a time, but not a date. Please see the explanation on wcf.date.dateFormat to learn more about the format characters.","title":"wcf.date.timeFormat"},{"location":"view/languages/#wcfdatefirstdayoftheweek","text":"Defaults to 0 . Sets the first day of the week: * 0 - Sunday * 1 - Monday","title":"wcf.date.firstDayOfTheWeek"},{"location":"view/languages/#wcfglobalpagedirection-rtl-support","text":"Defaults to ltr . Changing this value to rtl will reverse the page direction and enable the right-to-left support for phrases. Additionally, a special version of the stylesheet is loaded that contains all necessary adjustments for the reverse direction.","title":"wcf.global.pageDirection - RTL support"},{"location":"view/template-plugins/","text":"Template Plugins # 5.3+ anchor # The anchor template plugin creates a HTML elements. The easiest way to use the template plugin is to pass it an instance of ITitledLinkObject : 1 { anchor object = $object } generates the same output as 1 getLink () } \"> { $object -> getTitle () } Instead of an object parameter, a link and content parameter can be used: 1 { anchor link = $linkObject content = $content } where $linkObject implements ILinkableObject and $content is either an object implementing ITitledObject or having a __toString() method or $content is a string or a number. The last special attribute is append whose contents are appended to the href attribute of the generated anchor element. All of the other attributes matching ~^[a-z]+([A-z]+)+$~ , expect for href which is disallowed, are added as attributes to the anchor element. If an object attribute is present, the object also implements IPopoverObject and if the return value of IPopoverObject::getPopoverLinkClass() is included in the class attribute of the anchor tag, data-object-id is automatically added. This functionality makes it easy to generate links with popover support. Instead of 1 getLink () } \" class=\"blogEntryLink\" data-object-id=\" { @ $entry -> entryID } \"> { $entry -> subject } using 1 { anchor object = $entry class = 'blogEntryLink' } is sufficient if Entry::getPopoverLinkClass() returns blogEntryLink . 5.3+ anchorAttributes # anchorAttributes compliments the StringUtil::getAnchorTagAttributes(string, bool): string method. It allows to easily generate the necessary attributes for an anchor tag based off the destination URL. 1 Attribute Description url destination URL appendHref whether the href attribute should be generated; true by default isUgc whether the rel=\"ugc\" attribute should be generated; false by default appendClassname whether the class=\"externalURL\" attribute should be generated; true by default append # If a string should be appended to the value of a variable, append can be used: 1 2 3 4 5 6 7 { assign var = templateVariable value = 'newValue' } { $templateVariable } { * prints 'newValue * } { append var = templateVariable value = '2' } { $templateVariable } { * now prints 'newValue2 * } If the variables does not exist yet, append creates a new one with the given value. If append is used on an array as the variable, the value is appended to all elements of the array. assign # New template variables can be declared and new values can be assigned to existing template variables using assign : 1 2 3 { assign var = templateVariable value = 'newValue' } { $templateVariable } { * prints 'newValue * } capture # In some situations, assign is not sufficient to assign values to variables in templates if the value is complex. Instead, capture can be used: 1 2 3 4 5 6 7 { capture var = templateVariable } { if $foo }

    { $bar }

    { else } { $baz } { /if } { /capture } concat # concat is a modifier used to concatenate multiple strings: 1 2 3 4 5 { assign var = foo value = 'foo' } { assign var = templateVariable value = 'bar' | concat : $foo } { $templateVariable } { * prints 'foobar * } counter # counter can be used to generate and optionally print a counter: 1 2 3 4 5 6 7 { counter name = fooCounter print = true } { * prints '1' * } { counter name = fooCounter print = true } { * prints '2' now * } { counter name = fooCounter } { * prints nothing, but counter value is '3' now internally * } { counter name = fooCounter print = true } { * prints '4' * } Counter supports the following attributes: Attribute Description assign optional name of the template variable the current counter value is assigned to direction counting direction, either up or down ; up by default name name of the counter, relevant if multiple counters are used simultaneously print if true , the current counter value is printed; false by default skip positive counting increment; 1 by default start start counter value; 1 by default 5.4+ csrfToken # {csrfToken} prints out the session's CSRF token (\u201cSecurity Token\u201d). 1 2 3 4 5
    { * snip * } { csrfToken } The {csrfToken} template plugin supports a type parameter. Specifying this parameter might be required in rare situations. Please check the implementation for details. currency # currency is a modifier used to format currency values with two decimals using language dependent thousands separators and decimal point: 1 2 3 { assign var = currencyValue value = 12.345 } { $currencyValue | currency } { * prints '12.34' * } cycle # cycle can be used to cycle between different values: 1 2 3 4 5 6 7 { cycle name = fooCycle values = 'bar,baz' } { * prints 'bar' * } { cycle name = fooCycle } { * prints 'baz' * } { cycle name = fooCycle advance = false } { * prints 'baz' again * } { cycle name = fooCycle } { * prints 'bar' * } The values attribute only has to be present for the first call. If cycle is used in a loop, the presence of the same values in consecutive calls has no effect. Only once the values change, the cycle is reset. Attribute Description advance if true , the current cycle value is advanced to the next value; true by default assign optional name of the template variable the current cycle value is assigned to; if used, print is set to false delimiter delimiter between the different cycle values; , by default name name of the cycle, relevant if multiple cycles are used simultaneously print if true , the current cycle value is printed, false by default reset if true , the current cycle value is set to the first value, false by default values string containing the different cycles values, also see delimiter date # date generated a formatted date using wcf\\util\\DateUtil::format() with DateUtil::DATE_FORMAT internally. 1 { $timestamp | date } 3.1+ dateInterval # dateInterval calculates the difference between two unix timestamps and generated a textual date interval. 1 { dateInterval start = $startTimestamp end = $endTimestamp full = true format = 'sentence' } Attribute Description end end of the time interval; current timestamp by default (though either start or end has to be set) format output format, either default , sentence , or plain ; defaults to default , see wcf\\util\\DateUtil::FORMAT_* constants full if true , full difference in minutes is shown; if false , only the longest time interval is shown; false by default start start of the time interval; current timestamp by default (though either start or end has to be set) encodeJS # encodeJS encodes a string to be used as a single-quoted string in JavaScript by replacing \\\\ with \\\\\\\\ , ' with \\' , linebreaks with \\n , and / with \\/ . 1 2 3 encodeJSON # encodeJSON encodes a JSON string to be used as a single-quoted string in JavaScript by replacing \\\\ with \\\\\\\\ , ' with ' , linebreaks with \\n , and / with \\/ . Additionally, htmlspecialchars is applied to the string. 1 ' { @ $foo | encodeJSON } ' escapeCDATA # escapeCDATA encodes a string to be used in a CDATA element by replacing ]]> with ]]]]> . 1 event # event provides extension points in templates that template listeners can use. 1 { event name = 'foo' } fetch # fetch fetches the contents of a file using file_get_contents . 1 2 3 { fetch file = 'foo.html' } { * prints the contents of `foo.html` * } { fetch file = 'bar.html' assign = bar } { * assigns the contents of `foo.html` to `$bar`; does not print the contents * } filesizeBinary # filesizeBinary formats the filesize using binary filesize (in bytes). 1 { $filesize | filesizeBinary } filesize # filesize formats the filesize using filesize (in bytes). 1 { $filesize | filesize } hascontent # In many cases, conditional statements can be used to determine if a certain section of a template is shown: 1 2 3 { if $foo === 'bar' } only shown if $foo is bar { /if } In some situations, however, such conditional statements are not sufficient. One prominent example is a template event: 1 2 3 4 5 6 7 8 9 { if $foo === 'bar' }
     1
    +
    +
    userNotificationEvent.xml
    +
     1
      2
      3
      4
    @@ -2131,6 +2133,7 @@ Defines whether this type of email notifications is enabled by default.

    </data>
    +
    @@ -2139,7 +2142,7 @@ Defines whether this type of email notifications is enabled by default.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/package/pip/user-profile-menu/index.html b/5.4/package/pip/user-profile-menu/index.html index 17824c50..1e669244 100644 --- a/5.4/package/pip/user-profile-menu/index.html +++ b/5.4/package/pip/user-profile-menu/index.html @@ -2068,7 +2068,9 @@ the class has to implement the wcf\system\menu\user\profile\content\IUserP

    Optional

    The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the tab to be shown.

    Example#

    -
     1
    +
    +
    userProfileMenu.xml
    +
     1
      2
      3
      4
    @@ -2089,6 +2091,7 @@ the class has to implement the wcf\system\menu\user\profile\content\IUserP
     </data>
     
    +
    @@ -2097,7 +2100,7 @@ the class has to implement the wcf\system\menu\user\profile\content\IUserP
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/api/caches_persistent-caches/index.html b/5.4/php/api/caches_persistent-caches/index.html index 4d9bf80b..e171a37f 100644 --- a/5.4/php/api/caches_persistent-caches/index.html +++ b/5.4/php/api/caches_persistent-caches/index.html @@ -2060,37 +2060,12 @@ of background on caches and examples that should help you in your decision.

    AbstractCacheBuilder#

    Every cache builder should derive from the base class AbstractCacheBuilder that already implements the mandatory interface ICacheBuilder.

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    <?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;
    -  }
    -}
    -
    -
    +
    +
    php
    + ```php/api/caches/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 parameters it requires and how the returned data looks like. It does not need @@ -2134,7 +2109,7 @@ rebuild for whatever reason.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/api/caches_runtime-caches/index.html b/5.4/php/api/caches_runtime-caches/index.html index a8862709..0b2b4230 100644 --- a/5.4/php/api/caches_runtime-caches/index.html +++ b/5.4/php/api/caches_runtime-caches/index.html @@ -2065,55 +2065,11 @@ In most instances, you only need to set the AbstractRuntimeCache::$listCla

    Example#

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    -24
    <?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;
    -}
    -
    -
    +
    +
    php
    + ```php/api/caches/UserRuntimeCache.class.php + ``` +
    @@ -2122,7 +2078,7 @@ In most instances, you only need to set the AbstractRuntimeCache::$listCla
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/api/cronjobs/index.html b/5.4/php/api/cronjobs/index.html index 0714fc7f..bd652a17 100644 --- a/5.4/php/api/cronjobs/index.html +++ b/5.4/php/api/cronjobs/index.html @@ -2004,7 +2004,9 @@

    This page focuses on the technical aspects of cronjobs, the cronjob package installation plugin page covers how you can actually register a cronjob.

    Example#

    -
     1
    +
    +
    files/lib/system/cronjob/LastActivityCronjob.class.php
    +
     1
      2
      3
      4
    @@ -2039,7 +2041,7 @@
     
     /**
      * 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>
    @@ -2053,16 +2055,18 @@
             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";
    +                        wcf".WCF_N."_session session
    +                SET     user_table.lastActivityTime = session.lastActivityTime
    +                WHERE   user_table.userID = session.userID
    +                    AND session.userID <> 0";
             $statement = WCF::getDB()->prepareStatement($sql);
             $statement->execute();
         }
     }
     
    +
    +

    ICronjob Interface#

    Every cronjob needs to implement the wcf\system\cronjob\ICronjob interface which requires the execute(Cronjob $cronjob) method to be implemented. This method is called by wcf\system\cronjob\CronjobScheduler when executing the cronjobs.

    @@ -2088,7 +2092,7 @@ EOT
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/api/events/index.html b/5.4/php/api/events/index.html index 080536fe..744f5851 100644 --- a/5.4/php/api/events/index.html +++ b/5.4/php/api/events/index.html @@ -2045,33 +2045,12 @@ A comprehensive list of all available events is provided Introductory Example#

    Let's start with a simple example to illustrate how the event system works. Consider this pre-existing class:

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    <?php
    -namespace wcf\system\example;
    -use wcf\system\event\EventHandler;
    -
    -class ExampleComponent {
    -    public $var = 1;
    -
    -    public function getVar() {
    -        EventHandler::getInstance()->fireAction($this, 'getVar');
    +
    +
    php
    + ```php/api/events/ExampleComponent.class.php + ``` +
    - return $this->var; - } -} -
    -

    where an event with event name getVar is fired in the getVar() method.

    If you create an object of this class and call the getVar() method, the return value will be 1, of course:

     1
    @@ -2104,23 +2083,12 @@ Consider this pre-existing class:

    Now, consider that we have registered the following event listener to this event:

    -
    1
    -2
    -3
    -4
    -5
    -6
    -7
    -8
    <?php
    -namespace wcf\system\event\listener;
    -
    -class ExampleEventListener implements IParameterizedEventListener {
    -    public function execute($eventObj, $className, $eventName, array &$parameters) {
    -        $eventObj->var = 2;
    -    }
    -}
    -
    -
    +
    +
    php
    + ```php/api/events/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(). As ExampleComponent::$var is a public property, the event listener code can change it and set it to 2.

    @@ -2146,102 +2114,29 @@ The only thing to do is to call the wcf\system\event\EventHandler::fireAct

    Example: Using $parameters argument#

    Consider the following method which gets some text that the methods parses.

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    <?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);
    +
    +
    php
    + ```php/api/events/ExampleParser1.class.php + ``` +
    - return $parameters['text']; - } -} -
    -

    After the default parsing by the method itself, the author wants to enable plugins to do additional parsing and thus fires an event and passes the parsed text as an additional parameter. Then, a plugin can deliver the following event listener

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    <?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]
    +
    +
    php
    + ```php/api/events/ExampleParserEventListener.class.php + ``` +
    - $parameters['text'] = $text; - } -} -
    -

    which can access the text via $parameters['text'].

    This example can also be perfectly used to illustrate how to name multiple events in the same method. Let's assume that the author wants to enable plugins to change the text before and after the method does its own parsing and thus fires two events:

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    <?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'];
    -    }
    -}
    -
    -
    +
    +
    php
    + ```php/api/events/ExampleParser2.class.php + ``` +
    +

    Advanced Example: Additional Form Field#

    One common reason to use event listeners is to add an additional field to a pre-existing form (in combination with template listeners, which we will not cover here). We will assume that users are able to do both, create and edit the objects via this form. @@ -2270,91 +2165,12 @@ The points in the program flow of AbstractFo

    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.

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    -24
    -25
    -26
    -27
    -28
    -29
    -30
    -31
    -32
    -33
    -34
    -35
    -36
    -37
    -38
    -39
    -40
    -41
    -42
    <?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');
    -        }
    -    }
    -}
    -
    -
    +
    +
    php
    + ```php/api/events/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. Furthermore, the type-hinting of the parameter illustrates in which contexts the method is actually called which will become clear in the following discussion of the individual methods:

    @@ -2368,53 +2184,11 @@ Furthermore, the type-hinting of the parameter illustrates in which contexts the
  • validate() also needs to be called in both cases as the input data always has to be validated.
  • Lastly, the following XML file has to be used to register the event listeners (you can find more information about how to register event listeners on the eventListener package installation plugin page):

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    <?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>
    -
    -
    +
    +
    xml
    + ```php/api/events/eventListener.xml + ``` +
    @@ -2423,7 +2197,7 @@ Furthermore, the type-hinting of the parameter illustrates in which contexts the
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/api/sitemaps/index.html b/5.4/php/api/sitemaps/index.html index 3a800c4a..33c2b3ff 100644 --- a/5.4/php/api/sitemaps/index.html +++ b/5.4/php/api/sitemaps/index.html @@ -1979,81 +1979,12 @@ The DatabaseObject class must implement the interface wcf\dat You can, for example, specify additional query conditions in the method.

    As an example, the implementation for users looks like this:

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    -24
    -25
    -26
    -27
    -28
    -29
    -30
    -31
    -32
    -33
    -34
    -35
    -36
    -37
    <?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');
    -    }
    -}
    -
    -
    +
    +
    php
    + ```php/api/sitemaps/UserSitemapObject.class.php + ``` +
    +

    Next, the sitemap object must be registered as an object type:

    1
     2
    @@ -2086,7 +2017,7 @@ The language variable follows the pattern wcf.acp.sitemap.objectType.{obje
     
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/api/user_notifications/index.html b/5.4/php/api/user_notifications/index.html index 50cbdaba..06540299 100644 --- a/5.4/php/api/user_notifications/index.html +++ b/5.4/php/api/user_notifications/index.html @@ -2015,184 +2015,27 @@

    WoltLab Suite includes a powerful user notification system that supports notifications directly shown on the website and emails sent immediately or on a daily basis.

    objectType.xml#

    For any type of object related to events, you have to define an object type for the object type definition com.woltlab.wcf.notification.objectType:

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    <?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>
    -
    -
    +
    +
    xml
    + ```php/api/user_notifications/objectType.xml + ``` +
    +

    The referenced class FooUserNotificationObjectType has to implement the IUserNotificationObjectType interface, which should be done by extending AbstractUserNotificationObjectType.

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    -24
    -25
    -26
    -27
    -28
    -29
    -30
    -31
    <?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;
    -}
    -
    -
    +
    +
    php
    + ```php/api/user_notifications/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 whose name you have to set as the value of the $decoratorClassName property.

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    -24
    -25
    -26
    -27
    -28
    -29
    -30
    -31
    -32
    -33
    -34
    -35
    -36
    -37
    -38
    -39
    -40
    -41
    -42
    -43
    -44
    <?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;
    -    }
    -}
    -
    -
    +Additionally, you have to create a class that implements the IUserNotificationObject whose name you have to set as the value of the $decoratorClassName property.

    +
    +
    php
    + ```php/api/user_notifications/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 interface so that the decorated Foo can handle this method call itself.
    • @@ -2204,311 +2047,21 @@ Additionally, you have to create a class that implements the userNotificationEvent.xml#

      Each event that you fire in your package needs to be registered using the user notification event package installation plugin. An example file might look like this:

      -
       1
      - 2
      - 3
      - 4
      - 5
      - 6
      - 7
      - 8
      - 9
      -10
      -11
      <?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>
      -
      -
      +
      +
      xml
      + ```php/api/user_notifications/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 interface by extending the AbstractUserNotificationEvent class or the AbstractSharedUserNotificationEvent 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.

      -
        1
      -  2
      -  3
      -  4
      -  5
      -  6
      -  7
      -  8
      -  9
      - 10
      - 11
      - 12
      - 13
      - 14
      - 15
      - 16
      - 17
      - 18
      - 19
      - 20
      - 21
      - 22
      - 23
      - 24
      - 25
      - 26
      - 27
      - 28
      - 29
      - 30
      - 31
      - 32
      - 33
      - 34
      - 35
      - 36
      - 37
      - 38
      - 39
      - 40
      - 41
      - 42
      - 43
      - 44
      - 45
      - 46
      - 47
      - 48
      - 49
      - 50
      - 51
      - 52
      - 53
      - 54
      - 55
      - 56
      - 57
      - 58
      - 59
      - 60
      - 61
      - 62
      - 63
      - 64
      - 65
      - 66
      - 67
      - 68
      - 69
      - 70
      - 71
      - 72
      - 73
      - 74
      - 75
      - 76
      - 77
      - 78
      - 79
      - 80
      - 81
      - 82
      - 83
      - 84
      - 85
      - 86
      - 87
      - 88
      - 89
      - 90
      - 91
      - 92
      - 93
      - 94
      - 95
      - 96
      - 97
      - 98
      - 99
      -100
      -101
      -102
      -103
      -104
      -105
      -106
      -107
      -108
      -109
      -110
      -111
      -112
      -113
      -114
      -115
      -116
      -117
      -118
      -119
      -120
      -121
      -122
      -123
      -124
      -125
      -126
      -127
      -128
      -129
      -130
      -131
      -132
      -133
      -134
      -135
      -136
      -137
      -138
      -139
      <?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);
      -    }
      -}
      -
      -
      +
      +
      php
      + ```php/api/user_notifications/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. @@ -2588,7 +2141,7 @@ In this case, you can use UserNotificationHandler::markAsConfirmed() - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/apps/index.html b/5.4/php/apps/index.html index b3d8128e..83a4258f 100644 --- a/5.4/php/apps/index.html +++ b/5.4/php/apps/index.html @@ -2200,7 +2200,7 @@ it includes everything that is required for a basic app.

    - Last update: 2021-04-07 + Last update: 2021-04-23
    diff --git a/5.4/php/code-style/index.html b/5.4/php/code-style/index.html index 193272ad..16f16f17 100644 --- a/5.4/php/code-style/index.html +++ b/5.4/php/code-style/index.html @@ -2284,51 +2284,12 @@ The following example illustrates the different cases that can occur when workin

    Static Getters (of DatabaseObject Classes)#

    Some database objects provide static getters, either if they are decorators or for a unique combination of database table columns, like wcf\data\box\Box::getBoxByIdentifier():

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    <?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);
    -    }
    -}
    -
    -
    +
    +
    php
    + ```php/code-style/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 name of such getters should generally follow the convention get{object type}By{column or other description}.

    @@ -2387,7 +2348,7 @@ Therefore, the line must be split into multiple lines with each argument in a se
    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/php/database-objects/index.html b/5.4/php/database-objects/index.html index a7f069fb..c1a3709b 100644 --- a/5.4/php/database-objects/index.html +++ b/5.4/php/database-objects/index.html @@ -2190,52 +2190,22 @@

    Developers are required to provide the proper DatabaseObject implementations themselves, they're not automatically generated, all though the actual code that needs to be written is rather small. The following examples assume the fictional database table wcf1_example, exampleID as the auto-incrementing primary key and the column bar to store some text.

    DatabaseObject#

    The basic model derives from wcf\data\DatabaseObject and provides a convenient constructor to fetch a single row or construct an instance using pre-loaded rows.

    -
    1
    -2
    -3
    -4
    -5
    <?php
    -namespace wcf\data\example;
    -use wcf\data\DatabaseObject;
    +
    +
    php
    + ```php/database-objects/Example.class.php + ``` +
    -class Example extends DatabaseObject {} -
    -

    The class is intended to be empty by default and there only needs to be code if you want to add additional logic to your model. Both the class name and primary key are determined by DatabaseObject using the namespace and class name of the derived class. The example above uses the namespace wcf\… which is used as table prefix and the class name Example is converted into exampleID, resulting in the database table name wcfN_example with the primary key exampleID.

    You can prevent this automatic guessing by setting the class properties $databaseTableName and $databaseTableIndexName manually.

    DatabaseObjectDecorator#

    If you already have a DatabaseObject class and would like to extend it with additional data or methods, for example by providing a class ViewableExample which features view-related changes without polluting the original object, you can use DatabaseObjectDecorator which a default implementation of a decorator for database objects.

    -
     1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    -10
    -11
    -12
    -13
    -14
    -15
    <?php
    -namespace wcf\data\example;
    -use wcf\data\DatabaseObjectDecorator;
    -
    -class ViewableExample extends DatabaseObjectDecorator {
    -    protected static $baseClass = Example::class;
    -
    -    public function getOutput() {
    -        $output = '';
    -
    -        // [determine output]
    +
    +
    php
    + ```php/database-objects/ViewableExample.class.php + ``` +
    - return $output; - } -} -
    -

    It is mandatory to set the static $baseClass property to the name of the decorated class.

    Like for any decorator, you can directly access the decorated object's properties and methods for a decorated object by accessing the property or calling the method on the decorated object. You can access the decorated objects directly via DatabaseObjectDecorator::getDecoratedObject().

    @@ -2244,21 +2214,12 @@ You can access the decorated objects directly via DatabaseObjectDecorator:

    This is the low-level interface to manipulate data rows, it is recommended to use AbstractDatabaseObjectAction.

    Adding, editing and deleting models is done using the DatabaseObjectEditor class that decorates a DatabaseObject and uses its data to perform the actions.

    -
    1
    -2
    -3
    -4
    -5
    -6
    -7
    <?php
    -namespace wcf\data\example;
    -use wcf\data\DatabaseObjectEditor;
    +
    +
    php
    + ```php/database-objects/ExampleEditor.class.php + ``` +
    -class ExampleEditor extends DatabaseObjectEditor { - protected static $baseClass = Example::class; -} -
    -

    The editor class requires you to provide the fully qualified name of the model, that is the class name including the complete namespace. Database table name and index key will be pulled directly from the model.

    Create a new row#

    Inserting a new row into the database table is provided through DatabaseObjectEditor::create() which yields a DatabaseObject instance after creation.

    @@ -2327,21 +2288,12 @@ You can access the decorated objects directly via DatabaseObjectDecorator:

    DatabaseObjectList#

    Every row is represented as a single instance of the model, but the instance creation deals with single rows only. Retrieving larger sets of rows would be quite inefficient due to the large amount of queries that will be dispatched. This is solved with the DatabaseObjectList object that exposes an interface to query the database table using arbitrary conditions for data selection. All rows will be fetched using a single query and the resulting rows are automatically loaded into separate models.

    -
    1
    -2
    -3
    -4
    -5
    -6
    -7
    <?php
    -namespace wcf\data\example;
    -use wcf\data\DatabaseObjectList;
    +
    +
    php
    + ```php/database-objects/ExampleList.class.php + ``` +
    -class ExampleList extends DatabaseObjectList { - public $className = Example::class; -} -
    -

    The following code listing illustrates loading a large set of examples and iterating over the list to retrieve the objects.

     1
      2
    @@ -2405,19 +2357,12 @@ This can be achieved by setting the $objectClassName property to th
     

    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:

    -
    1
    -2
    -3
    -4
    -5
    -6
    <?php
    -namespace wcf\data\example;
    +
    +
    php
    + ```php/database-objects/ViewableExampleList.class.php + ``` +
    -class ViewableExampleList extends ExampleList { - public $decoratorClassName = ViewableExample::class; -} -
    -

    AbstractDatabaseObjectAction#

    Row creation and manipulation can be performed using the aforementioned DatabaseObjectEditor class, but this approach has two major issues:

      @@ -2425,21 +2370,12 @@ This can be achieved by setting the $objectClassName property to th
    1. Data is passed to the database adapter without any further processing.

    The AbstractDatabaseObjectAction solves both problems by wrapping around the editor class and thus provide an additional layer between the action that should be taken and the actual process. The first problem is solved by a fixed set of events being fired, the second issue is addressed by having a single entry point for all data editing.

    -
    1
    -2
    -3
    -4
    -5
    -6
    -7
    <?php
    -namespace wcf\data\example;
    -use wcf\data\AbstractDatabaseObjectAction;
    +
    +
    php
    + ```php/database-objects/ExampleAction.class.php + ``` +
    -class ExampleAction extends AbstractDatabaseObjectAction { - public $className = ExampleEditor::class; -} -
    -

    Executing an Action#

    The method AbstractDatabaseObjectAction::validateAction() is internally used for AJAX method invocation and must not be called programmatically.

    @@ -2536,7 +2472,7 @@ This can be achieved by setting the $objectClassName property to th
    - Last update: 2021-02-11 + Last update: 2021-04-23
    diff --git a/5.4/php/gdpr/index.html b/5.4/php/gdpr/index.html index de7db8f6..3982876c 100644 --- a/5.4/php/gdpr/index.html +++ b/5.4/php/gdpr/index.html @@ -2120,7 +2120,9 @@ include any personal data stored for your plugin or app by yourself.

    The event export is fired before any data is sent out, but after any Core data has been dumped to the $data property.

    Example code#

    - For the table body\u2019s column, we need to make sure that the birthday is only show if it is actually set: 1 Adding Birthday in Front End # In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person\u2019s \u201cstatistics\u201d. 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: 1 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. Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable. To show the birthday, we use the following template code for the personStatistics template event, which again makes sure that the birthday is only shown if it is actually set: 1 2 3 4 { if $person -> birthday }
    { lang } wcf.person.birthday { /lang }
    { @ $person -> birthday | strtotime | date }
    { /if } templateListener.xml # The following code shows the templateListener.xml file used to install all mentioned template listeners: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 columnHeads admin {lang}wcf.person.birthday{/lang}]]> personList columns admin {if $person->birthday}{@$person->birthday|strtotime|date}{/if}]]> personList personStatistics user personList sortField user personList In cases where a template is used, we simply use the include syntax to load the template. eventListener.xml # 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. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 admin wcf\\acp\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener admin wcf\\acp\\form\\PersonAddForm createForm wcf\\system\\event\\listener\\BirthdayPersonAddFormListener 1 user wcf\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener 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 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 WoltLab Suite Core Tutorial: People (Birthday) Adds a birthday field to the people management system as part of a tutorial to create packages. 5.4.0 2021-04-16 WoltLab GmbH http://www.woltlab.com com.woltlab.wcf com.woltlab.wcf.people com.woltlab.wcf acp/database/install_com.woltlab.wcf.people.birthday.php This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people. The complete source code of this part can be found on GitHub .","title":"Part 2"},{"location":"tutorial/series/part_2/#part-2-event-and-template-listeners","text":"In the first part of this tutorial series, we have created the base structure of our people management package. In further parts, we will use the package of the first part as a basis to directly add new features. In order to explain how event listeners and template works, however, we will not directly adding a new feature to the package by altering it in this part, but we will assume that somebody else created the package and that we want to extend it the \u201ccorrect\u201d way by creating a plugin. The goal of the small plugin that will be created in this part is to add the birthday of the managed people. As in the first part, we will not bother with careful validation of the entered date but just make sure that it is a valid date.","title":"Part 2: Event and Template Listeners"},{"location":"tutorial/series/part_2/#package-functionality","text":"The package should provide the following possibilities/functions: List person\u2019s birthday (if set) in people list in the ACP Sort people list by birthday in the ACP Add or remove birthday when adding or editing person List person\u2019s birthday (if set) in people list in the front end Sort people list by birthday in the front end","title":"Package Functionality"},{"location":"tutorial/series/part_2/#used-components","text":"We will use the following package installation plugins: database package installation plugin , eventListener package installation plugin , file package installation plugin , language package installation plugin , template package installation plugin , templateListener package installation plugin . For more information about the event system, please refer to the dedicated page on events .","title":"Used Components"},{"location":"tutorial/series/part_2/#package-structure","text":"The package will have the following file structure: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u251c\u2500\u2500 eventListener.xml \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.birthday.php \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 system \u2502 \u2514\u2500\u2500 event \u2502 \u2514\u2500\u2500 listener \u2502 \u251c\u2500\u2500 BirthdayPersonAddFormListener.class.php \u2502 \u2514\u2500\u2500 BirthdaySortFieldPersonListPageListener.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 templateListener.xml \u2514\u2500\u2500 templates \u251c\u2500\u2500 __personListBirthday.tpl \u2514\u2500\u2500 __personListBirthdaySortField.tpl","title":"Package Structure"},{"location":"tutorial/series/part_2/#extending-person-model","text":"The existing model of a person only contains the person\u2019s 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 : 1 2 3 4 5 6 7 8 9 10 11 columns ([ DateDatabaseTableColumn :: create ( 'birthday' ), ]), ]; If we have a Person object , 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 .","title":"Extending Person Model"},{"location":"tutorial/series/part_2/#setting-birthday-in-acp","text":"To set the birthday of a person, we only have to add another form field with an event listener: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class BirthdayPersonAddFormListener extends AbstractEventListener { /** * @see AbstractFormBuilderForm::createForm() */ protected function onCreateForm ( PersonAddForm $form ) : void { /** @var FormContainer $dataContainer */ $dataContainer = $form -> form -> getNodeById ( 'data' ); $dataContainer -> appendChild ( DateFormField :: create ( 'birthday' ) -> label ( 'wcf.person.birthday' ) -> saveValueFormat ( 'Y-m-d' ) -> nullable () ); } } registered via 1 2 3 4 5 6 7 admin wcf\\acp\\form\\PersonAddForm createForm wcf\\system\\event\\listener\\BirthdayPersonAddFormListener 1 in eventListener.xml , see below . As BirthdayPersonAddFormListener extends AbstractEventListener and as the name of relevant event is createForm , AbstractEventListener internally automatically calls onCreateForm() with the event object as the parameter. It is important to set 1 so that the event listener is also executed for PersonEditForm , which extends PersonAddForm . The language item wcf.person.birthday used in the label is the only new one for this package: 1 2 3 4 5 6 < 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/5.4/language.xsd\" languagecode = \"de\" > < category name = \"wcf.person\" > < item name = \"wcf.person.birthday\" > 1 2 3 4 5 6 < 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/5.4/language.xsd\" languagecode = \"en\" > < category name = \"wcf.person\" > < item name = \"wcf.person.birthday\" > ","title":"Setting Birthday in ACP"},{"location":"tutorial/series/part_2/#adding-birthday-table-column-in-acp","text":"To add a birthday column to the person list page in the ACP, we need three parts: an event listener that makes the birthday database table column a valid sort field, a template listener that adds the birthday column to the table\u2019s head, and a template listener that adds the birthday column to the table\u2019s rows. The first part is a very simple class: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class BirthdaySortFieldPersonListPageListener extends AbstractEventListener { /** * @see SortablePage::validateSortField() */ public function onValidateSortField ( SortablePage $page ) : void { $page -> validSortFields [] = 'birthday' ; } } 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. As the relevant template codes are only one line each, we will simply put them directly in the templateListener.xml file that will be shown later on . The code for the table head is similar to the other th elements: 1 For the table body\u2019s column, we need to make sure that the birthday is only show if it is actually set: 1 ","title":"Adding Birthday Table Column in ACP"},{"location":"tutorial/series/part_2/#adding-birthday-in-front-end","text":"In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person\u2019s \u201cstatistics\u201d. 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: 1 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. Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable. To show the birthday, we use the following template code for the personStatistics template event, which again makes sure that the birthday is only shown if it is actually set: 1 2 3 4 { if $person -> birthday }
    { lang } wcf.person.birthday { /lang }
    { @ $person -> birthday | strtotime | date }
    { /if }","title":"Adding Birthday in Front End"},{"location":"tutorial/series/part_2/#templatelistenerxml","text":"The following code shows the templateListener.xml file used to install all mentioned template listeners: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 columnHeads admin {lang}wcf.person.birthday{/lang}]]> personList columns admin {if $person->birthday}{@$person->birthday|strtotime|date}{/if}]]> personList personStatistics user personList sortField user personList In cases where a template is used, we simply use the include syntax to load the template.","title":"templateListener.xml"},{"location":"tutorial/series/part_2/#eventlistenerxml","text":"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. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 admin wcf\\acp\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener admin wcf\\acp\\form\\PersonAddForm createForm wcf\\system\\event\\listener\\BirthdayPersonAddFormListener 1 user wcf\\page\\PersonListPage validateSortField wcf\\system\\event\\listener\\BirthdaySortFieldPersonListPageListener ","title":"eventListener.xml"},{"location":"tutorial/series/part_2/#packagexml","text":"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 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 WoltLab Suite Core Tutorial: People (Birthday) Adds a birthday field to the people management system as part of a tutorial to create packages. 5.4.0 2021-04-16 WoltLab GmbH http://www.woltlab.com com.woltlab.wcf com.woltlab.wcf.people com.woltlab.wcf acp/database/install_com.woltlab.wcf.people.birthday.php This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people. The complete source code of this part can be found on GitHub .","title":"package.xml"},{"location":"tutorial/series/part_3/","text":"Part 3: Person Page and Comments # In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details. To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite\u2019s reusable comment functionality. Package Functionality # In addition to the existing functions from part 1 , the package will provide the following possibilities/functions after this part of the tutorial: Details page for each person linked in the front end person list Comment on people on their respective page (can be disabled per person) User online location for person details page with name and link to person details page Create menu items linking to specific person details pages Used Components # In addition to the components used in part 1 , we will use the objectType package installation plugin , use the comment API , create a runtime cache , and create a page handler. Package Structure # The complete package will have the following file structure (including the files from part 1 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u251c\u2500\u2500 acpMenu.xml \u251c\u2500\u2500 acptemplates \u2502 \u251c\u2500\u2500 personAdd.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u2514\u2500\u2500 lib \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u251c\u2500\u2500 form \u2502 \u2502 \u2502 \u251c\u2500\u2500 PersonAddForm.class.php \u2502 \u2502 \u2502 \u2514\u2500\u2500 PersonEditForm.class.php \u2502 \u2502 \u2514\u2500\u2500 page \u2502 \u2502 \u2514\u2500\u2500 PersonListPage.class.php \u2502 \u251c\u2500\u2500 data \u2502 \u2502 \u2514\u2500\u2500 person \u2502 \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonAction.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonEditor.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonList.class.php \u2502 \u251c\u2500\u2500 page \u2502 \u2502 \u251c\u2500\u2500 PersonListPage.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonPage.class.php \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 cache \u2502 \u2502 \u2514\u2500\u2500 runtime \u2502 \u2502 \u2514\u2500\u2500 PersonRuntimeCache.class.php \u2502 \u251c\u2500\u2500 comment \u2502 \u2502 \u2514\u2500\u2500 manager \u2502 \u2502 \u2514\u2500\u2500 PersonCommentManager.class.php \u2502 \u2514\u2500\u2500 page \u2502 \u2514\u2500\u2500 handler \u2502 \u2514\u2500\u2500 PersonPageHandler.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 menuItem.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 page.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u2514\u2500\u2500 userGroupOption.xml We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new Person::getLink() method and new language items have been added. For all changes, please refer to the source code on GitHub . Runtime Cache # To reduce the number of database queries when different APIs require person objects, we implement a runtime cache for people: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 * @package WoltLabSuite\\Core\\System\\Cache\\Runtime * * @method Person[] getCachedObjects() * @method Person getObject($objectID) * @method Person[] getObjects(array $objectIDs) */ class PersonRuntimeCache extends AbstractRuntimeCache { /** * @inheritDoc */ protected $listClassName = PersonList :: class ; } 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 : 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.person.personComment com.woltlab.wcf.comment.commentableContent wcf\\system\\comment\\manager\\PersonCommentManager The PersonCommentManager class extended ICommentManager \u2019s default implementation AbstractCommentManager : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 * @package WoltLabSuite\\Core\\System\\Comment\\Manager */ class PersonCommentManager extends AbstractCommentManager { /** * @inheritDoc */ protected $permissionAdd = 'user.person.canAddComment' ; /** * @inheritDoc */ protected $permissionAddWithoutModeration = 'user.person.canAddCommentWithoutModeration' ; /** * @inheritDoc */ protected $permissionCanModerate = 'mod.person.canModerateComment' ; /** * @inheritDoc */ protected $permissionDelete = 'user.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionEdit = 'user.person.canEditComment' ; /** * @inheritDoc */ protected $permissionModDelete = 'mod.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionModEdit = 'mod.person.canEditComment' ; /** * @inheritDoc */ public function getLink ( $objectTypeID , $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * @inheritDoc */ public function isAccessible ( $objectID , $validateWritePermission = false ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function getTitle ( $objectTypeID , $objectID , $isResponse = false ) { if ( $isResponse ) { return WCF :: getLanguage () -> get ( 'wcf.person.commentResponse' ); } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.person.comment' ); } /** * @inheritDoc */ public function updateCounter ( $objectID , $value ) { ( new PersonEditor ( new Person ( $objectID ))) -> updateCounters ([ 'comments' => $value ]); } } First, the system is told the names of the permissions via the $permission* properties. More information about comment permissions can be found here . The getLink() method returns the link to the person with the passed comment id. As in isAccessible() , PersonRuntimeCache is used to potentially save database queries. The isAccessible() method checks if the active user can access the relevant person. As we do not have any special restrictions for accessing people, we only need to check if the person exists. The getTitle() method returns the title used for comments and responses, which is just a generic language item in this case. The updateCounter() updates the comments\u2019 counter of the person. We have added a new comments database table column to the wcf1_person database table in order to keep track on the number of comments. Additionally, we have added a new enableComments database table column to the wcf1_person database table whose value can be set when creating or editing a person in the ACP. With this option, comments on individual people can be disabled. Liking comments is already built-in and only requires some extra code in the PersonPage class for showing the likes of pre-loaded comments. Person Page # PersonPage # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 * @package WoltLabSuite\\Core\\Page */ class PersonPage extends AbstractPage { /** * list of comments * @var StructuredCommentList */ public $commentList ; /** * person comment manager object * @var PersonCommentManager */ public $commentManager ; /** * id of the person comment object type * @var integer */ public $commentObjectTypeID = 0 ; /** * shown person * @var Person */ public $person ; /** * id of the shown person * @var integer */ public $personID = 0 ; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'commentCanAdd' => WCF :: getSession () -> getPermission ( 'user.person.canAddComment' ), 'commentList' => $this -> commentList , 'commentObjectTypeID' => $this -> commentObjectTypeID , 'lastCommentTime' => $this -> commentList ? $this -> commentList -> getMinCommentTime () : 0 , 'likeData' => MODULE_LIKE && $this -> commentList ? $this -> commentList -> getLikeData () : [], 'person' => $this -> person , ]); } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( $this -> person -> enableComments ) { $this -> commentObjectTypeID = CommentHandler :: getInstance () -> getObjectTypeID ( 'com.woltlab.wcf.person.personComment' ); $this -> commentManager = CommentHandler :: getInstance () -> getObjectType ( $this -> commentObjectTypeID ) -> getProcessor (); $this -> commentList = CommentHandler :: getInstance () -> getCommentList ( $this -> commentManager , $this -> commentObjectTypeID , $this -> person -> personID ); } } /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) { $this -> personID = \\intval ( $_REQUEST [ 'id' ]); } $this -> person = new Person ( $this -> personID ); if ( ! $this -> person -> personID ) { throw new IllegalLinkException (); } } } The PersonPage class is similar to the PersonEditForm in the ACP in that it reads the id of the requested person from the request data and validates the id in readParameters() . The rest of the code only handles fetching the list of comments on the requested person. In readData() , this list is fetched using CommentHandler::getCommentList() if comments are enabled for the person. The assignVariables() method assigns some additional template variables like $commentCanAdd , which is 1 if the active person can add comments and is 0 otherwise, $lastCommentTime , which contains the UNIX timestamp of the last comment, and $likeData , which contains data related to the likes for the disabled comments. person.tpl # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 {capture assign='pageTitle'}{$person} - {lang}wcf.person.list{/lang}{/capture} {capture assign='contentTitle'}{$person}{/capture} {include file='header'} {if $person->enableComments} {if $commentList|count || $commentCanAdd}

    {lang}wcf.person.comments{/lang} {if $person->comments}{#$person->comments}{/if}

    {include file='__commentJavaScript' commentContainerID='personCommentList'}
      personID}\" {* *}data-object-type-id=\"{@$commentObjectTypeID}\" {* *}data-comments=\"{if $person->comments}{@$commentList->countObjects()}{else}0{/if}\" {* *}data-last-comment-time=\"{@$lastCommentTime}\" {* *}> {include file='commentListAddComment' wysiwygSelector='personCommentListAddComment'} {include file='commentList'}
    {/if} {/if}
    {hascontent} {/hascontent}
    {include file='footer'} For now, the person template is still very empty and only shows the comments in the content area. The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container ul#personCommentList element for the comments shown by commentList template. The ul#personCommentList elements has five additional data- attributes required by the JavaScript API for comments for loading more comments or creating new ones. The commentListAddComment template adds the WYSIWYG support. The attribute wysiwygSelector should be the id of the comment list personCommentList with an additional AddComment suffix. page.xml # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 system wcf\\page\\PersonListPage Personen-Liste Person List Personen People system wcf\\page\\PersonPage wcf\\system\\page\\handler\\PersonPageHandler Person Person 1 com.woltlab.wcf.people.PersonList 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: It has a element with a class name as value. This aspect will be discussed in more detail in the next section. There are no elements because, both, the title and the content of the page are dynamically generated in the template. The tells the system that this page requires an object id to properly work, in this case a valid person id. This page has a page, the person list page. In general, the details page for any type of object that is listed on a different page has the list page as its parent. PersonPageHandler # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 * @package WoltLabSuite\\Core\\System\\Page\\Handler */ class PersonPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler { use TOnlineLocationPageHandler ; /** * @inheritDoc */ public function getLink ( $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * Returns the textual description if a user is currently online viewing this page. * * @see IOnlineLocationPageHandler::getOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data * @return string */ public function getOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID === null ) { return '' ; } $person = PersonRuntimeCache :: getInstance () -> getObject ( $user -> pageObjectID ); if ( $person === null ) { return '' ; } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.page.onlineLocation.' . $page -> identifier , [ 'person' => $person ]); } /** * @inheritDoc */ public function isValid ( $objectID = null ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function lookup ( $searchString ) { $conditionBuilder = new PreparedStatementConditionBuilder ( false , 'OR' ); $conditionBuilder -> add ( 'person.firstName LIKE ?' , [ '%' . $searchString . '%' ]); $conditionBuilder -> add ( 'person.lastName LIKE ?' , [ '%' . $searchString . '%' ]); $personList = new PersonList (); $personList -> getConditionBuilder () -> add ( $conditionBuilder , $conditionBuilder -> getParameters ()); $personList -> readObjects (); $results = []; foreach ( $personList as $person ) { $results [] = [ 'image' => 'fa-user' , 'link' => $person -> getLink (), 'objectID' => $person -> personID , 'title' => $person -> getTitle (), ]; } return $results ; } /** * Prepares fetching all necessary data for the textual description if a user is currently online * viewing this page. * * @see IOnlineLocationPageHandler::prepareOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data */ public function prepareOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID !== null ) { PersonRuntimeCache :: getInstance () -> cacheObjectID ( $user -> pageObjectID ); } } } Like any page handler, the PersonPageHandler class has to implement the IMenuPageHandler interface, which should be done by extending the AbstractMenuPageHandler class. As we want administrators to link to specific people in menus, for example, we have to also implement the ILookupPageHandler interface by extending the AbstractLookupPageHandler class. For the ILookupPageHandler interface, we need to implement three methods: getLink($objectID) returns the link to the person page with the given id. In this case, we simply delegate this method call to the Person object returned by PersonRuntimeCache::getObject() . isValid($objectID) returns true if the person with the given id exists, otherwise false . Here, we use PersonRuntimeCache::getObject() again and check if the return value is null , which is the case for non-existing people. lookup($searchString) is used when setting up an internal link and when searching for the linked person. This method simply searches the first and last name of the people and returns an array with the person data. While the link , the objectID , and the title element are self-explanatory, the image element can either contain an HTML tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with fa- ). Additionally, the class also implements IOnlineLocationPageHandler which is used to determine the online location of users. To ensure upwards-compatibility if the IOnlineLocationPageHandler interface changes, the TOnlineLocationPageHandler trait is used. The IOnlineLocationPageHandler interface requires two methods to be implemented: getOnlineLocation(Page $page, UserOnline $user) returns the textual description of the online location. The language item for the user online locations should use the pattern wcf.page.onlineLocation.{page identifier} . prepareOnlineLocation(Page $page, UserOnline $user) is called for each user online before the getOnlineLocation() calls. In this case, calling prepareOnlineLocation() first enables us to add all relevant person ids to the person runtime cache so that for all getOnlineLocation() calls combined, only one database query is necessary to fetch all person objects. This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person. The complete source code of this part can be found on GitHub .","title":"Part 3"},{"location":"tutorial/series/part_3/#part-3-person-page-and-comments","text":"In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details. To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite\u2019s reusable comment functionality.","title":"Part 3: Person Page and Comments"},{"location":"tutorial/series/part_3/#package-functionality","text":"In addition to the existing functions from part 1 , the package will provide the following possibilities/functions after this part of the tutorial: Details page for each person linked in the front end person list Comment on people on their respective page (can be disabled per person) User online location for person details page with name and link to person details page Create menu items linking to specific person details pages","title":"Package Functionality"},{"location":"tutorial/series/part_3/#used-components","text":"In addition to the components used in part 1 , we will use the objectType package installation plugin , use the comment API , create a runtime cache , and create a page handler.","title":"Used Components"},{"location":"tutorial/series/part_3/#package-structure","text":"The complete package will have the following file structure (including the files from part 1 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u251c\u2500\u2500 acpMenu.xml \u251c\u2500\u2500 acptemplates \u2502 \u251c\u2500\u2500 personAdd.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u2514\u2500\u2500 lib \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u251c\u2500\u2500 form \u2502 \u2502 \u2502 \u251c\u2500\u2500 PersonAddForm.class.php \u2502 \u2502 \u2502 \u2514\u2500\u2500 PersonEditForm.class.php \u2502 \u2502 \u2514\u2500\u2500 page \u2502 \u2502 \u2514\u2500\u2500 PersonListPage.class.php \u2502 \u251c\u2500\u2500 data \u2502 \u2502 \u2514\u2500\u2500 person \u2502 \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonAction.class.php \u2502 \u2502 \u251c\u2500\u2500 PersonEditor.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonList.class.php \u2502 \u251c\u2500\u2500 page \u2502 \u2502 \u251c\u2500\u2500 PersonListPage.class.php \u2502 \u2502 \u2514\u2500\u2500 PersonPage.class.php \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 cache \u2502 \u2502 \u2514\u2500\u2500 runtime \u2502 \u2502 \u2514\u2500\u2500 PersonRuntimeCache.class.php \u2502 \u251c\u2500\u2500 comment \u2502 \u2502 \u2514\u2500\u2500 manager \u2502 \u2502 \u2514\u2500\u2500 PersonCommentManager.class.php \u2502 \u2514\u2500\u2500 page \u2502 \u2514\u2500\u2500 handler \u2502 \u2514\u2500\u2500 PersonPageHandler.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 menuItem.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 page.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u2514\u2500\u2500 userGroupOption.xml We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new Person::getLink() method and new language items have been added. For all changes, please refer to the source code on GitHub .","title":"Package Structure"},{"location":"tutorial/series/part_3/#runtime-cache","text":"To reduce the number of database queries when different APIs require person objects, we implement a runtime cache for people: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 * @package WoltLabSuite\\Core\\System\\Cache\\Runtime * * @method Person[] getCachedObjects() * @method Person getObject($objectID) * @method Person[] getObjects(array $objectIDs) */ class PersonRuntimeCache extends AbstractRuntimeCache { /** * @inheritDoc */ protected $listClassName = PersonList :: class ; }","title":"Runtime Cache"},{"location":"tutorial/series/part_3/#comments","text":"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 : 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.person.personComment com.woltlab.wcf.comment.commentableContent wcf\\system\\comment\\manager\\PersonCommentManager The PersonCommentManager class extended ICommentManager \u2019s default implementation AbstractCommentManager : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 * @package WoltLabSuite\\Core\\System\\Comment\\Manager */ class PersonCommentManager extends AbstractCommentManager { /** * @inheritDoc */ protected $permissionAdd = 'user.person.canAddComment' ; /** * @inheritDoc */ protected $permissionAddWithoutModeration = 'user.person.canAddCommentWithoutModeration' ; /** * @inheritDoc */ protected $permissionCanModerate = 'mod.person.canModerateComment' ; /** * @inheritDoc */ protected $permissionDelete = 'user.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionEdit = 'user.person.canEditComment' ; /** * @inheritDoc */ protected $permissionModDelete = 'mod.person.canDeleteComment' ; /** * @inheritDoc */ protected $permissionModEdit = 'mod.person.canEditComment' ; /** * @inheritDoc */ public function getLink ( $objectTypeID , $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * @inheritDoc */ public function isAccessible ( $objectID , $validateWritePermission = false ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function getTitle ( $objectTypeID , $objectID , $isResponse = false ) { if ( $isResponse ) { return WCF :: getLanguage () -> get ( 'wcf.person.commentResponse' ); } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.person.comment' ); } /** * @inheritDoc */ public function updateCounter ( $objectID , $value ) { ( new PersonEditor ( new Person ( $objectID ))) -> updateCounters ([ 'comments' => $value ]); } } First, the system is told the names of the permissions via the $permission* properties. More information about comment permissions can be found here . The getLink() method returns the link to the person with the passed comment id. As in isAccessible() , PersonRuntimeCache is used to potentially save database queries. The isAccessible() method checks if the active user can access the relevant person. As we do not have any special restrictions for accessing people, we only need to check if the person exists. The getTitle() method returns the title used for comments and responses, which is just a generic language item in this case. The updateCounter() updates the comments\u2019 counter of the person. We have added a new comments database table column to the wcf1_person database table in order to keep track on the number of comments. Additionally, we have added a new enableComments database table column to the wcf1_person database table whose value can be set when creating or editing a person in the ACP. With this option, comments on individual people can be disabled. Liking comments is already built-in and only requires some extra code in the PersonPage class for showing the likes of pre-loaded comments.","title":"Comments"},{"location":"tutorial/series/part_3/#person-page","text":"","title":"Person Page"},{"location":"tutorial/series/part_3/#personpage","text":"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 * @package WoltLabSuite\\Core\\Page */ class PersonPage extends AbstractPage { /** * list of comments * @var StructuredCommentList */ public $commentList ; /** * person comment manager object * @var PersonCommentManager */ public $commentManager ; /** * id of the person comment object type * @var integer */ public $commentObjectTypeID = 0 ; /** * shown person * @var Person */ public $person ; /** * id of the shown person * @var integer */ public $personID = 0 ; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'commentCanAdd' => WCF :: getSession () -> getPermission ( 'user.person.canAddComment' ), 'commentList' => $this -> commentList , 'commentObjectTypeID' => $this -> commentObjectTypeID , 'lastCommentTime' => $this -> commentList ? $this -> commentList -> getMinCommentTime () : 0 , 'likeData' => MODULE_LIKE && $this -> commentList ? $this -> commentList -> getLikeData () : [], 'person' => $this -> person , ]); } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( $this -> person -> enableComments ) { $this -> commentObjectTypeID = CommentHandler :: getInstance () -> getObjectTypeID ( 'com.woltlab.wcf.person.personComment' ); $this -> commentManager = CommentHandler :: getInstance () -> getObjectType ( $this -> commentObjectTypeID ) -> getProcessor (); $this -> commentList = CommentHandler :: getInstance () -> getCommentList ( $this -> commentManager , $this -> commentObjectTypeID , $this -> person -> personID ); } } /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) { $this -> personID = \\intval ( $_REQUEST [ 'id' ]); } $this -> person = new Person ( $this -> personID ); if ( ! $this -> person -> personID ) { throw new IllegalLinkException (); } } } The PersonPage class is similar to the PersonEditForm in the ACP in that it reads the id of the requested person from the request data and validates the id in readParameters() . The rest of the code only handles fetching the list of comments on the requested person. In readData() , this list is fetched using CommentHandler::getCommentList() if comments are enabled for the person. The assignVariables() method assigns some additional template variables like $commentCanAdd , which is 1 if the active person can add comments and is 0 otherwise, $lastCommentTime , which contains the UNIX timestamp of the last comment, and $likeData , which contains data related to the likes for the disabled comments.","title":"PersonPage"},{"location":"tutorial/series/part_3/#persontpl","text":"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 {capture assign='pageTitle'}{$person} - {lang}wcf.person.list{/lang}{/capture} {capture assign='contentTitle'}{$person}{/capture} {include file='header'} {if $person->enableComments} {if $commentList|count || $commentCanAdd}

    {lang}wcf.person.comments{/lang} {if $person->comments}{#$person->comments}{/if}

    {include file='__commentJavaScript' commentContainerID='personCommentList'}
      personID}\" {* *}data-object-type-id=\"{@$commentObjectTypeID}\" {* *}data-comments=\"{if $person->comments}{@$commentList->countObjects()}{else}0{/if}\" {* *}data-last-comment-time=\"{@$lastCommentTime}\" {* *}> {include file='commentListAddComment' wysiwygSelector='personCommentListAddComment'} {include file='commentList'}
    {/if} {/if}
    {hascontent} {/hascontent}
    {include file='footer'} For now, the person template is still very empty and only shows the comments in the content area. The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container ul#personCommentList element for the comments shown by commentList template. The ul#personCommentList elements has five additional data- attributes required by the JavaScript API for comments for loading more comments or creating new ones. The commentListAddComment template adds the WYSIWYG support. The attribute wysiwygSelector should be the id of the comment list personCommentList with an additional AddComment suffix.","title":"person.tpl"},{"location":"tutorial/series/part_3/#pagexml","text":"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 system wcf\\page\\PersonListPage Personen-Liste Person List Personen People system wcf\\page\\PersonPage wcf\\system\\page\\handler\\PersonPageHandler Person Person 1 com.woltlab.wcf.people.PersonList 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: It has a element with a class name as value. This aspect will be discussed in more detail in the next section. There are no elements because, both, the title and the content of the page are dynamically generated in the template. The tells the system that this page requires an object id to properly work, in this case a valid person id. This page has a page, the person list page. In general, the details page for any type of object that is listed on a different page has the list page as its parent.","title":"page.xml"},{"location":"tutorial/series/part_3/#personpagehandler","text":"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 * @package WoltLabSuite\\Core\\System\\Page\\Handler */ class PersonPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler { use TOnlineLocationPageHandler ; /** * @inheritDoc */ public function getLink ( $objectID ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) -> getLink (); } /** * Returns the textual description if a user is currently online viewing this page. * * @see IOnlineLocationPageHandler::getOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data * @return string */ public function getOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID === null ) { return '' ; } $person = PersonRuntimeCache :: getInstance () -> getObject ( $user -> pageObjectID ); if ( $person === null ) { return '' ; } return WCF :: getLanguage () -> getDynamicVariable ( 'wcf.page.onlineLocation.' . $page -> identifier , [ 'person' => $person ]); } /** * @inheritDoc */ public function isValid ( $objectID = null ) { return PersonRuntimeCache :: getInstance () -> getObject ( $objectID ) !== null ; } /** * @inheritDoc */ public function lookup ( $searchString ) { $conditionBuilder = new PreparedStatementConditionBuilder ( false , 'OR' ); $conditionBuilder -> add ( 'person.firstName LIKE ?' , [ '%' . $searchString . '%' ]); $conditionBuilder -> add ( 'person.lastName LIKE ?' , [ '%' . $searchString . '%' ]); $personList = new PersonList (); $personList -> getConditionBuilder () -> add ( $conditionBuilder , $conditionBuilder -> getParameters ()); $personList -> readObjects (); $results = []; foreach ( $personList as $person ) { $results [] = [ 'image' => 'fa-user' , 'link' => $person -> getLink (), 'objectID' => $person -> personID , 'title' => $person -> getTitle (), ]; } return $results ; } /** * Prepares fetching all necessary data for the textual description if a user is currently online * viewing this page. * * @see IOnlineLocationPageHandler::prepareOnlineLocation() * * @param Page $page visited page * @param UserOnline $user user online object with request data */ public function prepareOnlineLocation ( Page $page , UserOnline $user ) { if ( $user -> pageObjectID !== null ) { PersonRuntimeCache :: getInstance () -> cacheObjectID ( $user -> pageObjectID ); } } } Like any page handler, the PersonPageHandler class has to implement the IMenuPageHandler interface, which should be done by extending the AbstractMenuPageHandler class. As we want administrators to link to specific people in menus, for example, we have to also implement the ILookupPageHandler interface by extending the AbstractLookupPageHandler class. For the ILookupPageHandler interface, we need to implement three methods: getLink($objectID) returns the link to the person page with the given id. In this case, we simply delegate this method call to the Person object returned by PersonRuntimeCache::getObject() . isValid($objectID) returns true if the person with the given id exists, otherwise false . Here, we use PersonRuntimeCache::getObject() again and check if the return value is null , which is the case for non-existing people. lookup($searchString) is used when setting up an internal link and when searching for the linked person. This method simply searches the first and last name of the people and returns an array with the person data. While the link , the objectID , and the title element are self-explanatory, the image element can either contain an HTML tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with fa- ). Additionally, the class also implements IOnlineLocationPageHandler which is used to determine the online location of users. To ensure upwards-compatibility if the IOnlineLocationPageHandler interface changes, the TOnlineLocationPageHandler trait is used. The IOnlineLocationPageHandler interface requires two methods to be implemented: getOnlineLocation(Page $page, UserOnline $user) returns the textual description of the online location. The language item for the user online locations should use the pattern wcf.page.onlineLocation.{page identifier} . prepareOnlineLocation(Page $page, UserOnline $user) is called for each user online before the getOnlineLocation() calls. In this case, calling prepareOnlineLocation() first enables us to add all relevant person ids to the person runtime cache so that for all getOnlineLocation() calls combined, only one database query is necessary to fetch all person objects. This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person. The complete source code of this part can be found on GitHub .","title":"PersonPageHandler"},{"location":"tutorial/series/part_4/","text":"Part 4: Box and Box Conditions # In this part of our tutorial series, we add support for creating boxes listing people. Package Functionality # In addition to the existing functions from part 3 , the package will provide the following functionality after this part of the tutorial: Creating boxes dynamically listing people Filtering the people listed in boxes using conditions Used Components # In addition to the components used in previous parts, we will use the objectTypeDefinition package installation plugin and use the box and condition APIs. To pre-install a specific person list box, we refer to the documentation of the box package installation plugin . Package Structure # The complete package will have the following file structure ( excluding unchanged files from part 3 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u251c\u2500\u2500 files \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 box \u2502 \u2502 \u2514\u2500\u2500 PersonListBoxController.class.php \u2502 \u2514\u2500\u2500 condition \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 PersonFirstNameTextPropertyCondition.class.php \u2502 \u2514\u2500\u2500 PersonLastNameTextPropertyCondition.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 objectTypeDefinition.xml \u2514\u2500\u2500 templates \u2514\u2500\u2500 boxPersonList.tpl For all changes, please refer to the source code on GitHub . Box Controller # In addition to static boxes with fixed contents, administrators are able to create dynamic boxes with contents from the database. In our case here, we want administrators to be able to create boxes listing people. To do so, we first have to register a new object type for this person list box controller for the object type definition com.woltlab.wcf.boxController : 1 2 3 4 5 com.woltlab.wcf.personList com.woltlab.wcf.boxController wcf\\system\\box\\PersonListBoxController The com.woltlab.wcf.boxController object type definition requires the provided class to implement wcf\\system\\box\\IBoxController : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 * @package WoltLabSuite\\Core\\System\\Box */ class PersonListBoxController extends AbstractDatabaseObjectListBoxController { /** * @inheritDoc */ protected $conditionDefinition = 'com.woltlab.wcf.box.personList.condition' ; /** * @inheritDoc */ public $defaultLimit = 5 ; /** * @inheritDoc */ protected $sortFieldLanguageItemPrefix = 'wcf.person' ; /** * @inheritDoc */ protected static $supportedPositions = [ 'sidebarLeft' , 'sidebarRight' , ]; /** * @inheritDoc */ public $validSortFields = [ 'firstName' , 'lastName' , 'comments' , ]; /** * @inheritDoc */ public function getObjectList () { return new PersonList (); } /** * @inheritDoc */ protected function getTemplate () { return WCF :: getTPL () -> fetch ( 'boxPersonList' , 'wcf' , [ 'boxPersonList' => $this -> objectList , 'boxSortField' => $this -> sortField , 'boxPosition' => $this -> box -> position , ], true ); } } By extending AbstractDatabaseObjectListBoxController , we only have to provide minimal data ourself and rely mostly on the default implementation provided by AbstractDatabaseObjectListBoxController : As we will support conditions for the listed people, we have to set the relevant condition definition via $conditionDefinition . AbstractDatabaseObjectListBoxController already supports restricting the number of listed objects. To do so, you only have to specify the default number of listed objects via $defaultLimit . AbstractDatabaseObjectListBoxController also supports setting the sort order of the listed objects. You have to provide the supported sort fields via $validSortFields and specify the prefix used for the language items of the sort fields via $sortFieldLanguageItemPrefix so that for every $validSortField in $validSortFields , the language item {$sortFieldLanguageItemPrefix}.{$validSortField} must exist. The box system supports different positions . Each box controller specifies the positions it supports via $supportedPositions . To keep the implementation simple here as different positions might require different output in the template, we restrict ourselves to sidebars. getObjectList() returns an instance of DatabaseObjectList that is used to read the listed objects. getObjectList() itself must not call readObjects() , as AbstractDatabaseObjectListBoxController takes care of calling the method after adding the conditions and setting the sort order. getTemplate() returns the contents of the box relying on the boxPersonList template here: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
      { foreach from = $boxPersonList item = boxPerson }
    • { anchor object = $boxPerson }

      { capture assign = '__boxPersonDescription' }{ lang __optional = true } wcf.person.boxList.description. { $boxSortField }{ /lang }{ /capture } { if $__boxPersonDescription } { @ $__boxPersonDescription } { /if }
    • { /foreach }
    The template relies on a .sidebarItemList element, which is generally used for sidebar listings. (If different box positions were supported, we either have to generate different output by considering the value of $boxPosition in the template or by using different templates in getTemplate() .) One specific piece of code is the $__boxPersonDescription variable, which supports an optional description below the person's name relying on the optional language item wcf.person.boxList.description.{$boxSortField} . We only add one such language item when sorting the people by comments: In such a case, the number of comments will be shown. (When sorting by first and last name, there are no additional useful information that could be shown here, though the plugin from part 2 adding support for birthdays might also show the birthday when sorting by first or last name.) Lastly, we also provide the language item wcf.acp.box.boxController.com.woltlab.wcf.personList , which is used in the list of available box controllers. Conditions # The condition system can be used to generally filter a list of objects. In our case, the box system supports conditions to filter the objects shown in a specific box. Admittedly, our current person implementation only contains minimal data so that filtering might not make the most sense here but it will still show how to use the condition system for boxes. We will support filtering the people by their first and last name so that, for example, a box can be created listing all people with a specific first name. The first step for condition support is to register a object type definition for the relevant conditions requiring the IObjectListCondition interface: 1 2 3 4 5 6 7 8 9 com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\IObjectListCondition Next, we register the specific conditions for filtering by the first and last name using this object type condition: 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.people.firstName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonFirstNameTextPropertyCondition com.woltlab.wcf.people.lastName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonLastNameTextPropertyCondition 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: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 * @package WoltLabSuite\\Core\\System\\Condition */ class PersonFirstNameTextPropertyCondition extends AbstractObjectTextPropertyCondition { /** * @inheritDoc */ protected $className = Person :: class ; /** * @inheritDoc */ protected $description = 'wcf.person.condition.firstName.description' ; /** * @inheritDoc */ protected $fieldName = 'personFirstName' ; /** * @inheritDoc */ protected $label = 'wcf.person.firstName' ; /** * @inheritDoc */ protected $propertyName = 'firstName' ; /** * @inheritDoc */ protected $supportsMultipleValues = true ; } $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. 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. $description (optional), $fieldName , and $label are used in the output of the form field. (The implementation here is specific for AbstractObjectTextPropertyCondition . The wcf\\system\\condition namespace also contains several other default condition implementations.)","title":"Part 4"},{"location":"tutorial/series/part_4/#part-4-box-and-box-conditions","text":"In this part of our tutorial series, we add support for creating boxes listing people.","title":"Part 4: Box and Box Conditions"},{"location":"tutorial/series/part_4/#package-functionality","text":"In addition to the existing functions from part 3 , the package will provide the following functionality after this part of the tutorial: Creating boxes dynamically listing people Filtering the people listed in boxes using conditions","title":"Package Functionality"},{"location":"tutorial/series/part_4/#used-components","text":"In addition to the components used in previous parts, we will use the objectTypeDefinition package installation plugin and use the box and condition APIs. To pre-install a specific person list box, we refer to the documentation of the box package installation plugin .","title":"Used Components"},{"location":"tutorial/series/part_4/#package-structure","text":"The complete package will have the following file structure ( excluding unchanged files from part 3 ): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u251c\u2500\u2500 files \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 system \u2502 \u251c\u2500\u2500 box \u2502 \u2502 \u2514\u2500\u2500 PersonListBoxController.class.php \u2502 \u2514\u2500\u2500 condition \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 PersonFirstNameTextPropertyCondition.class.php \u2502 \u2514\u2500\u2500 PersonLastNameTextPropertyCondition.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 objectTypeDefinition.xml \u2514\u2500\u2500 templates \u2514\u2500\u2500 boxPersonList.tpl For all changes, please refer to the source code on GitHub .","title":"Package Structure"},{"location":"tutorial/series/part_4/#box-controller","text":"In addition to static boxes with fixed contents, administrators are able to create dynamic boxes with contents from the database. In our case here, we want administrators to be able to create boxes listing people. To do so, we first have to register a new object type for this person list box controller for the object type definition com.woltlab.wcf.boxController : 1 2 3 4 5 com.woltlab.wcf.personList com.woltlab.wcf.boxController wcf\\system\\box\\PersonListBoxController The com.woltlab.wcf.boxController object type definition requires the provided class to implement wcf\\system\\box\\IBoxController : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 * @package WoltLabSuite\\Core\\System\\Box */ class PersonListBoxController extends AbstractDatabaseObjectListBoxController { /** * @inheritDoc */ protected $conditionDefinition = 'com.woltlab.wcf.box.personList.condition' ; /** * @inheritDoc */ public $defaultLimit = 5 ; /** * @inheritDoc */ protected $sortFieldLanguageItemPrefix = 'wcf.person' ; /** * @inheritDoc */ protected static $supportedPositions = [ 'sidebarLeft' , 'sidebarRight' , ]; /** * @inheritDoc */ public $validSortFields = [ 'firstName' , 'lastName' , 'comments' , ]; /** * @inheritDoc */ public function getObjectList () { return new PersonList (); } /** * @inheritDoc */ protected function getTemplate () { return WCF :: getTPL () -> fetch ( 'boxPersonList' , 'wcf' , [ 'boxPersonList' => $this -> objectList , 'boxSortField' => $this -> sortField , 'boxPosition' => $this -> box -> position , ], true ); } } By extending AbstractDatabaseObjectListBoxController , we only have to provide minimal data ourself and rely mostly on the default implementation provided by AbstractDatabaseObjectListBoxController : As we will support conditions for the listed people, we have to set the relevant condition definition via $conditionDefinition . AbstractDatabaseObjectListBoxController already supports restricting the number of listed objects. To do so, you only have to specify the default number of listed objects via $defaultLimit . AbstractDatabaseObjectListBoxController also supports setting the sort order of the listed objects. You have to provide the supported sort fields via $validSortFields and specify the prefix used for the language items of the sort fields via $sortFieldLanguageItemPrefix so that for every $validSortField in $validSortFields , the language item {$sortFieldLanguageItemPrefix}.{$validSortField} must exist. The box system supports different positions . Each box controller specifies the positions it supports via $supportedPositions . To keep the implementation simple here as different positions might require different output in the template, we restrict ourselves to sidebars. getObjectList() returns an instance of DatabaseObjectList that is used to read the listed objects. getObjectList() itself must not call readObjects() , as AbstractDatabaseObjectListBoxController takes care of calling the method after adding the conditions and setting the sort order. getTemplate() returns the contents of the box relying on the boxPersonList template here: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
      { foreach from = $boxPersonList item = boxPerson }
    • { anchor object = $boxPerson }

      { capture assign = '__boxPersonDescription' }{ lang __optional = true } wcf.person.boxList.description. { $boxSortField }{ /lang }{ /capture } { if $__boxPersonDescription } { @ $__boxPersonDescription } { /if }
    • { /foreach }
    The template relies on a .sidebarItemList element, which is generally used for sidebar listings. (If different box positions were supported, we either have to generate different output by considering the value of $boxPosition in the template or by using different templates in getTemplate() .) One specific piece of code is the $__boxPersonDescription variable, which supports an optional description below the person's name relying on the optional language item wcf.person.boxList.description.{$boxSortField} . We only add one such language item when sorting the people by comments: In such a case, the number of comments will be shown. (When sorting by first and last name, there are no additional useful information that could be shown here, though the plugin from part 2 adding support for birthdays might also show the birthday when sorting by first or last name.) Lastly, we also provide the language item wcf.acp.box.boxController.com.woltlab.wcf.personList , which is used in the list of available box controllers.","title":"Box Controller"},{"location":"tutorial/series/part_4/#conditions","text":"The condition system can be used to generally filter a list of objects. In our case, the box system supports conditions to filter the objects shown in a specific box. Admittedly, our current person implementation only contains minimal data so that filtering might not make the most sense here but it will still show how to use the condition system for boxes. We will support filtering the people by their first and last name so that, for example, a box can be created listing all people with a specific first name. The first step for condition support is to register a object type definition for the relevant conditions requiring the IObjectListCondition interface: 1 2 3 4 5 6 7 8 9 com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\IObjectListCondition Next, we register the specific conditions for filtering by the first and last name using this object type condition: 1 2 3 4 5 6 7 8 9 10 com.woltlab.wcf.people.firstName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonFirstNameTextPropertyCondition com.woltlab.wcf.people.lastName com.woltlab.wcf.box.personList.condition wcf\\system\\condition\\person\\PersonLastNameTextPropertyCondition 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: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 * @package WoltLabSuite\\Core\\System\\Condition */ class PersonFirstNameTextPropertyCondition extends AbstractObjectTextPropertyCondition { /** * @inheritDoc */ protected $className = Person :: class ; /** * @inheritDoc */ protected $description = 'wcf.person.condition.firstName.description' ; /** * @inheritDoc */ protected $fieldName = 'personFirstName' ; /** * @inheritDoc */ protected $label = 'wcf.person.firstName' ; /** * @inheritDoc */ protected $propertyName = 'firstName' ; /** * @inheritDoc */ protected $supportsMultipleValues = true ; } $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. 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. $description (optional), $fieldName , and $label are used in the output of the form field. (The implementation here is specific for AbstractObjectTextPropertyCondition . The wcf\\system\\condition namespace also contains several other default condition implementations.)","title":"Conditions"},{"location":"tutorial/series/part_5/","text":"Part 5: Person Information # This part of our tutorial series lays the foundation for future parts in which we will be using additional APIs, which we have not used in this series yet. To make use of those APIs, we need content generated by users in the frontend. Package Functionality # In addition to the existing functions from part 4 , the package will provide the following functionality after this part of the tutorial: Users are able to add information on the people in the frontend. Users are able to edit and delete the pieces of information they added. Moderators are able to edit and delete all pieces of information. Used Components # In addition to the components used in previous parts, we will use the form builder API to create forms shown in dialogs instead of dedicated pages and we will, for the first time, add TypeScript code . Package Structure # The package will have the following file structure excluding unchanged files from previous parts: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u251c\u2500\u2500 js \u2502 \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2502 \u2514\u2500\u2500 Core \u2502 \u2502 \u2514\u2500\u2500 Controller \u2502 \u2502 \u2514\u2500\u2500 Person.js \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 data \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2514\u2500\u2500 information \u2502 \u251c\u2500\u2500 PersonInformation.class.php \u2502 \u251c\u2500\u2500 PersonInformationAction.class.php \u2502 \u251c\u2500\u2500 PersonInformationEditor.class.php \u2502 \u2514\u2500\u2500 PersonInformationList.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 ts \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2514\u2500\u2500 Core \u2502 \u2514\u2500\u2500 Controller \u2502 \u2514\u2500\u2500 Person.ts \u2514\u2500\u2500 userGroupOption.xml For all changes, please refer to the source code on GitHub . Miscellaneous # Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on: Several new user group options and the relevant language items have been added related to creating, editing, and deleting information: mod.person.canEditInformation and mod.person.canDeleteInformation are moderative permissions to edit and delete any piece of information, regardless of who created it. user.person.canAddInformation is the permission for users to add new pieces of information. user.person.canEditInformation and user.person.canDeleteInformation are the user permissions to edit and the piece of information they created. The actual information text will be entered via a WYSIWYG editor, which requires an object type of the definition com.woltlab.wcf.message : com.woltlab.wcf.people.information . personList.tpl has been adjusted to show the number of pieces of information in the person statistics section. We have not updated the person list box to also support sorting by the number of pieces of information added for each person. Person Information Model # The PHP file with the database layout has been updated as follows: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 columns ([ ObjectIdDatabaseTableColumn :: create ( 'personID' ), NotNullVarchar255DatabaseTableColumn :: create ( 'firstName' ), NotNullVarchar255DatabaseTableColumn :: create ( 'lastName' ), NotNullInt10DatabaseTableColumn :: create ( 'informationCount' ) -> defaultValue ( 0 ), SmallintDatabaseTableColumn :: create ( 'comments' ) -> length ( 5 ) -> notNull () -> defaultValue ( 0 ), DefaultTrueBooleanDatabaseTableColumn :: create ( 'enableComments' ), ]), DatabaseTable :: create ( 'wcf1_person_information' ) -> columns ([ ObjectIdDatabaseTableColumn :: create ( 'informationID' ), NotNullInt10DatabaseTableColumn :: create ( 'personID' ), TextDatabaseTableColumn :: create ( 'information' ), IntDatabaseTableColumn :: create ( 'userID' ) -> length ( 10 ), NotNullVarchar255DatabaseTableColumn :: create ( 'username' ), VarcharDatabaseTableColumn :: create ( 'ipAddress' ) -> length ( 39 ) -> notNull ( true ) -> defaultValue ( '' ), NotNullInt10DatabaseTableColumn :: create ( 'time' ), ]) -> foreignKeys ([ DatabaseTableForeignKey :: create () -> columns ([ 'personID' ]) -> referencedTable ( 'wcf1_person' ) -> referencedColumns ([ 'personID' ]) -> onDelete ( 'CASCADE' ), DatabaseTableForeignKey :: create () -> columns ([ 'userID' ]) -> referencedTable ( 'wcf1_user' ) -> referencedColumns ([ 'userID' ]) -> onDelete ( 'SET NULL' ), ]), ]; 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 . 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @property-read int $informationID unique id of the information * @property-read int $personID id of the person the information belongs to * @property-read string $information information text * @property-read int|null $userID id of the user who added the information or `null` if the user no longer exists * @property-read string $username name of the user who added the information * @property-read int $time timestamp at which the information was created */ class PersonInformation extends DatabaseObject { /** * Returns `true` if the active user can delete this piece of information and `false` otherwise. */ public function canDelete () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canDeleteInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canDeleteInformation' ); } /** * Returns `true` if the active user can edit this piece of information and `false` otherwise. */ public function canEdit () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canEditInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canEditInformation' ); } /** * Returns the formatted information. */ public function getFormattedInformation () : string { $processor = new HtmlOutputProcessor (); $processor -> process ( $this -> information , 'com.woltlab.wcf.people.information' , $this -> informationID ); return $processor -> getHtml (); } /** * Returns the person the information belongs to. */ public function getPerson () : Person { return PersonRuntimeCache :: getInstance () -> getObject ( $this -> personID ); } /** * Returns the user profile of the user who added the information. */ public function getUserProfile () : UserProfile { if ( $this -> userID ) { return UserProfileRuntimeCache :: getInstance () -> getObject ( $this -> userID ); } else { return UserProfile :: getGuestUserProfile ( $this -> username ); } } } 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. There also two getter methods for the person, the piece of information belongs to ( getPerson() ), and for the user profile of the user who created the information ( getUserProfile() ). In both cases, we use runtime caches, though in getUserProfile() , we also have to consider the case of the user who created the information being deleted, i.e. userID being null . For such a case, we also save the name of the user who created the information in username , so that we can return a guest user profile object in this case. The most interesting method is getFormattedInformation() , which returns the HTML code of the information text meant for output. To generate such an output, HtmlOutputProcessor::process() is used and here is where we first use the associated message object type com.woltlab.wcf.people.information mentioned before . 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: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 * @package WoltLabSuite\\Core\\Data\\PersonInformation * * @method PersonInformation current() * @method PersonInformation[] getObjects() * @method PersonInformation|null search($objectID) * @property PersonInformation[] $objects */ class PersonInformationList extends DatabaseObjectList { public function readObjects () { parent :: readObjects (); UserProfileRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_filter ( \\array_column ( $this -> objects , 'userID' )))); PersonRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_column ( $this -> objects , 'personID' ))); } } Listing and Deleting Person Information # The person.tpl template has been updated to include a block for listing the information at the beginning: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 { capture assign = 'pageTitle' }{ $person } - { lang } wcf.person.list { /lang }{ /capture } { capture assign = 'contentTitle' }{ $person }{ /capture } { include file = 'header' } { if $person -> informationCount || $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }

    { lang } wcf.person.information.list { /lang } { if $person -> informationCount } { # $person -> informationCount } { /if }

      { if $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }
    • { /if } { foreach from = $person -> getInformation () item = $information }
    • getObjectID () } \">
      getUserProfileHandler ()-> isIgnoredUser ( $information -> userID ) } ignoredUserContent { /if } \"> { user object = $information -> getUserProfile () type = 'avatar48' ariaHidden = 'true' tabindex = '-1' }

      { if $information -> userID } { user object = $information -> getUserProfile () } { else } { $information -> username } { /if } { @ $information -> time | time }

      getObjectID () } \"> { @ $information -> getFormattedInformation () }
    • { /foreach }
    { /if } { if $person -> enableComments } { if $commentList | count || $commentCanAdd }

    { lang } wcf.person.comments { /lang } { if $person -> comments } { # $person -> comments } { /if }

    { include file = '__commentJavaScript' commentContainerID = 'personCommentList' }
      personID } \" { * * } data-object-type-id=\" { @ $commentObjectTypeID } \" { * * } data-comments=\" { if $person -> comments }{ @ $commentList -> countObjects () }{ else } 0 { /if } \" { * * } data-last-comment-time=\" { @ $lastCommentTime } \" { * * } > { include file = 'commentListAddComment' wysiwygSelector = 'personCommentListAddComment' } { include file = 'commentList' }
    { /if } { /if }
    { hascontent } { /hascontent }
    { include file = 'footer' } To keep things simple here, we reuse the structure and CSS classes used for comments. Additionally, we always list all pieces of information. If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript. First, we note the jsObjectActionContainer class in combination with the data-object-action-class-name attribute, which are needed for the delete button for each piece of information, as explained here . In PersonInformationAction , we have overridden the default implementations of validateDelete() and delete() which are called after clicking on a delete button. In validateDelete() , we call PersonInformation::canDelete() on all pieces of information to be deleted for proper permission validation, and in delete() , we update the informationCount values of the people the deleted pieces of information belong to (see below). The button to add a new piece of information, #personInformationAddButton , and the buttons to edit existing pieces of information, .jsEditInformation , are controlled with JavaScript code initialized at the very end of the template. Lastly, in create() we provide default values for the time , userID , username , and ipAddress for cases like here when creating a new piece of information, where do not explicitly provide this data. Additionally, we extract the information text from the information_htmlInputProcessor parameter provided by the associated WYSIWYG form field and update the number of pieces of information created for the relevant person. Creating and Editing Person Information # To create new pieces of information or editing existing ones, we do not add new form controllers but instead use dialogs generated by the form builder API so that the user does not have to leave the person page. When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 /** * Provides the JavaScript code for the person page. * * @author Matthias Schmidt * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Controller/Person */ import FormBuilderDialog from \"WoltLabSuite/Core/Form/Builder/Dialog\" ; import * as Language from \"WoltLabSuite/Core/Language\" ; import * as UiNotification from \"WoltLabSuite/Core/Ui/Notification\" ; let addDialog : FormBuilderDialog ; const editDialogs = new Map < string , FormBuilderDialog > (); interface EditReturnValues { formattedInformation : string ; informationID : number ; } interface Options { canAddInformation : true ; } /** * Opens the edit dialog after clicking on the edit button for a piece of information. */ function editInformation ( event : Event ) : void { event . preventDefault (); const currentTarget = event . currentTarget as HTMLElement ; const information = currentTarget . closest ( \".jsObjectActionObject\" ) as HTMLElement ; const informationId = information . dataset . objectId ! ; if ( ! editDialogs . has ( informationId )) { editDialogs . set ( informationId , new FormBuilderDialog ( `personInformationEditDialog ${ informationId } ` , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getEditDialog\" , { actionParameters : { informationID : informationId , }, dialog : { title : Language.get ( \"wcf.person.information.edit\" ), }, submitActionName : \"submitEditDialog\" , successCallback ( returnValues : EditReturnValues ) { document . getElementById ( `personInformation ${ returnValues . informationID } ` ) ! . innerHTML = returnValues . formattedInformation ; UiNotification . show ( Language . get ( \"wcf.person.information.edit.success\" )); }, }, ), ); } editDialogs . get ( informationId ) ! . open (); } /** * Initializes the JavaScript code for the person page. */ export function init ( personId : number , options : Options ) : void { if ( options . canAddInformation ) { // Initialize the dialog to add new information. addDialog = new FormBuilderDialog ( \"personInformationAddDialog\" , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getAddDialog\" , { actionParameters : { personID : personId , }, dialog : { title : Language.get ( \"wcf.person.information.add\" ), }, submitActionName : \"submitAddDialog\" , successCallback () { UiNotification . show ( Language . get ( \"wcf.person.information.add.success\" ), () => window . location . reload ()); }, }, ); document . getElementById ( \"personInformationAddButton\" ) ! . addEventListener ( \"click\" , ( event ) => { event . preventDefault (); addDialog . open (); }); } document . querySelectorAll ( \".jsEditInformation\" ) . forEach (( el ) => el . addEventListener ( \"click\" , ( ev ) => editInformation ( ev ))); } We use the WoltLabSuite/Core/Form/Builder/Dialog module , 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. Explanation of the initialization arguments for WoltLabSuite/Core/Form/Builder/Dialog used here: The first argument is the id of the dialog used to identify it. The second argument is the PHP class name which provides the contents of the dialog's form and handles the data after the form is submitted. The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form. The fourth argument contains additional options: actionParameters are additional parameters send during each AJAX request. Here, we either pass the id of the person for who a new piece of information is added or the id of the edited piece of information. dialog contains the options for the dialog, see the DialogOptions interface. Here, we only provide the title of the dialog. submitActionName is the name of the method in the referenced PHP class that is called with the form data after submitting the form. successCallback is called after the submit AJAX request was successful. After adding a new piece of information, we reload the page, and after editing an existing piece of information, we update the existing information text with the updated text. (Dynamically inserting a newly added piece of information instead of reloading the page would also be possible, of course, but for this tutorial series, we kept things simple.) Next, we focus on PersonInformationAction , which actually provides the contents of these dialogs and creates and edits the information: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @method PersonInformationEditor[] getObjects() * @method PersonInformationEditor getSingleObject() */ class PersonInformationAction extends AbstractDatabaseObjectAction { /** * @var DialogFormDocument */ public $dialog ; /** * @var PersonInformation */ public $information ; /** * @return PersonInformation */ public function create () { if ( ! isset ( $this -> parameters [ 'data' ][ 'time' ])) { $this -> parameters [ 'data' ][ 'time' ] = TIME_NOW ; } if ( ! isset ( $this -> parameters [ 'data' ][ 'userID' ])) { $this -> parameters [ 'data' ][ 'userID' ] = WCF :: getUser () -> userID ; $this -> parameters [ 'data' ][ 'username' ] = WCF :: getUser () -> username ; } if ( LOG_IP_ADDRESS ) { if ( ! isset ( $this -> parameters [ 'data' ][ 'ipAddress' ])) { $this -> parameters [ 'data' ][ 'ipAddress' ] = WCF :: getSession () -> ipAddress ; } } else { unset ( $this -> parameters [ 'data' ][ 'ipAddress' ]); } if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } /** @var PersonInformation $information */ $information = parent :: create (); ( new PersonAction ([ $information -> personID ], 'update' , [ 'counters' => [ 'informationCount' => 1 , ], ])) -> executeAction (); return $information ; } /** * @inheritDoc */ public function update () { if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } parent :: update (); } /** * @inheritDoc */ public function validateDelete () { if ( empty ( $this -> objects )) { $this -> readObjects (); if ( empty ( $this -> objects )) { throw new UserInputException ( 'objectIDs' ); } } foreach ( $this -> getObjects () as $informationEditor ) { if ( ! $informationEditor -> canDelete ()) { throw new PermissionDeniedException (); } } } /** * @inheritDoc */ public function delete () { $deleteCount = parent :: delete (); if ( ! $deleteCount ) { return $deleteCount ; } $counterUpdates = []; foreach ( $this -> getObjects () as $informationEditor ) { if ( ! isset ( $counterUpdates [ $informationEditor -> personID ])) { $counterUpdates [ $informationEditor -> personID ] = 0 ; } $counterUpdates [ $informationEditor -> personID ] -- ; } WCF :: getDB () -> beginTransaction (); foreach ( $counterUpdates as $personID => $counterUpdate ) { ( new PersonEditor ( PersonRuntimeCache :: getInstance () -> getObject ( $personID ))) -> updateCounters ([ 'informationCount' => $counterUpdate , ]); } WCF :: getDB () -> commitTransaction (); return $deleteCount ; } /** * Validates the `getAddDialog` action. */ public function validateGetAddDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'personID' ); if ( PersonRuntimeCache :: getInstance () -> getObject ( $this -> parameters [ 'personID' ]) === null ) { throw new UserInputException ( 'personID' ); } } /** * Returns the data to show the dialog to add a new piece of information on a person. * * @return string[] */ public function getAddDialog () : array { $this -> buildDialog (); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitAddDialog` action. */ public function validateSubmitAddDialog () : void { $this -> validateGetAddDialog (); $this -> buildDialog (); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Creates a new piece of information on a person after submitting the dialog. * * @return string[] */ public function submitAddDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([], 'create' , \\array_merge ( $this -> dialog -> getData (), [ 'data' => [ 'personID' => $this -> parameters [ 'personID' ], ], ]))) -> executeAction (); return []; } /** * Validates the `getEditDialog` action. */ public function validateGetEditDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'informationID' ); $this -> information = new PersonInformation ( $this -> parameters [ 'informationID' ]); if ( ! $this -> information -> getObjectID ()) { throw new UserInputException ( 'informationID' ); } if ( ! $this -> information -> canEdit ()) { throw new IllegalLinkException (); } } /** * Returns the data to show the dialog to edit a piece of information on a person. * * @return string[] */ public function getEditDialog () : array { $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information ); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitEditDialog` action. */ public function validateSubmitEditDialog () : void { $this -> validateGetEditDialog (); $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information , false ); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Updates a piece of information on a person after submitting the edit dialog. * * @return string[] */ public function submitEditDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([ $this -> information ], 'update' , $this -> dialog -> getData ())) -> executeAction (); // Reload the information with the updated data. $information = new PersonInformation ( $this -> information -> getObjectID ()); return [ 'formattedInformation' => $information -> getFormattedInformation (), 'informationID' => $this -> information -> getObjectID (), ]; } /** * Builds the dialog to create or edit person information. */ protected function buildDialog () : void { if ( $this -> dialog !== null ) { return ; } $this -> dialog = DialogFormDocument :: create ( 'personInformationAddDialog' ) -> appendChild ( WysiwygFormContainer :: create ( 'information' ) -> messageObjectType ( 'com.woltlab.wcf.people.information' ) -> required () ); EventHandler :: getInstance () -> fireAction ( $this , 'buildDialog' ); $this -> dialog -> build (); } } 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. As the forms for adding and editing pieces of information have the same structure, this form is created in buildDialog() using a DialogFormDocument object, which is intended for forms in dialogs. We fire an event in buildDialog() so that plugins are able to easily extend the dialog with additional data. validateGetAddDialog() checks if the user has the permission to create new pieces of information and if a valid id for the person, the information will belong to, is given. The method configured in the WoltLabSuite/Core/Form/Builder/Dialog object returning the dialog is expected to return two values: the id of the form ( formId ) and the contents of form shown in the dialog ( dialog ). This data is returned by getAddDialog using the dialog build previously by buildDialog() . After the form is submitted, validateSubmitAddDialog() has to do the same basic validation as validateGetAddDialog() so that validateGetAddDialog() is simply called. Additionally, the form data is read and validated. In submitAddDialog() , we first check if there have been any validation errors: If any error occured during validation, we return the same data as in getAddDialog() so that the dialog is shown again with the erroneous fields marked as such. Otherwise, if the validation succeeded, the form data is used to create the new piece of information. In addition to the form data, we manually add the id of the person to whom the information belongs to. Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog. As we will simply be reloading the page, no such data is returned. An alternative to reloading to the page would be dynamically inserting the new piece of information in the list so that we would have to return the rendered list item for the new piece of information. The process for getting and submitting the dialog to edit existing pieces of information is similar to the process for adding new pieces of information. Instead of the id of the person, however, we now pass the id of the edited piece of information and in submitEditDialog() , we update the edited information instead of creating a new one like in submitAddDialog() . After editing a piece of information, we do not reload the page but dynamically update the text of the information in the TypeScript code so that we return the updated rendered information text and id of the edited pieced of information in submitAddDialog() . Username and IP Address Event Listeners # As we store the name of the user who create a new piece of information and store their IP address, we have to add event listeners to properly handle the following scenarios: If the user is renamed, the value of username stored with the person information has to be updated, which can be achieved by a simple event listener that only has to specify the name of relevant database table if AbstractUserActionRenameListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserActionRenameListener extends AbstractUserActionRenameListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 2. If users are merged, all pieces of information need to be assigned to the target user of the merging. Again, we only have to specify the name of relevant database table if AbstractUserMergeListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserMergeListener extends AbstractUserMergeListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 3. If the option to prune stored ip addresses after a certain period of time is enabled, we also have to prune them in the person information database table. Here we also only have to specify the name of the relevant database table and provide the mapping from the ipAddress column to the time column: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonPruneIpAddressesCronjobListener extends AbstractEventListener { protected function onExecute ( PruneIpAddressesCronjob $cronjob ) : void { $cronjob -> columns [ 'wcf' . WCF_N . '_person_information' ][ 'ipAddress' ] = 'time' ; } } 4. The ip addresses in the person information database table also have to be considered for the user data export which can also be done with minimal effort by providing the name of the relevant database table: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserExportGdprListener extends AbstractEventListener { protected function onExport ( UserExportGdprAction $action ) : void { $action -> ipAddresses [ 'com.woltlab.wcf.people' ] = [ 'wcf' . WCF_N . '_person_information' ]; } } Lastly, we present the updated eventListener.xml file with new entries for all of these event listeners: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 wcf\\data\\user\\UserAction rename wcf\\system\\event\\listener\\PersonUserActionRenameListener all wcf\\acp\\form\\UserMergeForm save wcf\\system\\event\\listener\\PersonUserMergeListener admin wcf\\system\\cronjob\\PruneIpAddressesCronjob execute wcf\\system\\event\\listener\\PersonPruneIpAddressesCronjobListener all wcf\\acp\\action\\UserExportGdprAction export wcf\\system\\event\\listener\\PersonUserExportGdprListener admin ","title":"Part 5"},{"location":"tutorial/series/part_5/#part-5-person-information","text":"This part of our tutorial series lays the foundation for future parts in which we will be using additional APIs, which we have not used in this series yet. To make use of those APIs, we need content generated by users in the frontend.","title":"Part 5: Person Information"},{"location":"tutorial/series/part_5/#package-functionality","text":"In addition to the existing functions from part 4 , the package will provide the following functionality after this part of the tutorial: Users are able to add information on the people in the frontend. Users are able to edit and delete the pieces of information they added. Moderators are able to edit and delete all pieces of information.","title":"Package Functionality"},{"location":"tutorial/series/part_5/#used-components","text":"In addition to the components used in previous parts, we will use the form builder API to create forms shown in dialogs instead of dedicated pages and we will, for the first time, add TypeScript code .","title":"Used Components"},{"location":"tutorial/series/part_5/#package-structure","text":"The package will have the following file structure excluding unchanged files from previous parts: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 \u251c\u2500\u2500 files \u2502 \u251c\u2500\u2500 acp \u2502 \u2502 \u2514\u2500\u2500 database \u2502 \u2502 \u2514\u2500\u2500 install_com.woltlab.wcf.people.php \u2502 \u251c\u2500\u2500 js \u2502 \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2502 \u2514\u2500\u2500 Core \u2502 \u2502 \u2514\u2500\u2500 Controller \u2502 \u2502 \u2514\u2500\u2500 Person.js \u2502 \u2514\u2500\u2500 lib \u2502 \u2514\u2500\u2500 data \u2502 \u2514\u2500\u2500 person \u2502 \u251c\u2500\u2500 Person.class.php \u2502 \u2514\u2500\u2500 information \u2502 \u251c\u2500\u2500 PersonInformation.class.php \u2502 \u251c\u2500\u2500 PersonInformationAction.class.php \u2502 \u251c\u2500\u2500 PersonInformationEditor.class.php \u2502 \u2514\u2500\u2500 PersonInformationList.class.php \u251c\u2500\u2500 language \u2502 \u251c\u2500\u2500 de.xml \u2502 \u2514\u2500\u2500 en.xml \u251c\u2500\u2500 objectType.xml \u251c\u2500\u2500 templates \u2502 \u251c\u2500\u2500 person.tpl \u2502 \u2514\u2500\u2500 personList.tpl \u251c\u2500\u2500 ts \u2502 \u2514\u2500\u2500 WoltLabSuite \u2502 \u2514\u2500\u2500 Core \u2502 \u2514\u2500\u2500 Controller \u2502 \u2514\u2500\u2500 Person.ts \u2514\u2500\u2500 userGroupOption.xml For all changes, please refer to the source code on GitHub .","title":"Package Structure"},{"location":"tutorial/series/part_5/#miscellaneous","text":"Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on: Several new user group options and the relevant language items have been added related to creating, editing, and deleting information: mod.person.canEditInformation and mod.person.canDeleteInformation are moderative permissions to edit and delete any piece of information, regardless of who created it. user.person.canAddInformation is the permission for users to add new pieces of information. user.person.canEditInformation and user.person.canDeleteInformation are the user permissions to edit and the piece of information they created. The actual information text will be entered via a WYSIWYG editor, which requires an object type of the definition com.woltlab.wcf.message : com.woltlab.wcf.people.information . personList.tpl has been adjusted to show the number of pieces of information in the person statistics section. We have not updated the person list box to also support sorting by the number of pieces of information added for each person.","title":"Miscellaneous"},{"location":"tutorial/series/part_5/#person-information-model","text":"The PHP file with the database layout has been updated as follows: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 columns ([ ObjectIdDatabaseTableColumn :: create ( 'personID' ), NotNullVarchar255DatabaseTableColumn :: create ( 'firstName' ), NotNullVarchar255DatabaseTableColumn :: create ( 'lastName' ), NotNullInt10DatabaseTableColumn :: create ( 'informationCount' ) -> defaultValue ( 0 ), SmallintDatabaseTableColumn :: create ( 'comments' ) -> length ( 5 ) -> notNull () -> defaultValue ( 0 ), DefaultTrueBooleanDatabaseTableColumn :: create ( 'enableComments' ), ]), DatabaseTable :: create ( 'wcf1_person_information' ) -> columns ([ ObjectIdDatabaseTableColumn :: create ( 'informationID' ), NotNullInt10DatabaseTableColumn :: create ( 'personID' ), TextDatabaseTableColumn :: create ( 'information' ), IntDatabaseTableColumn :: create ( 'userID' ) -> length ( 10 ), NotNullVarchar255DatabaseTableColumn :: create ( 'username' ), VarcharDatabaseTableColumn :: create ( 'ipAddress' ) -> length ( 39 ) -> notNull ( true ) -> defaultValue ( '' ), NotNullInt10DatabaseTableColumn :: create ( 'time' ), ]) -> foreignKeys ([ DatabaseTableForeignKey :: create () -> columns ([ 'personID' ]) -> referencedTable ( 'wcf1_person' ) -> referencedColumns ([ 'personID' ]) -> onDelete ( 'CASCADE' ), DatabaseTableForeignKey :: create () -> columns ([ 'userID' ]) -> referencedTable ( 'wcf1_user' ) -> referencedColumns ([ 'userID' ]) -> onDelete ( 'SET NULL' ), ]), ]; 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 . 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @property-read int $informationID unique id of the information * @property-read int $personID id of the person the information belongs to * @property-read string $information information text * @property-read int|null $userID id of the user who added the information or `null` if the user no longer exists * @property-read string $username name of the user who added the information * @property-read int $time timestamp at which the information was created */ class PersonInformation extends DatabaseObject { /** * Returns `true` if the active user can delete this piece of information and `false` otherwise. */ public function canDelete () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canDeleteInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canDeleteInformation' ); } /** * Returns `true` if the active user can edit this piece of information and `false` otherwise. */ public function canEdit () : bool { if ( WCF :: getUser () -> userID && WCF :: getUser () -> userID == $this -> userID && WCF :: getSession () -> getPermission ( 'user.person.canEditInformation' ) ) { return true ; } return WCF :: getSession () -> getPermission ( 'mod.person.canEditInformation' ); } /** * Returns the formatted information. */ public function getFormattedInformation () : string { $processor = new HtmlOutputProcessor (); $processor -> process ( $this -> information , 'com.woltlab.wcf.people.information' , $this -> informationID ); return $processor -> getHtml (); } /** * Returns the person the information belongs to. */ public function getPerson () : Person { return PersonRuntimeCache :: getInstance () -> getObject ( $this -> personID ); } /** * Returns the user profile of the user who added the information. */ public function getUserProfile () : UserProfile { if ( $this -> userID ) { return UserProfileRuntimeCache :: getInstance () -> getObject ( $this -> userID ); } else { return UserProfile :: getGuestUserProfile ( $this -> username ); } } } 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. There also two getter methods for the person, the piece of information belongs to ( getPerson() ), and for the user profile of the user who created the information ( getUserProfile() ). In both cases, we use runtime caches, though in getUserProfile() , we also have to consider the case of the user who created the information being deleted, i.e. userID being null . For such a case, we also save the name of the user who created the information in username , so that we can return a guest user profile object in this case. The most interesting method is getFormattedInformation() , which returns the HTML code of the information text meant for output. To generate such an output, HtmlOutputProcessor::process() is used and here is where we first use the associated message object type com.woltlab.wcf.people.information mentioned before . 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: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 * @package WoltLabSuite\\Core\\Data\\PersonInformation * * @method PersonInformation current() * @method PersonInformation[] getObjects() * @method PersonInformation|null search($objectID) * @property PersonInformation[] $objects */ class PersonInformationList extends DatabaseObjectList { public function readObjects () { parent :: readObjects (); UserProfileRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_filter ( \\array_column ( $this -> objects , 'userID' )))); PersonRuntimeCache :: getInstance () -> cacheObjectIDs ( \\array_unique ( \\array_column ( $this -> objects , 'personID' ))); } }","title":"Person Information Model"},{"location":"tutorial/series/part_5/#listing-and-deleting-person-information","text":"The person.tpl template has been updated to include a block for listing the information at the beginning: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 { capture assign = 'pageTitle' }{ $person } - { lang } wcf.person.list { /lang }{ /capture } { capture assign = 'contentTitle' }{ $person }{ /capture } { include file = 'header' } { if $person -> informationCount || $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }

    { lang } wcf.person.information.list { /lang } { if $person -> informationCount } { # $person -> informationCount } { /if }

      { if $__wcf -> session -> getPermission ( 'user.person.canAddInformation' ) }
    • { /if } { foreach from = $person -> getInformation () item = $information }
    • getObjectID () } \">
      getUserProfileHandler ()-> isIgnoredUser ( $information -> userID ) } ignoredUserContent { /if } \"> { user object = $information -> getUserProfile () type = 'avatar48' ariaHidden = 'true' tabindex = '-1' }

      { if $information -> userID } { user object = $information -> getUserProfile () } { else } { $information -> username } { /if } { @ $information -> time | time }

      getObjectID () } \"> { @ $information -> getFormattedInformation () }
    • { /foreach }
    { /if } { if $person -> enableComments } { if $commentList | count || $commentCanAdd }

    { lang } wcf.person.comments { /lang } { if $person -> comments } { # $person -> comments } { /if }

    { include file = '__commentJavaScript' commentContainerID = 'personCommentList' }
      personID } \" { * * } data-object-type-id=\" { @ $commentObjectTypeID } \" { * * } data-comments=\" { if $person -> comments }{ @ $commentList -> countObjects () }{ else } 0 { /if } \" { * * } data-last-comment-time=\" { @ $lastCommentTime } \" { * * } > { include file = 'commentListAddComment' wysiwygSelector = 'personCommentListAddComment' } { include file = 'commentList' }
    { /if } { /if }
    { hascontent } { /hascontent }
    { include file = 'footer' } To keep things simple here, we reuse the structure and CSS classes used for comments. Additionally, we always list all pieces of information. If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript. First, we note the jsObjectActionContainer class in combination with the data-object-action-class-name attribute, which are needed for the delete button for each piece of information, as explained here . In PersonInformationAction , we have overridden the default implementations of validateDelete() and delete() which are called after clicking on a delete button. In validateDelete() , we call PersonInformation::canDelete() on all pieces of information to be deleted for proper permission validation, and in delete() , we update the informationCount values of the people the deleted pieces of information belong to (see below). The button to add a new piece of information, #personInformationAddButton , and the buttons to edit existing pieces of information, .jsEditInformation , are controlled with JavaScript code initialized at the very end of the template. Lastly, in create() we provide default values for the time , userID , username , and ipAddress for cases like here when creating a new piece of information, where do not explicitly provide this data. Additionally, we extract the information text from the information_htmlInputProcessor parameter provided by the associated WYSIWYG form field and update the number of pieces of information created for the relevant person.","title":"Listing and Deleting Person Information"},{"location":"tutorial/series/part_5/#creating-and-editing-person-information","text":"To create new pieces of information or editing existing ones, we do not add new form controllers but instead use dialogs generated by the form builder API so that the user does not have to leave the person page. When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 /** * Provides the JavaScript code for the person page. * * @author Matthias Schmidt * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Controller/Person */ import FormBuilderDialog from \"WoltLabSuite/Core/Form/Builder/Dialog\" ; import * as Language from \"WoltLabSuite/Core/Language\" ; import * as UiNotification from \"WoltLabSuite/Core/Ui/Notification\" ; let addDialog : FormBuilderDialog ; const editDialogs = new Map < string , FormBuilderDialog > (); interface EditReturnValues { formattedInformation : string ; informationID : number ; } interface Options { canAddInformation : true ; } /** * Opens the edit dialog after clicking on the edit button for a piece of information. */ function editInformation ( event : Event ) : void { event . preventDefault (); const currentTarget = event . currentTarget as HTMLElement ; const information = currentTarget . closest ( \".jsObjectActionObject\" ) as HTMLElement ; const informationId = information . dataset . objectId ! ; if ( ! editDialogs . has ( informationId )) { editDialogs . set ( informationId , new FormBuilderDialog ( `personInformationEditDialog ${ informationId } ` , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getEditDialog\" , { actionParameters : { informationID : informationId , }, dialog : { title : Language.get ( \"wcf.person.information.edit\" ), }, submitActionName : \"submitEditDialog\" , successCallback ( returnValues : EditReturnValues ) { document . getElementById ( `personInformation ${ returnValues . informationID } ` ) ! . innerHTML = returnValues . formattedInformation ; UiNotification . show ( Language . get ( \"wcf.person.information.edit.success\" )); }, }, ), ); } editDialogs . get ( informationId ) ! . open (); } /** * Initializes the JavaScript code for the person page. */ export function init ( personId : number , options : Options ) : void { if ( options . canAddInformation ) { // Initialize the dialog to add new information. addDialog = new FormBuilderDialog ( \"personInformationAddDialog\" , \"wcf\\\\data\\\\person\\\\information\\\\PersonInformationAction\" , \"getAddDialog\" , { actionParameters : { personID : personId , }, dialog : { title : Language.get ( \"wcf.person.information.add\" ), }, submitActionName : \"submitAddDialog\" , successCallback () { UiNotification . show ( Language . get ( \"wcf.person.information.add.success\" ), () => window . location . reload ()); }, }, ); document . getElementById ( \"personInformationAddButton\" ) ! . addEventListener ( \"click\" , ( event ) => { event . preventDefault (); addDialog . open (); }); } document . querySelectorAll ( \".jsEditInformation\" ) . forEach (( el ) => el . addEventListener ( \"click\" , ( ev ) => editInformation ( ev ))); } We use the WoltLabSuite/Core/Form/Builder/Dialog module , 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. Explanation of the initialization arguments for WoltLabSuite/Core/Form/Builder/Dialog used here: The first argument is the id of the dialog used to identify it. The second argument is the PHP class name which provides the contents of the dialog's form and handles the data after the form is submitted. The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form. The fourth argument contains additional options: actionParameters are additional parameters send during each AJAX request. Here, we either pass the id of the person for who a new piece of information is added or the id of the edited piece of information. dialog contains the options for the dialog, see the DialogOptions interface. Here, we only provide the title of the dialog. submitActionName is the name of the method in the referenced PHP class that is called with the form data after submitting the form. successCallback is called after the submit AJAX request was successful. After adding a new piece of information, we reload the page, and after editing an existing piece of information, we update the existing information text with the updated text. (Dynamically inserting a newly added piece of information instead of reloading the page would also be possible, of course, but for this tutorial series, we kept things simple.) Next, we focus on PersonInformationAction , which actually provides the contents of these dialogs and creates and edits the information: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 * @package WoltLabSuite\\Core\\Data\\Person\\Information * * @method PersonInformationEditor[] getObjects() * @method PersonInformationEditor getSingleObject() */ class PersonInformationAction extends AbstractDatabaseObjectAction { /** * @var DialogFormDocument */ public $dialog ; /** * @var PersonInformation */ public $information ; /** * @return PersonInformation */ public function create () { if ( ! isset ( $this -> parameters [ 'data' ][ 'time' ])) { $this -> parameters [ 'data' ][ 'time' ] = TIME_NOW ; } if ( ! isset ( $this -> parameters [ 'data' ][ 'userID' ])) { $this -> parameters [ 'data' ][ 'userID' ] = WCF :: getUser () -> userID ; $this -> parameters [ 'data' ][ 'username' ] = WCF :: getUser () -> username ; } if ( LOG_IP_ADDRESS ) { if ( ! isset ( $this -> parameters [ 'data' ][ 'ipAddress' ])) { $this -> parameters [ 'data' ][ 'ipAddress' ] = WCF :: getSession () -> ipAddress ; } } else { unset ( $this -> parameters [ 'data' ][ 'ipAddress' ]); } if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } /** @var PersonInformation $information */ $information = parent :: create (); ( new PersonAction ([ $information -> personID ], 'update' , [ 'counters' => [ 'informationCount' => 1 , ], ])) -> executeAction (); return $information ; } /** * @inheritDoc */ public function update () { if ( ! empty ( $this -> parameters [ 'information_htmlInputProcessor' ])) { /** @var HtmlInputProcessor $htmlInputProcessor */ $htmlInputProcessor = $this -> parameters [ 'information_htmlInputProcessor' ]; $this -> parameters [ 'data' ][ 'information' ] = $htmlInputProcessor -> getHtml (); } parent :: update (); } /** * @inheritDoc */ public function validateDelete () { if ( empty ( $this -> objects )) { $this -> readObjects (); if ( empty ( $this -> objects )) { throw new UserInputException ( 'objectIDs' ); } } foreach ( $this -> getObjects () as $informationEditor ) { if ( ! $informationEditor -> canDelete ()) { throw new PermissionDeniedException (); } } } /** * @inheritDoc */ public function delete () { $deleteCount = parent :: delete (); if ( ! $deleteCount ) { return $deleteCount ; } $counterUpdates = []; foreach ( $this -> getObjects () as $informationEditor ) { if ( ! isset ( $counterUpdates [ $informationEditor -> personID ])) { $counterUpdates [ $informationEditor -> personID ] = 0 ; } $counterUpdates [ $informationEditor -> personID ] -- ; } WCF :: getDB () -> beginTransaction (); foreach ( $counterUpdates as $personID => $counterUpdate ) { ( new PersonEditor ( PersonRuntimeCache :: getInstance () -> getObject ( $personID ))) -> updateCounters ([ 'informationCount' => $counterUpdate , ]); } WCF :: getDB () -> commitTransaction (); return $deleteCount ; } /** * Validates the `getAddDialog` action. */ public function validateGetAddDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'personID' ); if ( PersonRuntimeCache :: getInstance () -> getObject ( $this -> parameters [ 'personID' ]) === null ) { throw new UserInputException ( 'personID' ); } } /** * Returns the data to show the dialog to add a new piece of information on a person. * * @return string[] */ public function getAddDialog () : array { $this -> buildDialog (); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitAddDialog` action. */ public function validateSubmitAddDialog () : void { $this -> validateGetAddDialog (); $this -> buildDialog (); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Creates a new piece of information on a person after submitting the dialog. * * @return string[] */ public function submitAddDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([], 'create' , \\array_merge ( $this -> dialog -> getData (), [ 'data' => [ 'personID' => $this -> parameters [ 'personID' ], ], ]))) -> executeAction (); return []; } /** * Validates the `getEditDialog` action. */ public function validateGetEditDialog () : void { WCF :: getSession () -> checkPermissions ([ 'user.person.canAddInformation' ]); $this -> readInteger ( 'informationID' ); $this -> information = new PersonInformation ( $this -> parameters [ 'informationID' ]); if ( ! $this -> information -> getObjectID ()) { throw new UserInputException ( 'informationID' ); } if ( ! $this -> information -> canEdit ()) { throw new IllegalLinkException (); } } /** * Returns the data to show the dialog to edit a piece of information on a person. * * @return string[] */ public function getEditDialog () : array { $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information ); return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } /** * Validates the `submitEditDialog` action. */ public function validateSubmitEditDialog () : void { $this -> validateGetEditDialog (); $this -> buildDialog (); $this -> dialog -> updatedObject ( $this -> information , false ); $this -> dialog -> requestData ( $_POST [ 'parameters' ][ 'data' ] ?? []); $this -> dialog -> readValues (); $this -> dialog -> validate (); } /** * Updates a piece of information on a person after submitting the edit dialog. * * @return string[] */ public function submitEditDialog () : array { // If there are any validation errors, show the form again. if ( $this -> dialog -> hasValidationErrors ()) { return [ 'dialog' => $this -> dialog -> getHtml (), 'formId' => $this -> dialog -> getId (), ]; } ( new static ([ $this -> information ], 'update' , $this -> dialog -> getData ())) -> executeAction (); // Reload the information with the updated data. $information = new PersonInformation ( $this -> information -> getObjectID ()); return [ 'formattedInformation' => $information -> getFormattedInformation (), 'informationID' => $this -> information -> getObjectID (), ]; } /** * Builds the dialog to create or edit person information. */ protected function buildDialog () : void { if ( $this -> dialog !== null ) { return ; } $this -> dialog = DialogFormDocument :: create ( 'personInformationAddDialog' ) -> appendChild ( WysiwygFormContainer :: create ( 'information' ) -> messageObjectType ( 'com.woltlab.wcf.people.information' ) -> required () ); EventHandler :: getInstance () -> fireAction ( $this , 'buildDialog' ); $this -> dialog -> build (); } } 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. As the forms for adding and editing pieces of information have the same structure, this form is created in buildDialog() using a DialogFormDocument object, which is intended for forms in dialogs. We fire an event in buildDialog() so that plugins are able to easily extend the dialog with additional data. validateGetAddDialog() checks if the user has the permission to create new pieces of information and if a valid id for the person, the information will belong to, is given. The method configured in the WoltLabSuite/Core/Form/Builder/Dialog object returning the dialog is expected to return two values: the id of the form ( formId ) and the contents of form shown in the dialog ( dialog ). This data is returned by getAddDialog using the dialog build previously by buildDialog() . After the form is submitted, validateSubmitAddDialog() has to do the same basic validation as validateGetAddDialog() so that validateGetAddDialog() is simply called. Additionally, the form data is read and validated. In submitAddDialog() , we first check if there have been any validation errors: If any error occured during validation, we return the same data as in getAddDialog() so that the dialog is shown again with the erroneous fields marked as such. Otherwise, if the validation succeeded, the form data is used to create the new piece of information. In addition to the form data, we manually add the id of the person to whom the information belongs to. Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog. As we will simply be reloading the page, no such data is returned. An alternative to reloading to the page would be dynamically inserting the new piece of information in the list so that we would have to return the rendered list item for the new piece of information. The process for getting and submitting the dialog to edit existing pieces of information is similar to the process for adding new pieces of information. Instead of the id of the person, however, we now pass the id of the edited piece of information and in submitEditDialog() , we update the edited information instead of creating a new one like in submitAddDialog() . After editing a piece of information, we do not reload the page but dynamically update the text of the information in the TypeScript code so that we return the updated rendered information text and id of the edited pieced of information in submitAddDialog() .","title":"Creating and Editing Person Information"},{"location":"tutorial/series/part_5/#username-and-ip-address-event-listeners","text":"As we store the name of the user who create a new piece of information and store their IP address, we have to add event listeners to properly handle the following scenarios: If the user is renamed, the value of username stored with the person information has to be updated, which can be achieved by a simple event listener that only has to specify the name of relevant database table if AbstractUserActionRenameListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserActionRenameListener extends AbstractUserActionRenameListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 2. If users are merged, all pieces of information need to be assigned to the target user of the merging. Again, we only have to specify the name of relevant database table if AbstractUserMergeListener is extended: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserMergeListener extends AbstractUserMergeListener { /** * @inheritDoc */ protected $databaseTables = [ 'wcf{WCF_N}_person_information' , ]; } 3. If the option to prune stored ip addresses after a certain period of time is enabled, we also have to prune them in the person information database table. Here we also only have to specify the name of the relevant database table and provide the mapping from the ipAddress column to the time column: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonPruneIpAddressesCronjobListener extends AbstractEventListener { protected function onExecute ( PruneIpAddressesCronjob $cronjob ) : void { $cronjob -> columns [ 'wcf' . WCF_N . '_person_information' ][ 'ipAddress' ] = 'time' ; } } 4. The ip addresses in the person information database table also have to be considered for the user data export which can also be done with minimal effort by providing the name of the relevant database table: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\Event\\Listener */ class PersonUserExportGdprListener extends AbstractEventListener { protected function onExport ( UserExportGdprAction $action ) : void { $action -> ipAddresses [ 'com.woltlab.wcf.people' ] = [ 'wcf' . WCF_N . '_person_information' ]; } } Lastly, we present the updated eventListener.xml file with new entries for all of these event listeners: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 wcf\\data\\user\\UserAction rename wcf\\system\\event\\listener\\PersonUserActionRenameListener all wcf\\acp\\form\\UserMergeForm save wcf\\system\\event\\listener\\PersonUserMergeListener admin wcf\\system\\cronjob\\PruneIpAddressesCronjob execute wcf\\system\\event\\listener\\PersonPruneIpAddressesCronjobListener all wcf\\acp\\action\\UserExportGdprAction export wcf\\system\\event\\listener\\PersonUserExportGdprListener admin ","title":"Username and IP Address Event Listeners"},{"location":"view/css/","text":"CSS # SCSS and CSS # SCSS is a scripting language that features a syntax similar to CSS and compiles into native CSS at runtime. It provides many great additions to CSS such as declaration nesting and variables, it is recommended to read the official guide to learn more. You can create .scss files containing only pure CSS code and it will work just fine, you are at no point required to write actual SCSS code. File Location # Please place your style files in a subdirectory of the style/ directory of the target application or the Core's style directory, for example style/layout/pageHeader.scss . Variables # You can access variables with $myVariable , variable interpolation (variables inside strings) is accomplished with #{$myVariable} . Linking images # Images used within a style must be located in the style's image folder. To get the folder name within the CSS the SCSS variable #{$style_image_path} can be used. The value will contain a trailing slash. Media Breakpoints # Media breakpoints instruct the browser to apply different CSS depending on the viewport dimensions, e.g. serving a desktop PC a different view than when viewed on a smartphone. 1 2 3 4 5 6 7 8 9 10 11 12 13 /* red background color for desktop pc */ @include screen-lg { body { background-color : red ; } } /* green background color on smartphones and tablets */ @include screen-md-down { body { background-color : green ; } } Available Breakpoints # Some very large smartphones, for example the Apple iPhone 7 Plus, do match the media query for Tablets (portrait) when viewed in landscape mode. Name Devices @media equivalent screen-xs Smartphones only (max-width: 544px) screen-sm Tablets (portrait) (min-width: 545px) and (max-width: 768px) screen-sm-down Tablets (portrait) and smartphones (max-width: 768px) screen-sm-up Tablets and desktop PC (min-width: 545px) screen-sm-md Tablets only (min-width: 545px) and (max-width: 1024px) screen-md Tablets (landscape) (min-width: 769px) and (max-width: 1024px) screen-md-down Smartphones and tablets (max-width: 1024px) screen-md-up Tablets (landscape) and desktop PC (min-width: 769px) screen-lg Desktop PC (min-width: 1025px) screen-lg-only Desktop PC (min-width: 1025px) and (max-width: 1280px) screen-lg-down Smartphones, tablets, and desktop PC (max-width: 1280px) screen-xl Desktop PC (min-width: 1281px) Asset Preloading # WoltLab Suite\u2019s SCSS compiler supports adding preloading metadata to the CSS. To communicate the preloading intent to the compiler, the --woltlab-suite-preload CSS variable is set to the result of the preload() function: 1 2 3 4 5 6 7 8 9 10 .fooBar { --woltlab-suite-preload : # { preload ( ' #{ $style_image_path } custom/background.png' , $ as : \"image\" , $ crossorigin : false , $ type : \"image/png\" ) } ; background : url ( ' #{ $style_image_path } custom/background.png' ); } The parameters of the preload() function map directly to the preloading properties that are used within the tag and the link: HTTP response header. The above example will result in a similar to the following being added to the generated HTML: 1 Use preloading sparingly for the most important resources where you can be certain that the browser will need them. Unused preloaded resources will unnecessarily waste bandwidth.","title":"CSS"},{"location":"view/css/#css","text":"","title":"CSS"},{"location":"view/css/#scss-and-css","text":"SCSS is a scripting language that features a syntax similar to CSS and compiles into native CSS at runtime. It provides many great additions to CSS such as declaration nesting and variables, it is recommended to read the official guide to learn more. You can create .scss files containing only pure CSS code and it will work just fine, you are at no point required to write actual SCSS code.","title":"SCSS and CSS"},{"location":"view/css/#file-location","text":"Please place your style files in a subdirectory of the style/ directory of the target application or the Core's style directory, for example style/layout/pageHeader.scss .","title":"File Location"},{"location":"view/css/#variables","text":"You can access variables with $myVariable , variable interpolation (variables inside strings) is accomplished with #{$myVariable} .","title":"Variables"},{"location":"view/css/#linking-images","text":"Images used within a style must be located in the style's image folder. To get the folder name within the CSS the SCSS variable #{$style_image_path} can be used. The value will contain a trailing slash.","title":"Linking images"},{"location":"view/css/#media-breakpoints","text":"Media breakpoints instruct the browser to apply different CSS depending on the viewport dimensions, e.g. serving a desktop PC a different view than when viewed on a smartphone. 1 2 3 4 5 6 7 8 9 10 11 12 13 /* red background color for desktop pc */ @include screen-lg { body { background-color : red ; } } /* green background color on smartphones and tablets */ @include screen-md-down { body { background-color : green ; } }","title":"Media Breakpoints"},{"location":"view/css/#available-breakpoints","text":"Some very large smartphones, for example the Apple iPhone 7 Plus, do match the media query for Tablets (portrait) when viewed in landscape mode. Name Devices @media equivalent screen-xs Smartphones only (max-width: 544px) screen-sm Tablets (portrait) (min-width: 545px) and (max-width: 768px) screen-sm-down Tablets (portrait) and smartphones (max-width: 768px) screen-sm-up Tablets and desktop PC (min-width: 545px) screen-sm-md Tablets only (min-width: 545px) and (max-width: 1024px) screen-md Tablets (landscape) (min-width: 769px) and (max-width: 1024px) screen-md-down Smartphones and tablets (max-width: 1024px) screen-md-up Tablets (landscape) and desktop PC (min-width: 769px) screen-lg Desktop PC (min-width: 1025px) screen-lg-only Desktop PC (min-width: 1025px) and (max-width: 1280px) screen-lg-down Smartphones, tablets, and desktop PC (max-width: 1280px) screen-xl Desktop PC (min-width: 1281px)","title":"Available Breakpoints"},{"location":"view/css/#asset-preloading","text":"WoltLab Suite\u2019s SCSS compiler supports adding preloading metadata to the CSS. To communicate the preloading intent to the compiler, the --woltlab-suite-preload CSS variable is set to the result of the preload() function: 1 2 3 4 5 6 7 8 9 10 .fooBar { --woltlab-suite-preload : # { preload ( ' #{ $style_image_path } custom/background.png' , $ as : \"image\" , $ crossorigin : false , $ type : \"image/png\" ) } ; background : url ( ' #{ $style_image_path } custom/background.png' ); } The parameters of the preload() function map directly to the preloading properties that are used within the tag and the link: HTTP response header. The above example will result in a similar to the following being added to the generated HTML: 1 Use preloading sparingly for the most important resources where you can be certain that the browser will need them. Unused preloaded resources will unnecessarily waste bandwidth.","title":"Asset Preloading"},{"location":"view/languages-naming-conventions/","text":"Language Naming Conventions # This page contains general rules for naming language items and for their values. API-specific rules are listed on the relevant API page: Comments Forms # Fields # If you have an application foo and a database object foo\\data\\bar\\Bar with a property baz that can be set via a form field, the name of the corresponding language item has to be foo.bar.baz . If you want to add an additional description below the field, use the language item foo.bar.baz.description . Error Texts # If an error of type {error type} for the previously mentioned form field occurs during validation, you have to use the language item foo.bar.baz.error.{error type} for the language item describing the error. Exception to this rule: There are several general error messages like wcf.global.form.error.empty that have to be used for general errors like an empty field that may not be empty to avoid duplication of the same error message text over and over again in different language items. Naming Conventions # If the entered text does not conform to some special rules, i.e. if the text is invalid, use invalid as error type. If the entered text is required to be unique but is already used for another object, use notUnique as error type. Confirmation messages # If the language item for an action is foo.bar.action , the language item for the confirmation message has to be foo.bar.action.confirmMessage instead of foo.bar.action.sure which is still used by some older language items. Type-Specific Deletion Confirmation Message # German # 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Icon wirklich l\u00f6schen? English # 1 Do you really want delete the {element type}? Example: 1 Do you really want delete the icon? Object-Specific Deletion Confirmation Message # German # 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} {object name} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Artikel {$article->getTitle()} wirklich l\u00f6schen? English # 1 Do you really want to delete the {element type} {object name}? Example: 1 Do you really want to delete the article {$article->getTitle()}? User Group Options # Comments # German # group type action example permission name language item user adding user.foo.canAddComment Kann Kommentare erstellen user deleting user.foo.canDeleteComment Kann eigene Kommentare l\u00f6schen user editing user.foo.canEditComment Kann eigene Kommentare bearbeiten moderator deleting mod.foo.canDeleteComment Kann Kommentare l\u00f6schen moderator editing mod.foo.canEditComment Kann Kommentare bearbeiten moderator moderating mod.foo.canModerateComment Kann Kommentare moderieren English # group type action example permission name language item user adding user.foo.canAddComment Can create comments user deleting user.foo.canDeleteComment Can delete their comments user editing user.foo.canEditComment Can edit their comments moderator deleting mod.foo.canDeleteComment Can delete comments moderator editing mod.foo.canEditComment Can edit comments moderator moderating mod.foo.canModerateComment Can moderate comments","title":"Language Naming Conventions"},{"location":"view/languages-naming-conventions/#language-naming-conventions","text":"This page contains general rules for naming language items and for their values. API-specific rules are listed on the relevant API page: Comments","title":"Language Naming Conventions"},{"location":"view/languages-naming-conventions/#forms","text":"","title":"Forms"},{"location":"view/languages-naming-conventions/#fields","text":"If you have an application foo and a database object foo\\data\\bar\\Bar with a property baz that can be set via a form field, the name of the corresponding language item has to be foo.bar.baz . If you want to add an additional description below the field, use the language item foo.bar.baz.description .","title":"Fields"},{"location":"view/languages-naming-conventions/#error-texts","text":"If an error of type {error type} for the previously mentioned form field occurs during validation, you have to use the language item foo.bar.baz.error.{error type} for the language item describing the error. Exception to this rule: There are several general error messages like wcf.global.form.error.empty that have to be used for general errors like an empty field that may not be empty to avoid duplication of the same error message text over and over again in different language items.","title":"Error Texts"},{"location":"view/languages-naming-conventions/#naming-conventions","text":"If the entered text does not conform to some special rules, i.e. if the text is invalid, use invalid as error type. If the entered text is required to be unique but is already used for another object, use notUnique as error type.","title":"Naming Conventions"},{"location":"view/languages-naming-conventions/#confirmation-messages","text":"If the language item for an action is foo.bar.action , the language item for the confirmation message has to be foo.bar.action.confirmMessage instead of foo.bar.action.sure which is still used by some older language items.","title":"Confirmation messages"},{"location":"view/languages-naming-conventions/#type-specific-deletion-confirmation-message","text":"","title":"Type-Specific Deletion Confirmation Message"},{"location":"view/languages-naming-conventions/#german","text":"1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} das Icon wirklich l\u00f6schen?","title":"German"},{"location":"view/languages-naming-conventions/#english","text":"1 Do you really want delete the {element type}? Example: 1 Do you really want delete the icon?","title":"English"},{"location":"view/languages-naming-conventions/#object-specific-deletion-confirmation-message","text":"","title":"Object-Specific Deletion Confirmation Message"},{"location":"view/languages-naming-conventions/#german_1","text":"1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {element type} {object name} wirklich l\u00f6schen? Example: 1 {if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Artikel {$article->getTitle()} wirklich l\u00f6schen?","title":"German"},{"location":"view/languages-naming-conventions/#english_1","text":"1 Do you really want to delete the {element type} {object name}? Example: 1 Do you really want to delete the article {$article->getTitle()}?","title":"English"},{"location":"view/languages-naming-conventions/#user-group-options","text":"","title":"User Group Options"},{"location":"view/languages-naming-conventions/#comments","text":"","title":"Comments"},{"location":"view/languages-naming-conventions/#german_2","text":"group type action example permission name language item user adding user.foo.canAddComment Kann Kommentare erstellen user deleting user.foo.canDeleteComment Kann eigene Kommentare l\u00f6schen user editing user.foo.canEditComment Kann eigene Kommentare bearbeiten moderator deleting mod.foo.canDeleteComment Kann Kommentare l\u00f6schen moderator editing mod.foo.canEditComment Kann Kommentare bearbeiten moderator moderating mod.foo.canModerateComment Kann Kommentare moderieren","title":"German"},{"location":"view/languages-naming-conventions/#english_2","text":"group type action example permission name language item user adding user.foo.canAddComment Can create comments user deleting user.foo.canDeleteComment Can delete their comments user editing user.foo.canEditComment Can edit their comments moderator deleting mod.foo.canDeleteComment Can delete comments moderator editing mod.foo.canEditComment Can edit comments moderator moderating mod.foo.canModerateComment Can moderate comments","title":"English"},{"location":"view/languages/","text":"Languages # WoltLab Suite offers full i18n support with its integrated language system, including but not limited to dynamic phrases using template scripting and the built-in support for right-to-left languages. Phrases are deployed using the language package installation plugin, please also read the naming conventions for language items . Special Phrases # wcf.date.dateFormat # Many characters in the format have a special meaning and will be replaced with date fragments. If you want to include a literal character, you'll have to use the backslash \\ as an escape sequence to indicate that the character should be output as-is rather than being replaced. For example, Y-m-d will be output as 2018-03-30 , but \\Y-m-d will result in Y-03-30 . Defaults to M jS Y . The date format without time using PHP's format characters for the date() function. This value is also used inside the JavaScript implementation, where the characters are mapped to an equivalent representation. wcf.date.timeFormat # Defaults to g:i a . The date format that is used to represent a time, but not a date. Please see the explanation on wcf.date.dateFormat to learn more about the format characters. wcf.date.firstDayOfTheWeek # Defaults to 0 . Sets the first day of the week: * 0 - Sunday * 1 - Monday wcf.global.pageDirection - RTL support # Defaults to ltr . Changing this value to rtl will reverse the page direction and enable the right-to-left support for phrases. Additionally, a special version of the stylesheet is loaded that contains all necessary adjustments for the reverse direction.","title":"Languages"},{"location":"view/languages/#languages","text":"WoltLab Suite offers full i18n support with its integrated language system, including but not limited to dynamic phrases using template scripting and the built-in support for right-to-left languages. Phrases are deployed using the language package installation plugin, please also read the naming conventions for language items .","title":"Languages"},{"location":"view/languages/#special-phrases","text":"","title":"Special Phrases"},{"location":"view/languages/#wcfdatedateformat","text":"Many characters in the format have a special meaning and will be replaced with date fragments. If you want to include a literal character, you'll have to use the backslash \\ as an escape sequence to indicate that the character should be output as-is rather than being replaced. For example, Y-m-d will be output as 2018-03-30 , but \\Y-m-d will result in Y-03-30 . Defaults to M jS Y . The date format without time using PHP's format characters for the date() function. This value is also used inside the JavaScript implementation, where the characters are mapped to an equivalent representation.","title":"wcf.date.dateFormat"},{"location":"view/languages/#wcfdatetimeformat","text":"Defaults to g:i a . The date format that is used to represent a time, but not a date. Please see the explanation on wcf.date.dateFormat to learn more about the format characters.","title":"wcf.date.timeFormat"},{"location":"view/languages/#wcfdatefirstdayoftheweek","text":"Defaults to 0 . Sets the first day of the week: * 0 - Sunday * 1 - Monday","title":"wcf.date.firstDayOfTheWeek"},{"location":"view/languages/#wcfglobalpagedirection-rtl-support","text":"Defaults to ltr . Changing this value to rtl will reverse the page direction and enable the right-to-left support for phrases. Additionally, a special version of the stylesheet is loaded that contains all necessary adjustments for the reverse direction.","title":"wcf.global.pageDirection - RTL support"},{"location":"view/template-plugins/","text":"Template Plugins # 5.3+ anchor # The anchor template plugin creates a HTML elements. The easiest way to use the template plugin is to pass it an instance of ITitledLinkObject : 1 { anchor object = $object } generates the same output as 1 getLink () } \"> { $object -> getTitle () } Instead of an object parameter, a link and content parameter can be used: 1 { anchor link = $linkObject content = $content } where $linkObject implements ILinkableObject and $content is either an object implementing ITitledObject or having a __toString() method or $content is a string or a number. The last special attribute is append whose contents are appended to the href attribute of the generated anchor element. All of the other attributes matching ~^[a-z]+([A-z]+)+$~ , expect for href which is disallowed, are added as attributes to the anchor element. If an object attribute is present, the object also implements IPopoverObject and if the return value of IPopoverObject::getPopoverLinkClass() is included in the class attribute of the anchor tag, data-object-id is automatically added. This functionality makes it easy to generate links with popover support. Instead of 1 getLink () } \" class=\"blogEntryLink\" data-object-id=\" { @ $entry -> entryID } \"> { $entry -> subject } using 1 { anchor object = $entry class = 'blogEntryLink' } is sufficient if Entry::getPopoverLinkClass() returns blogEntryLink . 5.3+ anchorAttributes # anchorAttributes compliments the StringUtil::getAnchorTagAttributes(string, bool): string method. It allows to easily generate the necessary attributes for an anchor tag based off the destination URL. 1 Attribute Description url destination URL appendHref whether the href attribute should be generated; true by default isUgc whether the rel=\"ugc\" attribute should be generated; false by default appendClassname whether the class=\"externalURL\" attribute should be generated; true by default append # If a string should be appended to the value of a variable, append can be used: 1 2 3 4 5 6 7 { assign var = templateVariable value = 'newValue' } { $templateVariable } { * prints 'newValue * } { append var = templateVariable value = '2' } { $templateVariable } { * now prints 'newValue2 * } If the variables does not exist yet, append creates a new one with the given value. If append is used on an array as the variable, the value is appended to all elements of the array. assign # New template variables can be declared and new values can be assigned to existing template variables using assign : 1 2 3 { assign var = templateVariable value = 'newValue' } { $templateVariable } { * prints 'newValue * } capture # In some situations, assign is not sufficient to assign values to variables in templates if the value is complex. Instead, capture can be used: 1 2 3 4 5 6 7 { capture var = templateVariable } { if $foo }

    { $bar }

    { else } { $baz } { /if } { /capture } concat # concat is a modifier used to concatenate multiple strings: 1 2 3 4 5 { assign var = foo value = 'foo' } { assign var = templateVariable value = 'bar' | concat : $foo } { $templateVariable } { * prints 'foobar * } counter # counter can be used to generate and optionally print a counter: 1 2 3 4 5 6 7 { counter name = fooCounter print = true } { * prints '1' * } { counter name = fooCounter print = true } { * prints '2' now * } { counter name = fooCounter } { * prints nothing, but counter value is '3' now internally * } { counter name = fooCounter print = true } { * prints '4' * } Counter supports the following attributes: Attribute Description assign optional name of the template variable the current counter value is assigned to direction counting direction, either up or down ; up by default name name of the counter, relevant if multiple counters are used simultaneously print if true , the current counter value is printed; false by default skip positive counting increment; 1 by default start start counter value; 1 by default 5.4+ csrfToken # {csrfToken} prints out the session's CSRF token (\u201cSecurity Token\u201d). 1 2 3 4 5
    { * snip * } { csrfToken } The {csrfToken} template plugin supports a type parameter. Specifying this parameter might be required in rare situations. Please check the implementation for details. currency # currency is a modifier used to format currency values with two decimals using language dependent thousands separators and decimal point: 1 2 3 { assign var = currencyValue value = 12.345 } { $currencyValue | currency } { * prints '12.34' * } cycle # cycle can be used to cycle between different values: 1 2 3 4 5 6 7 { cycle name = fooCycle values = 'bar,baz' } { * prints 'bar' * } { cycle name = fooCycle } { * prints 'baz' * } { cycle name = fooCycle advance = false } { * prints 'baz' again * } { cycle name = fooCycle } { * prints 'bar' * } The values attribute only has to be present for the first call. If cycle is used in a loop, the presence of the same values in consecutive calls has no effect. Only once the values change, the cycle is reset. Attribute Description advance if true , the current cycle value is advanced to the next value; true by default assign optional name of the template variable the current cycle value is assigned to; if used, print is set to false delimiter delimiter between the different cycle values; , by default name name of the cycle, relevant if multiple cycles are used simultaneously print if true , the current cycle value is printed, false by default reset if true , the current cycle value is set to the first value, false by default values string containing the different cycles values, also see delimiter date # date generated a formatted date using wcf\\util\\DateUtil::format() with DateUtil::DATE_FORMAT internally. 1 { $timestamp | date } 3.1+ dateInterval # dateInterval calculates the difference between two unix timestamps and generated a textual date interval. 1 { dateInterval start = $startTimestamp end = $endTimestamp full = true format = 'sentence' } Attribute Description end end of the time interval; current timestamp by default (though either start or end has to be set) format output format, either default , sentence , or plain ; defaults to default , see wcf\\util\\DateUtil::FORMAT_* constants full if true , full difference in minutes is shown; if false , only the longest time interval is shown; false by default start start of the time interval; current timestamp by default (though either start or end has to be set) encodeJS # encodeJS encodes a string to be used as a single-quoted string in JavaScript by replacing \\\\ with \\\\\\\\ , ' with \\' , linebreaks with \\n , and / with \\/ . 1 2 3 encodeJSON # encodeJSON encodes a JSON string to be used as a single-quoted string in JavaScript by replacing \\\\ with \\\\\\\\ , ' with ' , linebreaks with \\n , and / with \\/ . Additionally, htmlspecialchars is applied to the string. 1 ' { @ $foo | encodeJSON } ' escapeCDATA # escapeCDATA encodes a string to be used in a CDATA element by replacing ]]> with ]]]]> . 1 event # event provides extension points in templates that template listeners can use. 1 { event name = 'foo' } fetch # fetch fetches the contents of a file using file_get_contents . 1 2 3 { fetch file = 'foo.html' } { * prints the contents of `foo.html` * } { fetch file = 'bar.html' assign = bar } { * assigns the contents of `foo.html` to `$bar`; does not print the contents * } filesizeBinary # filesizeBinary formats the filesize using binary filesize (in bytes). 1 { $filesize | filesizeBinary } filesize # filesize formats the filesize using filesize (in bytes). 1 { $filesize | filesize } hascontent # In many cases, conditional statements can be used to determine if a certain section of a template is shown: 1 2 3 { if $foo === 'bar' } only shown if $foo is bar { /if } In some situations, however, such conditional statements are not sufficient. One prominent example is a template event: 1 2 3 4 5 6 7 8 9 { if $foo === 'bar' }
      { if $foo === 'bar' }
    • Bar
    • { /if } { event name = 'listItems' } { /if } In this example, if $foo !== 'bar' , the list will not be shown, regardless of the additional template code provided by template listeners. In such a situation, hascontent has to be used: 1 2 3 4 5 6 7 8 9 10 11 { hascontent }
        { content } { if $foo === 'bar' }
      • Bar
      • { /if } { event name = 'listItems' } { /content }
      { /hascontent } If the part of the template wrapped in the content tags has any (trimmed) content, the part of the template wrapped by hascontent tags is shown (including the part wrapped by the content tags), otherwise nothing is shown. Thus, this construct avoids an empty list compared to the if solution above. Like foreach , hascontent also supports an else part: 1 2 3 4 5 6 7 8 9 { hascontent }
        { content } { * \u2026 * } { /content }
      { hascontentelse } no list { /hascontent } htmlCheckboxes # htmlCheckboxes generates a list of HTML checkboxes. 1 2 3 { htmlCheckboxes name = foo options = $fooOptions selected = $currentFoo } { htmlCheckboxes name = bar output = $barLabels values = $barValues selected = $currentBar } Attribute Description 5.2+ disabled if true , all checkboxes are disabled disableEncoding if true , the values are not passed through wcf\\util\\StringUtil::encodeHTML() ; false by default name name attribute of the input checkbox element output array used as keys and values for options if present; not present by default options array selectable options with the key used as value attribute and the value as the checkbox label selected current selected value(s) separator separator between the different checkboxes in the generated output; empty string by default values array with values used in combination with output , where output is only used as keys for options htmlOptions # htmlOptions generates an select HTML element. 1 2 3 4 5 6 { htmlOptions name = 'foo' options = $options selected = $selected } Attribute Description disableEncoding if true , the values are not passed through wcf\\util\\StringUtil::encodeHTML() ; false by default object optional instance of wcf\\data\\DatabaseObjectList that provides the selectable options (overwrites options attribute internally) name name attribute of the select element; if not present, only the contents of the select element are printed output array used as keys and values for options if present; not present by default values array with values used in combination with output , where output is only used as keys for options options array selectable options with the key used as value attribute and the value as the option label; if a value is an array, an optgroup is generated with the array key as the optgroup label selected current selected value(s) All additional attributes are added as attributes of the select HTML element. implode # implodes transforms an array into a string and prints it. 1 { implode from = $array key = key item = item glue = \";\" }{ $key } : { $value }{ /implode } Attribute Description from array with the imploded values glue separator between the different array values; ', ' by default item template variable name where the current array value is stored during the iteration key optional template variable name where the current array key is stored during the iteration 5.2+ ipSearch # ipSearch generates a link to search for an IP address. 1 { \"127.0.0.1\" | ipSearch } 3.0+ js # js generates script tags based on whether ENABLE_DEBUG_MODE and VISITOR_USE_TINY_BUILD are enabled. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { js application = 'wbb' file = 'WBB' } { * generates 'http://example.com/js/WBB.js' * } { js application = 'wcf' file = 'WCF.Like' bundle = 'WCF.Combined' } { * generates 'http://example.com/wcf/js/WCF.Like.js' if ENABLE_DEBUG_MODE=1 * } { * generates 'http://example.com/wcf/js/WCF.Combined.min.js' if ENABLE_DEBUG_MODE=0 * } { js application = 'wcf' lib = 'jquery' } { * generates 'http://example.com/wcf/js/3rdParty/jquery.js' * } { js application = 'wcf' lib = 'jquery-ui' file = 'awesomeWidget' } { * generates 'http://example.com/wcf/js/3rdParty/jquery-ui/awesomeWidget.js' * } { js application = 'wcf' file = 'WCF.Like' bundle = 'WCF.Combined' hasTiny = true } { * generates 'http://example.com/wcf/js/WCF.Like.js' if ENABLE_DEBUG_MODE=1 * } { * generates 'http://example.com/wcf/js/WCF.Combined.min.js' (ENABLE_DEBUG_MODE=0 * } { * generates 'http://example.com/wcf/js/WCF.Combined.tiny.min.js' if ENABLE_DEBUG_MODE=0 and VISITOR_USE_TINY_BUILD=1 * } 5.3+ jslang # jslang works like lang with the difference that the resulting string is automatically passed through encodeJS . 1 2 3 4 5 6 7 require(['Language', /* \u2026 */], function(Language, /* \u2026 */) { Language . addObject ( { 'app.foo.bar' : '{jslang}app.foo.bar{/jslang}' , } ); // \u2026 } ); lang # lang replaces a language items with its value. 1 2 3 4 5 6 7 { lang } foo.bar.baz { /lang } { lang __literal = true } foo.bar.baz { /lang } { lang foo = 'baz' } foo.bar.baz { /lang } { lang } foo.bar.baz. { $action }{ /lang } Attribute Description __encode if true , the output will be passed through StringUtil::encodeHTML() __literal if true , template variables will not resolved but printed as they are in the language item; false by default __optional if true and the language item does not exist, an empty string is printed; false by default All additional attributes are available when parsing the language item. language # language replaces a language items with its value. If the template variable __language exists, this language object will be used instead of WCF::getLanguage() . This modifier is useful when assigning the value directly to a variable. 1 2 3 { $languageItem | language } { assign var = foo value = $languageItem | language } link # link generates internal links using LinkHandler . 1
      Foo Attribute Description application abbreviation of the application the controller belongs to; wcf by default controller name of the controller; if not present, the landing page is linked in the frontend and the index page in the ACP encode if true , the generated link is passed through wcf\\util\\StringUtil::encodeHTML() ; true by default isEmail sets encode=false and forces links to link to the frontend Additional attributes are passed to LinkHandler::getLink() . newlineToBreak # newlineToBreak transforms newlines into HTML
      elements after encoding the content via wcf\\util\\StringUtil::encodeHTML() . 1 { $foo | newlineToBreak } 5.4+ objectAction # objectAction generates action buttons to be used in combination with the WoltLabSuite/Core/Ui/Object/Action API . For detailed information on its usage, we refer to the extensive documentation in the ObjectActionFunctionTemplatePlugin class itself. 3.0+ page # page generates an internal link to a CMS page. 1 2 3 4 5 6 7 { page } com.woltlab.wcf.CookiePolicy { /page } { page pageID = 1 }{ /page } { page language = 'de' } com.woltlab.wcf.CookiePolicy { /page } { page languageID = 2 } com.woltlab.wcf.CookiePolicy { /page } Attribute Description pageID unique id of the page (cannot be used together with a page identifier as value) languageID id of the page language (cannot be used together with language ) language language code of the page language (cannot be used together with languageID ) pages # pages generates a pagination. 1 2 3 { pages controller = 'FooList' link = \"pageNo=%d\" print = true assign = pagesLinks } { * prints pagination * } { @ $pagesLinks } { * prints same pagination again * } Attribute Description assign optional name of the template variable the pagination is assigned to controller controller name of the generated links link additional link parameter where %d will be replaced with the relevant page number pages maximum number of of pages; by default, the template variable $pages is used print if false and assign=true , the pagination is not printed application , id , object , title additional parameters passed to LinkHandler::getLink() to generate page links plainTime # plainTime formats a timestamp to include year, month, day, hour, and minutes. The exact formatting depends on the current language (via the language items wcf.date.dateTimeFormat , wcf.date.dateFormat , and wcf.date.timeFormat ). 1 { $timestamp | plainTime } 5.3+ plural # plural allows to easily select the correct plural form of a phrase based on a given value . The pluralization logic follows the Unicode Language Plural Rules for cardinal numbers. The # placeholder within the resulting phrase is replaced by the value . It is automatically formatted using StringUtil::formatNumeric . English: Note the use of 1 if the number ( # ) is not used within the phrase and the use of one otherwise. They are equivalent for English, but following this rule generalizes better to other languages, helping the translator. 1 2 3 4 { assign var = numberOfWorlds value = 2 }

      Hello { plural value = $numberOfWorlds 1 = 'World' other = 'Worlds' } !

      There { plural value = $numberOfWorlds 1 = 'is one world' other = 'are # worlds' } !

      There { plural value = $numberOfWorlds one = 'is # world' other = 'are # worlds' } !

      German: 1 2 3 4 { assign var = numberOfWorlds value = 2 }

      Hallo { plural value = $numberOfWorlds 1 = 'Welt' other = 'Welten' } !

      Es gibt { plural value = $numberOfWorlds 1 = 'eine Welt' other = '# Welten' } !

      Es gibt { plural value = $numberOfWorlds one = '# Welt' other = '# Welten' } !

      Romanian: Note the additional use of few which is not required in English or German. 1 2 3 4 { assign var = numberOfWorlds value = 2 }

      Salut { plural value = $numberOfWorlds 1 = 'lume' other = 'lumi' } !

      Exist\u0103 { plural value = $numberOfWorlds 1 = 'o lume' few = '# lumi' other = '# de lumi' } !

      Exist\u0103 { plural value = $numberOfWorlds one = '# lume' few = '# lumi' other = '# de lumi' } !

      Russian: Note the difference between 1 (exactly 1 ) and one (ending in 1 , except ending in 11 ). 1 2 3 { assign var = numberOfWorlds value = 2 }

      \u041f\u0440\u0438\u0432\u0435\u0442 { plural value = $numberOfWorld 1 = '\u043c\u0438\u0440' other = '\u043c\u0438\u0440\u044b' } !

      \u0415\u0441\u0442\u044c { plural value = $numberOfWorlds 1 = '\u043c\u0438\u0440' one = '# \u043c\u0438\u0440' few = '# \u043c\u0438\u0440\u0430' many = '# \u043c\u0438\u0440\u043e\u0432' other = '# \u043c\u0438\u0440\u043e\u0432' } !

      Attribute Description value The value that is used to select the proper phrase. other The phrase that is used when no other selector matches. Any Category Name The phrase that is used when value belongs to the named category. Available categories depend on the language. Any Integer The phrase that is used when value is that exact integer. prepend # If a string should be prepended to the value of a variable, prepend can be used: 1 2 3 4 5 6 7 { assign var = templateVariable value = 'newValue' } { $templateVariable } { * prints 'newValue * } { prepend var = templateVariable value = '2' } { $templateVariable } { * now prints '2newValue' * } If the variables does not exist yet, prepend creates a new one with the given value. If prepend is used on an array as the variable, the value is prepended to all elements of the array. shortUnit # shortUnit shortens numbers larger than 1000 by using unit suffixes: 1 2 { 10000 | shortUnit } { * prints 10k * } { 5400000 | shortUnit } { * prints 5.4M * } smallpages # smallpages generates a smaller version of pages by using adding the small CSS class to the generated
     1
    +
    +
    files/lib/system/event/listener/MyUserExportGdprActionListener.class.php
    +
     1
      2
      3
      4
    @@ -2148,25 +2150,27 @@ has been dumped to the $data property.

    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'; - } + public function execute(/** @var UserExportGdprAction $eventObj */$eventObj, $className, $eventName, array &$parameters) { + /** @var UserProfile $user */ + $user = $eventObj->user; + + $eventObj->data['my.fancy.plugin'] = [ + 'superPersonalData' => "This text is super personal and should be included in the output", + 'weirdIpAddresses' => $eventObj->exportIpAddresses('app'.WCF_N.'_non_standard_column_names_for_ip_addresses', 'ipAddressColumnName', 'timeColumnName', 'userIDColumnName') + ]; + $eventObj->exportUserProperties[] = 'shouldAlwaysExportThisField'; + $eventObj->exportUserPropertiesIfNotEmpty[] = 'myFancyField'; + $eventObj->exportUserOptionSettings[] = 'thisSettingIsAlwaysExported'; + $eventObj->exportUserOptionSettingsIfNotEmpty[] = 'someSettingContainingPersonalData'; + $eventObj->ipAddresses['my.fancy.plugin'] = ['wcf'.WCF_N.'_my_fancy_table', 'wcf'.WCF_N.'_i_also_store_ipaddresses_here']; + $eventObj->skipUserOptions[] = 'thisLooksLikePersonalDataButItIsNot'; + $eventObj->skipUserOptions[] = 'thisIsAlsoNotPersonalDataPleaseIgnoreIt'; + } }
    +
    +

    $data#

    Contains the entire data that will be included in the exported JSON file, some fields may already exist (such as 'com.woltlab.wcf') and while you may add or @@ -2214,7 +2218,7 @@ because it does not contain personal data, such as internal data.

    - Last update: 2021-01-08 + Last update: 2021-04-23
    diff --git a/5.4/search/search_index.json b/5.4/search/search_index.json index 3f633257..5c442357 100644 --- a/5.4/search/search_index.json +++ b/5.4/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"WoltLab Suite 5.4 Documentation # Introduction # This documentation explains the basic API functionality and the creation of own packages. It is expected that you are somewhat experienced with PHP , object-oriented programming and MySQL . Head over to the quick start tutorial to learn more. About WoltLab Suite # WoltLab Suite Core as well as most of the other packages are available on GitHub and are licensed under the terms of the GNU Lesser General Public License 2.1 .","title":"WoltLab Suite 5.4 Documentation"},{"location":"#woltlab-suite-54-documentation","text":"","title":"WoltLab Suite 5.4 Documentation"},{"location":"#introduction","text":"This documentation explains the basic API functionality and the creation of own packages. It is expected that you are somewhat experienced with PHP , object-oriented programming and MySQL . Head over to the quick start tutorial to learn more.","title":"Introduction"},{"location":"#about-woltlab-suite","text":"WoltLab Suite Core as well as most of the other packages are available on GitHub and are licensed under the terms of the GNU Lesser General Public License 2.1 .","title":"About WoltLab Suite"},{"location":"getting-started/","text":"Creating a simple package # Setup and Requirements # This guide will help you to create a simple package that provides a simple test page. It is nothing too fancy, but you can use it as the foundation for your next project. There are some requirements you should met before starting: Text editor with syntax highlighting for PHP, Notepad++ is a solid pick *.php and *.tpl should be encoded with ANSI/ASCII *.xml are always encoded with UTF-8, but omit the BOM (byte-order-mark) Use tabs instead of spaces to indent lines It is recommended to set the tab width to 8 spaces, this is used in the entire software and will ease reading the source files An active installation of WoltLab Suite 3 An application to create *.tar archives, e.g. 7-Zip on Windows The package.xml File # We want to create a simple page that will display the sentence \"Hello World\" embedded into the application frame. Create an empty directory in the workspace of your choice to start with. Create a new file called package.xml and insert the code below: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Simple Package A simple package to demonstrate the package system of WoltLab Suite Core 1.0.0 2019-04-28 Your Name http://www.example.com com.woltlab.wcf There is an entire chapter on the package system that explains what the code above does and how you can adjust it to fit your needs. For now we'll keep it as it is. The PHP Class # The next step is to create the PHP class which will serve our page: Create the directory files in the same directory where package.xml is located Open files and create the directory lib Open lib and create the directory page Within the directory page , please create the file TestPage.class.php Copy and paste the following code into the TestPage.class.php : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 */ class TestPage extends AbstractPage { /** * @var string */ protected $greet = '' ; /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_GET [ 'greet' ])) $this -> greet = $_GET [ 'greet' ]; } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( empty ( $this -> greet )) { $this -> greet = 'World' ; } } /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'greet' => $this -> greet ]); } } The class inherits from wcf\\page\\AbstractPage , the default implementation of pages without form controls. It defines quite a few methods that will be automatically invoked in a specific order, for example readParameters() before readData() and finally assignVariables() to pass arbitrary values to the template. The property $greet is defined as World , but can optionally be populated through a GET variable ( index.php?test/&greet=You would output Hello You! ). This extra code illustrates the separation of data processing that takes place within all sort of pages, where all user-supplied data is read from within a single method. It helps organizing the code, but most of all it enforces a clean class logic that does not start reading user input at random places, including the risk to only escape the input of variable $_GET['foo'] 4 out of 5 times. Reading and processing the data is only half the story, now we need a template to display the actual content for our page. You don't need to specify it yourself, it will be automatically guessed based on your namespace and class name, you can read more about it later . Last but not least, you must not include the closing PHP tag ?> at the end, it can cause PHP to break on whitespaces and is not required at all. The Template # Navigate back to the root directory of your package until you see both the files directory and the package.xml . Now create a directory called templates , open it and create the file test.tpl . 1 2 3 4 5 6 7 { include file = 'header' }
    Hello { $greet } !
    { include file = 'footer' } Templates are a mixture of HTML and Smarty-like template scripting to overcome the static nature of raw HTML. The above code will display the phrase Hello World! in the application frame, just as any other page would render. The included templates header and footer are responsible for the majority of the overall page functionality, but offer a whole lot of customization abilities to influence their behavior and appearance. The Page Definition # The package now contains the PHP class and the matching template, but it is still missing the page definition. Please create the file page.xml in your project's root directory, thus on the same level as the package.xml . 1 2 3 4 5 6 7 8 9 10 wcf\\page\\TestPage Test Page system You can provide a lot more data for a page, including logical nesting and dedicated handler classes for display in menus. Building the Package # If you have followed the above guidelines carefully, your package directory should now look like this: 1 2 3 4 5 6 7 8 \u251c\u2500\u2500 files \u2502 \u2514\u2500\u2500 lib \u2502 \u251c\u2500\u2500 page \u2502 \u2502 \u251c\u2500\u2500 TestPage.class.php \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 page.xml \u251c\u2500\u2500 templates \u2502 \u2514\u2500\u2500 test.tpl Both files and templates are archive-based package components, that deploy their payload using tar archives rather than adding the raw files to the package file. Please create the archive files.tar and add the contents of the files/* directory, but not the directory files/ itself. Repeat the same process for the templates directory, but this time with the file name templates.tar . Place both files in the root of your project. Last but not least, create the package archive com.example.test.tar and add all the files listed below. files.tar package.xml page.xml templates.tar The archive's filename can be anything you want, all though it is the general convention to use the package name itself for easier recognition. Installation # Open the Administration Control Panel and navigate to Configuration > Packages > Install Package , click on Upload Package and select the file com.example.test.tar from your disk. Follow the on-screen instructions until it has been successfully installed. Open a new browser tab and navigate to your newly created page. If WoltLab Suite is installed at https://example.com/wsc/ , then the URL should read https://example.com/wsc/index.php?test/ . Congratulations, you have just created your first package! Developer Tools # This feature is available with WoltLab Suite 3.1 or newer only. The developer tools provide an interface to synchronize the data of an installed package with a bare repository on the local disk. You can re-import most PIPs at any time and have the changes applied without crafting a manual update. This process simulates a regular package update with a single PIP only, and resets the cache after the import has been completed. Registering a Project # Projects require the absolute path to the package directory, that is, the directory where it can find the package.xml . It is not required to install an package to register it as a project, but you have to install it in order to work with it. It does not install the package by itself! There is a special button on the project list that allows for a mass-import of projects based on a search path. Each direct child directory of the provided path will be tested and projects created this way will use the identifier extracted from the package.xml . Synchronizing # The install instructions in the package.xml are ignored when offering the PIP imports, the detection works entirely based on the default filename for each PIP. On top of that, only PIPs that implement the interface wcf\\system\\devtools\\pip\\IIdempotentPackageInstallationPlugin are valid for import, as it indicates that importing the PIP multiple times will have no side-effects and that the result is deterministic regardless of the number of times it has been imported. Some built-in PIPs, such as sql or script , do not qualify for this step and remain unavailable at all times. However, you can still craft and perform an actual package update to have these PIPs executed. Appendix # Template Guessing # The class name including the namespace is used to automatically determine the path to the template and its name. The example above used the page class name wcf\\page\\TestPage that is then split into four distinct parts: wcf , the internal abbreviation of WoltLab Suite Core (previously known as WoltLab Community Framework) \\page\\ (ignored) Test , the actual name that is used for both the template and the URL Page (page type, ignored) The fragments 1. and 3. from above are used to construct the path to the template: /templates/test.tpl (the first letter of Test is being converted to lower-case).","title":"Getting Started"},{"location":"getting-started/#creating-a-simple-package","text":"","title":"Creating a simple package"},{"location":"getting-started/#setup-and-requirements","text":"This guide will help you to create a simple package that provides a simple test page. It is nothing too fancy, but you can use it as the foundation for your next project. There are some requirements you should met before starting: Text editor with syntax highlighting for PHP, Notepad++ is a solid pick *.php and *.tpl should be encoded with ANSI/ASCII *.xml are always encoded with UTF-8, but omit the BOM (byte-order-mark) Use tabs instead of spaces to indent lines It is recommended to set the tab width to 8 spaces, this is used in the entire software and will ease reading the source files An active installation of WoltLab Suite 3 An application to create *.tar archives, e.g. 7-Zip on Windows","title":"Setup and Requirements"},{"location":"getting-started/#the-packagexml-file","text":"We want to create a simple page that will display the sentence \"Hello World\" embedded into the application frame. Create an empty directory in the workspace of your choice to start with. Create a new file called package.xml and insert the code below: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Simple Package A simple package to demonstrate the package system of WoltLab Suite Core 1.0.0 2019-04-28 Your Name http://www.example.com com.woltlab.wcf There is an entire chapter on the package system that explains what the code above does and how you can adjust it to fit your needs. For now we'll keep it as it is.","title":"The package.xml File"},{"location":"getting-started/#the-php-class","text":"The next step is to create the PHP class which will serve our page: Create the directory files in the same directory where package.xml is located Open files and create the directory lib Open lib and create the directory page Within the directory page , please create the file TestPage.class.php Copy and paste the following code into the TestPage.class.php : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 */ class TestPage extends AbstractPage { /** * @var string */ protected $greet = '' ; /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_GET [ 'greet' ])) $this -> greet = $_GET [ 'greet' ]; } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( empty ( $this -> greet )) { $this -> greet = 'World' ; } } /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'greet' => $this -> greet ]); } } The class inherits from wcf\\page\\AbstractPage , the default implementation of pages without form controls. It defines quite a few methods that will be automatically invoked in a specific order, for example readParameters() before readData() and finally assignVariables() to pass arbitrary values to the template. The property $greet is defined as World , but can optionally be populated through a GET variable ( index.php?test/&greet=You would output Hello You! ). This extra code illustrates the separation of data processing that takes place within all sort of pages, where all user-supplied data is read from within a single method. It helps organizing the code, but most of all it enforces a clean class logic that does not start reading user input at random places, including the risk to only escape the input of variable $_GET['foo'] 4 out of 5 times. Reading and processing the data is only half the story, now we need a template to display the actual content for our page. You don't need to specify it yourself, it will be automatically guessed based on your namespace and class name, you can read more about it later . Last but not least, you must not include the closing PHP tag ?> at the end, it can cause PHP to break on whitespaces and is not required at all.","title":"The PHP Class"},{"location":"getting-started/#the-template","text":"Navigate back to the root directory of your package until you see both the files directory and the package.xml . Now create a directory called templates , open it and create the file test.tpl . 1 2 3 4 5 6 7 { include file = 'header' }
    Hello { $greet } !
    { include file = 'footer' } Templates are a mixture of HTML and Smarty-like template scripting to overcome the static nature of raw HTML. The above code will display the phrase Hello World! in the application frame, just as any other page would render. The included templates header and footer are responsible for the majority of the overall page functionality, but offer a whole lot of customization abilities to influence their behavior and appearance.","title":"The Template"},{"location":"getting-started/#the-page-definition","text":"The package now contains the PHP class and the matching template, but it is still missing the page definition. Please create the file page.xml in your project's root directory, thus on the same level as the package.xml . 1 2 3 4 5 6 7 8 9 10 wcf\\page\\TestPage Test Page system You can provide a lot more data for a page, including logical nesting and dedicated handler classes for display in menus.","title":"The Page Definition"},{"location":"getting-started/#building-the-package","text":"If you have followed the above guidelines carefully, your package directory should now look like this: 1 2 3 4 5 6 7 8 \u251c\u2500\u2500 files \u2502 \u2514\u2500\u2500 lib \u2502 \u251c\u2500\u2500 page \u2502 \u2502 \u251c\u2500\u2500 TestPage.class.php \u251c\u2500\u2500 package.xml \u251c\u2500\u2500 page.xml \u251c\u2500\u2500 templates \u2502 \u2514\u2500\u2500 test.tpl Both files and templates are archive-based package components, that deploy their payload using tar archives rather than adding the raw files to the package file. Please create the archive files.tar and add the contents of the files/* directory, but not the directory files/ itself. Repeat the same process for the templates directory, but this time with the file name templates.tar . Place both files in the root of your project. Last but not least, create the package archive com.example.test.tar and add all the files listed below. files.tar package.xml page.xml templates.tar The archive's filename can be anything you want, all though it is the general convention to use the package name itself for easier recognition.","title":"Building the Package"},{"location":"getting-started/#installation","text":"Open the Administration Control Panel and navigate to Configuration > Packages > Install Package , click on Upload Package and select the file com.example.test.tar from your disk. Follow the on-screen instructions until it has been successfully installed. Open a new browser tab and navigate to your newly created page. If WoltLab Suite is installed at https://example.com/wsc/ , then the URL should read https://example.com/wsc/index.php?test/ . Congratulations, you have just created your first package!","title":"Installation"},{"location":"getting-started/#developer-tools","text":"This feature is available with WoltLab Suite 3.1 or newer only. The developer tools provide an interface to synchronize the data of an installed package with a bare repository on the local disk. You can re-import most PIPs at any time and have the changes applied without crafting a manual update. This process simulates a regular package update with a single PIP only, and resets the cache after the import has been completed.","title":"Developer Tools"},{"location":"getting-started/#registering-a-project","text":"Projects require the absolute path to the package directory, that is, the directory where it can find the package.xml . It is not required to install an package to register it as a project, but you have to install it in order to work with it. It does not install the package by itself! There is a special button on the project list that allows for a mass-import of projects based on a search path. Each direct child directory of the provided path will be tested and projects created this way will use the identifier extracted from the package.xml .","title":"Registering a Project"},{"location":"getting-started/#synchronizing","text":"The install instructions in the package.xml are ignored when offering the PIP imports, the detection works entirely based on the default filename for each PIP. On top of that, only PIPs that implement the interface wcf\\system\\devtools\\pip\\IIdempotentPackageInstallationPlugin are valid for import, as it indicates that importing the PIP multiple times will have no side-effects and that the result is deterministic regardless of the number of times it has been imported. Some built-in PIPs, such as sql or script , do not qualify for this step and remain unavailable at all times. However, you can still craft and perform an actual package update to have these PIPs executed.","title":"Synchronizing"},{"location":"getting-started/#appendix","text":"","title":"Appendix"},{"location":"getting-started/#template-guessing","text":"The class name including the namespace is used to automatically determine the path to the template and its name. The example above used the page class name wcf\\page\\TestPage that is then split into four distinct parts: wcf , the internal abbreviation of WoltLab Suite Core (previously known as WoltLab Community Framework) \\page\\ (ignored) Test , the actual name that is used for both the template and the URL Page (page type, ignored) The fragments 1. and 3. from above are used to construct the path to the template: /templates/test.tpl (the first letter of Test is being converted to lower-case).","title":"Template Guessing"},{"location":"javascript/code-snippets/","text":"Code Snippets - JavaScript API # This is a list of code snippets that do not fit into any of the other articles and merely describe how to achieve something very specific, rather than explaining the inner workings of a function. ImageViewer # The ImageViewer is available on all frontend pages by default, you can easily add images to the viewer by wrapping the thumbnails with a link with the CSS class jsImageViewer that points to the full version. 1 2 3 < a href = \"http://example.com/full.jpg\" class = \"jsImageViewer\" > < img src = \"http://example.com/thumbnail.jpg\" > ","title":"Code Snippets"},{"location":"javascript/code-snippets/#code-snippets-javascript-api","text":"This is a list of code snippets that do not fit into any of the other articles and merely describe how to achieve something very specific, rather than explaining the inner workings of a function.","title":"Code Snippets - JavaScript API"},{"location":"javascript/code-snippets/#imageviewer","text":"The ImageViewer is available on all frontend pages by default, you can easily add images to the viewer by wrapping the thumbnails with a link with the CSS class jsImageViewer that points to the full version. 1 2 3 < a href = \"http://example.com/full.jpg\" class = \"jsImageViewer\" > < img src = \"http://example.com/thumbnail.jpg\" > ","title":"ImageViewer"},{"location":"javascript/general-usage/","text":"General JavaScript Usage # WoltLab Suite 5.4 introduced support for TypeScript, migrating all existing modules to TypeScript. The JavaScript section of the documentation is not yet updated to account for the changes, possibly explaining concepts that cannot be applied as-is when writing TypeScript. You can learn about basic TypeScript use in WoltLab Suite, such as consuming WoltLab Suite\u2019s types in own packages, within in the TypeScript section . The History of the Legacy API # The WoltLab Suite 3.0 introduced a new API based on AMD-Modules with ES5-JavaScript that was designed with high performance and visible dependencies in mind. This was a fundamental change in comparison to the legacy API that was build many years before while jQuery was still a thing and we had to deal with ancient browsers such as Internet Explorer 9 that felt short in both CSS and JavaScript capabilities. Fast forward a few years, the old API is still around and most important, it is actively being used by some components that have not been rewritten yet. This has been done to preserve the backwards-compatibility and to avoid the significant amount of work that it requires to rewrite a component. The components invoked on page initialization have all been rewritten to use the modern API, but some deferred objects that are invoked later during the page runtime may still use the old API. However, the legacy API is deprecated and you should not rely on it for new components at all. It slowly but steadily gets replaced up until a point where its last bits are finally removed from the code base. Embedding JavaScript inside Templates # The { * do not include `` here, the footer template is the last bit of code! * } { include file = 'footer' } Content Header # There are two different methods to set the content header, one sets only the actual values, but leaves the outer HTML untouched, that is generated by the header template. This is the recommended approach and you should avoid using the alternative method whenever possible. Recommended Approach # 1 2 3 4 5 6 { * This is automatically set using the page data and should not be set manually! * } { capture assign = 'contentTitle' } Custom Content Title { /capture } { capture assign = 'contentDescription' } Optional description that is displayed right after the title. { /capture } { capture assign = 'contentHeaderNavigation' } List of navigation buttons displayed right next to the title. { /capture } Alternative # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { capture assign = 'contentHeader' }

    Custom Content Title

    Custom Content Description

    { /capture }","title":"Templates"},{"location":"migration/wcf21/templates/#wcf-21x-templates","text":"","title":"WCF 2.1.x - Templates"},{"location":"migration/wcf21/templates/#page-layout","text":"The template structure has been overhauled and it is no longer required nor recommended to include internal templates, such as documentHeader , headInclude or userNotice . Instead use a simple {include file='header'} that now takes care of of the entire application frame. Templates must not include a trailing after including the footer template. The documentHeader , headInclude and userNotice template should no longer be included manually, the same goes with the element, please use {include file='header'} instead. The sidebarOrientation variable for the header template has been removed and no longer works. header.boxHeadline has been unified and now reads header.contentHeader Please see the full example at the end of this page for more information.","title":"Page Layout"},{"location":"migration/wcf21/templates/#sidebars","text":"Sidebars are now dynamically populated by the box system, this requires a small change to unify the markup. Additionally the usage of
    has been deprecated due to browser inconsistencies and bugs and should be replaced with section.box . Previous markup used in WoltLab Community Framework 2.1 and earlier: 1 2 3 4 5 6 7 < fieldset > < legend > < div > The new markup since WoltLab Suite 3.0: 1 2 3 4 5 6 7 < section class = \"box\" > < h2 class = \"boxTitle\" > < div class = \"boxContent\" > ","title":"Sidebars"},{"location":"migration/wcf21/templates/#forms","text":"The input tag for session ids SID_INPUT_TAG has been deprecated and no longer yields any content, it can be safely removed. In previous versions forms have been wrapped in
    \u2026
    which no longer has any effect and should be removed. If you're using the preview feature for WYSIWYG-powered input fields, you need to alter the preview button include instruction: 1 { include file = 'messageFormPreviewButton' previewMessageObjectType = 'com.example.foo.bar' previewMessageObjectID = 0 } The message object id should be non-zero when editing.","title":"Forms"},{"location":"migration/wcf21/templates/#icons","text":"The old .icon- classes have been removed, you are required to use the official .fa- class names from FontAwesome. This does not affect the generic classes .icon (indicates an icon) and .icon (e.g. .icon16 that sets the dimensions), these are still required and have not been deprecated. Before: 1 < span class = \"icon icon16 icon-list\" > Now: 1 < span class = \"icon icon16 fa-list\" >","title":"Icons"},{"location":"migration/wcf21/templates/#changed-icon-names","text":"Quite a few icon names have been renamed, the official wiki lists the new icon names in FontAwesome 4.","title":"Changed Icon Names"},{"location":"migration/wcf21/templates/#changed-classes","text":".dataList has been replaced and should now read
      (same applies to
        ) .framedIconList has been changed into .userAvatarList","title":"Changed Classes"},{"location":"migration/wcf21/templates/#removed-elements-and-classes","text":"
    , with impacts on native elements such as lists. You can now disable the class usage by defining your event as raw HTML: 1 2 3 4 5 Example Provider [a-zA-Z0-9])]]> ","title":"mediaProvider.xml"},{"location":"migration/wsc30/php/#php-callback","text":"The full match is provided for $url , while any capture groups from the regular expression are assigned to $matches . 1 2 3 4 5 6 objectList as $message ) { // ... if ( ! $message -> enableHtml ) { // ... } else { // OLD: $this -> getHtmlInputProcessor () -> processEmbeddedContent ( $message -> message , 'com.example.foo.message' , $message -> messageID ); // REPLACE WITH: $this -> getHtmlInputProcessor () -> reprocess ( $message -> message , 'com.example.foo.message' , $message -> messageID ); $data [ 'message' ] = $this -> getHtmlInputProcessor () -> getHtml (); } // ... }","title":"Example Usage"},{"location":"migration/wsc30/templates/","text":"Migrating from WSC 3.0 - Templates # Comment-System Overhaul # Unfortunately, there has been a breaking change related to the creation of comments. You need to apply the changes below before being able to create new comments. Adding Comments # Existing implementations need to include a new template right before including the generic commentList template. 1 2 3 4 < ul id = \"exampleCommentList\" class = \"commentList containerList\" data- ... > {include file='commentListAddComment' wysiwygSelector='exampleCommentListAddComment'} {include file='commentList'} Redesigned ACP User List # Custom interaction buttons were previously added through the template event rowButtons and were merely a link-like element with an icon inside. This is still valid and supported for backwards-compatibility, but it is recommend to adapt to the new drop-down-style options using the new template event dropdownItems . 1 2 3 4 5 < span class = \"icon icon16 fa-list jsTooltip\" title = \"Button Title\" > < li >< a href = \"#\" class = \"jsMyButton\" > Button Title Sidebar Toogle-Buttons on Mobile Device # You cannot override the button label for sidebars containing navigation menus. The page sidebars are automatically collapsed and presented as one or, when both sidebar are present, two condensed buttons. They use generic sidebar-related labels when open or closed, with the exception of embedded menus which will change the button label to read \"Show/Hide Navigation\". You can provide a custom label before including the sidebars by assigning the new labels to a few special variables: 1 2 3 4 {assign var='__sidebarLeftShow' value='Show Left Sidebar'} {assign var='__sidebarLeftHide' value='Hide Left Sidebar'} {assign var='__sidebarRightShow' value='Show Right Sidebar'} {assign var='__sidebarRightHide' value='Hide Right Sidebar'}","title":"Templates"},{"location":"migration/wsc30/templates/#migrating-from-wsc-30-templates","text":"","title":"Migrating from WSC 3.0 - Templates"},{"location":"migration/wsc30/templates/#comment-system-overhaul","text":"Unfortunately, there has been a breaking change related to the creation of comments. You need to apply the changes below before being able to create new comments.","title":"Comment-System Overhaul"},{"location":"migration/wsc30/templates/#adding-comments","text":"Existing implementations need to include a new template right before including the generic commentList template. 1 2 3 4 < ul id = \"exampleCommentList\" class = \"commentList containerList\" data- ... > {include file='commentListAddComment' wysiwygSelector='exampleCommentListAddComment'} {include file='commentList'} ","title":"Adding Comments"},{"location":"migration/wsc30/templates/#redesigned-acp-user-list","text":"Custom interaction buttons were previously added through the template event rowButtons and were merely a link-like element with an icon inside. This is still valid and supported for backwards-compatibility, but it is recommend to adapt to the new drop-down-style options using the new template event dropdownItems . 1 2 3 4 5 < span class = \"icon icon16 fa-list jsTooltip\" title = \"Button Title\" > < li >< a href = \"#\" class = \"jsMyButton\" > Button Title ","title":"Redesigned ACP User List"},{"location":"migration/wsc30/templates/#sidebar-toogle-buttons-on-mobile-device","text":"You cannot override the button label for sidebars containing navigation menus. The page sidebars are automatically collapsed and presented as one or, when both sidebar are present, two condensed buttons. They use generic sidebar-related labels when open or closed, with the exception of embedded menus which will change the button label to read \"Show/Hide Navigation\". You can provide a custom label before including the sidebars by assigning the new labels to a few special variables: 1 2 3 4 {assign var='__sidebarLeftShow' value='Show Left Sidebar'} {assign var='__sidebarLeftHide' value='Hide Left Sidebar'} {assign var='__sidebarRightShow' value='Show Right Sidebar'} {assign var='__sidebarRightHide' value='Hide Right Sidebar'}","title":"Sidebar Toogle-Buttons on Mobile Device"},{"location":"migration/wsc31/form-builder/","text":"Migrating from WSC 3.1 - Form Builder # Example: Two Text Form Fields # As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the first part of the tutorial series will be updated to the new form builder API. This form is the perfect first examples as it is very simple with only two text fields whose only restriction is that they have to be filled out and that their values may not be longer than 255 characters each. As a reminder, here are the two relevant PHP files and the relevant template file: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonAddForm extends AbstractForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person.add' ; /** * first name of the person * @var string */ public $firstName = '' ; /** * last name of the person * @var string */ public $lastName = '' ; /** * @inheritDoc */ public $neededPermissions = [ 'admin.content.canManagePeople' ]; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'action' => 'add' , 'firstName' => $this -> firstName , 'lastName' => $this -> lastName ]); } /** * @inheritDoc */ public function readFormParameters () { parent :: readFormParameters (); if ( isset ( $_POST [ 'firstName' ])) $this -> firstName = StringUtil :: trim ( $_POST [ 'firstName' ]); if ( isset ( $_POST [ 'lastName' ])) $this -> lastName = StringUtil :: trim ( $_POST [ 'lastName' ]); } /** * @inheritDoc */ public function save () { parent :: save (); $this -> objectAction = new PersonAction ([], 'create' , [ 'data' => array_merge ( $this -> additionalFields , [ 'firstName' => $this -> firstName , 'lastName' => $this -> lastName ]) ]); $this -> objectAction -> executeAction (); $this -> saved (); // reset values $this -> firstName = '' ; $this -> lastName = '' ; // show success message WCF :: getTPL () -> assign ( 'success' , true ); } /** * @inheritDoc */ public function validate () { parent :: validate (); // validate first name if ( empty ( $this -> firstName )) { throw new UserInputException ( 'firstName' ); } if ( mb_strlen ( $this -> firstName ) > 255 ) { throw new UserInputException ( 'firstName' , 'tooLong' ); } // validate last name if ( empty ( $this -> lastName )) { throw new UserInputException ( 'lastName' ); } if ( mb_strlen ( $this -> lastName ) > 255 ) { throw new UserInputException ( 'lastName' , 'tooLong' ); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonEditForm extends PersonAddForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person' ; /** * edited person object * @var Person */ public $person = null ; /** * id of the edited person * @var integer */ public $personID = 0 ; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'action' => 'edit' , 'person' => $this -> person ]); } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( empty ( $_POST )) { $this -> firstName = $this -> person -> firstName ; $this -> lastName = $this -> person -> lastName ; } } /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) $this -> personID = intval ( $_REQUEST [ 'id' ]); $this -> person = new Person ( $this -> personID ); if ( ! $this -> person -> personID ) { throw new IllegalLinkException (); } } /** * @inheritDoc */ public function save () { AbstractForm :: save (); $this -> objectAction = new PersonAction ([ $this -> person ], 'update' , [ 'data' => array_merge ( $this -> additionalFields , [ 'firstName' => $this -> firstName , 'lastName' => $this -> lastName ]) ]); $this -> objectAction -> executeAction (); $this -> saved (); // show success message WCF :: getTPL () -> assign ( 'success' , true ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 { include file = 'header' pageTitle = 'wcf.acp.person.' | concat : $action } < header class = \"contentHeader\" > < div class = \"contentHeaderTitle\" > < h1 class = \"contentTitle\" > { lang } wcf . acp . person . { $action }{ / lang } < nav class = \"contentHeaderNavigation\" > < ul > < li >< a href = \"{link controller='PersonList'}{/link}\" class = \"button\" >< span class = \"icon icon16 fa-list\" > < span > { lang } wcf . acp . menu . link . person . list { / lang } { event name = 'contentHeaderNavigation' } { include file = 'formError' } { if $success | isset } < p class = \"success\" > { lang } wcf . global . success . { $action }{ / lang } { / if } < form method = \"post\" action = \"{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object= $person }{/link}{/if}\" > < div class = \"section\" > < dl { if $errorField == 'firstName' } class = \"formError\" { / if } > < dt >< label for = \"firstName\" > { lang } wcf . person . firstName { / lang } < dd > < input type = \"text\" id = \"firstName\" name = \"firstName\" value = \" { $firstName } \" required autofocus maxlength = \"255\" class = \"long\" > { if $errorField == 'firstName' } < small class = \"innerError\" > { if $errorType == 'empty' } { lang } wcf . global . form . error . empty { / lang } { else } { lang } wcf . acp . person . firstName . error . { $errorType }{ / lang } { / if } { / if } < dl { if $errorField == 'lastName' } class = \"formError\" { / if } > < dt >< label for = \"lastName\" > { lang } wcf . person . lastName { / lang } < dd > < input type = \"text\" id = \"lastName\" name = \"lastName\" value = \" { $lastName } \" required maxlength = \"255\" class = \"long\" > { if $errorField == 'lastName' } < small class = \"innerError\" > { if $errorType == 'empty' } { lang } wcf . global . form . error . empty { / lang } { else } { lang } wcf . acp . person . lastName . error . { $errorType }{ / lang } { / if } { / if } { event name = 'dataFields' } { event name = 'sections' } < div class = \"formSubmit\" > < input type = \"submit\" value = \"{lang}wcf.global.button.submit{/lang}\" accesskey = \"s\" > { @ SECURITY_TOKEN_INPUT_TAG } { include file = 'footer' } Updating the template is easy as the complete form is replace by a single line of code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { include file = 'header' pageTitle = 'wcf.acp.person.' | concat : $action } < header class = \"contentHeader\" > < div class = \"contentHeaderTitle\" > < h1 class = \"contentTitle\" > { lang } wcf . acp . person . { $action }{ / lang } < nav class = \"contentHeaderNavigation\" > < ul > < li >< a href = \"{link controller='PersonList'}{/link}\" class = \"button\" >< span class = \"icon icon16 fa-list\" > < span > { lang } wcf . acp . menu . link . person . list { / lang } { event name = 'contentHeaderNavigation' } { @ $form -> getHtml ()} { include file = 'footer' } PersonEditForm also becomes much simpler: only the edited Person object must be read: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonEditForm extends PersonAddForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person' ; /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) { $this -> formObject = new Person ( intval ( $_REQUEST [ 'id' ])); if ( ! $this -> formObject -> personID ) { throw new IllegalLinkException (); } } } } Most of the work is done in PersonAddForm : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonAddForm extends AbstractFormBuilderForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person.add' ; /** * @inheritDoc */ public $formAction = 'create' ; /** * @inheritDoc */ public $neededPermissions = [ 'admin.content.canManagePeople' ]; /** * @inheritDoc */ public $objectActionClass = PersonAction :: class ; /** * @inheritDoc */ protected function createForm () { parent :: createForm (); $dataContainer = FormContainer :: create ( 'data' ) -> appendChildren ([ TextFormField :: create ( 'firstName' ) -> label ( 'wcf.person.firstName' ) -> required () -> maximumLength ( 255 ), TextFormField :: create ( 'lastName' ) -> label ( 'wcf.person.lastName' ) -> required () -> maximumLength ( 255 ) ]); $this -> form -> appendChild ( $dataContainer ); } } But, as you can see, the number of lines almost decreased by half. All changes are due to extending AbstractFormBuilderForm : $formAction is added and set to create as the form is used to create a new person. In the edit form, $formAction has not to be set explicitly as it is done automatically if a $formObject is set. $objectActionClass is set to PersonAction::class and is the class name of the used AbstractForm::$objectAction object to create and update the Person object. AbstractFormBuilderForm::createForm() is overridden and the form contents are added: a form container representing the div.section element from the old version and the two form fields with the same ids and labels as before. The contents of the old validate() method is put into two method calls: required() to ensure that the form is filled out and maximumLength(255) to ensure that the names are not longer than 255 characters.","title":"Migrating from WSC 3.1 - Form Builder"},{"location":"migration/wsc31/form-builder/#migrating-from-wsc-31-form-builder","text":"","title":"Migrating from WSC 3.1 - Form Builder"},{"location":"migration/wsc31/form-builder/#example-two-text-form-fields","text":"As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the first part of the tutorial series will be updated to the new form builder API. This form is the perfect first examples as it is very simple with only two text fields whose only restriction is that they have to be filled out and that their values may not be longer than 255 characters each. As a reminder, here are the two relevant PHP files and the relevant template file: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonAddForm extends AbstractForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person.add' ; /** * first name of the person * @var string */ public $firstName = '' ; /** * last name of the person * @var string */ public $lastName = '' ; /** * @inheritDoc */ public $neededPermissions = [ 'admin.content.canManagePeople' ]; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'action' => 'add' , 'firstName' => $this -> firstName , 'lastName' => $this -> lastName ]); } /** * @inheritDoc */ public function readFormParameters () { parent :: readFormParameters (); if ( isset ( $_POST [ 'firstName' ])) $this -> firstName = StringUtil :: trim ( $_POST [ 'firstName' ]); if ( isset ( $_POST [ 'lastName' ])) $this -> lastName = StringUtil :: trim ( $_POST [ 'lastName' ]); } /** * @inheritDoc */ public function save () { parent :: save (); $this -> objectAction = new PersonAction ([], 'create' , [ 'data' => array_merge ( $this -> additionalFields , [ 'firstName' => $this -> firstName , 'lastName' => $this -> lastName ]) ]); $this -> objectAction -> executeAction (); $this -> saved (); // reset values $this -> firstName = '' ; $this -> lastName = '' ; // show success message WCF :: getTPL () -> assign ( 'success' , true ); } /** * @inheritDoc */ public function validate () { parent :: validate (); // validate first name if ( empty ( $this -> firstName )) { throw new UserInputException ( 'firstName' ); } if ( mb_strlen ( $this -> firstName ) > 255 ) { throw new UserInputException ( 'firstName' , 'tooLong' ); } // validate last name if ( empty ( $this -> lastName )) { throw new UserInputException ( 'lastName' ); } if ( mb_strlen ( $this -> lastName ) > 255 ) { throw new UserInputException ( 'lastName' , 'tooLong' ); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonEditForm extends PersonAddForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person' ; /** * edited person object * @var Person */ public $person = null ; /** * id of the edited person * @var integer */ public $personID = 0 ; /** * @inheritDoc */ public function assignVariables () { parent :: assignVariables (); WCF :: getTPL () -> assign ([ 'action' => 'edit' , 'person' => $this -> person ]); } /** * @inheritDoc */ public function readData () { parent :: readData (); if ( empty ( $_POST )) { $this -> firstName = $this -> person -> firstName ; $this -> lastName = $this -> person -> lastName ; } } /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) $this -> personID = intval ( $_REQUEST [ 'id' ]); $this -> person = new Person ( $this -> personID ); if ( ! $this -> person -> personID ) { throw new IllegalLinkException (); } } /** * @inheritDoc */ public function save () { AbstractForm :: save (); $this -> objectAction = new PersonAction ([ $this -> person ], 'update' , [ 'data' => array_merge ( $this -> additionalFields , [ 'firstName' => $this -> firstName , 'lastName' => $this -> lastName ]) ]); $this -> objectAction -> executeAction (); $this -> saved (); // show success message WCF :: getTPL () -> assign ( 'success' , true ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 { include file = 'header' pageTitle = 'wcf.acp.person.' | concat : $action } < header class = \"contentHeader\" > < div class = \"contentHeaderTitle\" > < h1 class = \"contentTitle\" > { lang } wcf . acp . person . { $action }{ / lang } < nav class = \"contentHeaderNavigation\" > < ul > < li >< a href = \"{link controller='PersonList'}{/link}\" class = \"button\" >< span class = \"icon icon16 fa-list\" > < span > { lang } wcf . acp . menu . link . person . list { / lang } { event name = 'contentHeaderNavigation' } { include file = 'formError' } { if $success | isset } < p class = \"success\" > { lang } wcf . global . success . { $action }{ / lang } { / if } < form method = \"post\" action = \"{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object= $person }{/link}{/if}\" > < div class = \"section\" > < dl { if $errorField == 'firstName' } class = \"formError\" { / if } > < dt >< label for = \"firstName\" > { lang } wcf . person . firstName { / lang } < dd > < input type = \"text\" id = \"firstName\" name = \"firstName\" value = \" { $firstName } \" required autofocus maxlength = \"255\" class = \"long\" > { if $errorField == 'firstName' } < small class = \"innerError\" > { if $errorType == 'empty' } { lang } wcf . global . form . error . empty { / lang } { else } { lang } wcf . acp . person . firstName . error . { $errorType }{ / lang } { / if } { / if } < dl { if $errorField == 'lastName' } class = \"formError\" { / if } > < dt >< label for = \"lastName\" > { lang } wcf . person . lastName { / lang } < dd > < input type = \"text\" id = \"lastName\" name = \"lastName\" value = \" { $lastName } \" required maxlength = \"255\" class = \"long\" > { if $errorField == 'lastName' } < small class = \"innerError\" > { if $errorType == 'empty' } { lang } wcf . global . form . error . empty { / lang } { else } { lang } wcf . acp . person . lastName . error . { $errorType }{ / lang } { / if } { / if } { event name = 'dataFields' } { event name = 'sections' } < div class = \"formSubmit\" > < input type = \"submit\" value = \"{lang}wcf.global.button.submit{/lang}\" accesskey = \"s\" > { @ SECURITY_TOKEN_INPUT_TAG } { include file = 'footer' } Updating the template is easy as the complete form is replace by a single line of code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { include file = 'header' pageTitle = 'wcf.acp.person.' | concat : $action } < header class = \"contentHeader\" > < div class = \"contentHeaderTitle\" > < h1 class = \"contentTitle\" > { lang } wcf . acp . person . { $action }{ / lang } < nav class = \"contentHeaderNavigation\" > < ul > < li >< a href = \"{link controller='PersonList'}{/link}\" class = \"button\" >< span class = \"icon icon16 fa-list\" > < span > { lang } wcf . acp . menu . link . person . list { / lang } { event name = 'contentHeaderNavigation' } { @ $form -> getHtml ()} { include file = 'footer' } PersonEditForm also becomes much simpler: only the edited Person object must be read: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonEditForm extends PersonAddForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person' ; /** * @inheritDoc */ public function readParameters () { parent :: readParameters (); if ( isset ( $_REQUEST [ 'id' ])) { $this -> formObject = new Person ( intval ( $_REQUEST [ 'id' ])); if ( ! $this -> formObject -> personID ) { throw new IllegalLinkException (); } } } } Most of the work is done in PersonAddForm : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 * @package WoltLabSuite\\Core\\Acp\\Form */ class PersonAddForm extends AbstractFormBuilderForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.person.add' ; /** * @inheritDoc */ public $formAction = 'create' ; /** * @inheritDoc */ public $neededPermissions = [ 'admin.content.canManagePeople' ]; /** * @inheritDoc */ public $objectActionClass = PersonAction :: class ; /** * @inheritDoc */ protected function createForm () { parent :: createForm (); $dataContainer = FormContainer :: create ( 'data' ) -> appendChildren ([ TextFormField :: create ( 'firstName' ) -> label ( 'wcf.person.firstName' ) -> required () -> maximumLength ( 255 ), TextFormField :: create ( 'lastName' ) -> label ( 'wcf.person.lastName' ) -> required () -> maximumLength ( 255 ) ]); $this -> form -> appendChild ( $dataContainer ); } } But, as you can see, the number of lines almost decreased by half. All changes are due to extending AbstractFormBuilderForm : $formAction is added and set to create as the form is used to create a new person. In the edit form, $formAction has not to be set explicitly as it is done automatically if a $formObject is set. $objectActionClass is set to PersonAction::class and is the class name of the used AbstractForm::$objectAction object to create and update the Person object. AbstractFormBuilderForm::createForm() is overridden and the form contents are added: a form container representing the div.section element from the old version and the two form fields with the same ids and labels as before. The contents of the old validate() method is put into two method calls: required() to ensure that the form is filled out and maximumLength(255) to ensure that the names are not longer than 255 characters.","title":"Example: Two Text Form Fields"},{"location":"migration/wsc31/like/","text":"Migrating from WSC 3.1 - Like System # Introduction # With version 5.2 of WoltLab Suite Core the like system was completely replaced by the new reactions system. This makes it necessary to make some adjustments to existing code so that your plugin integrates completely into the new system. However, we have kept these adjustments as small as possible so that it is possible to use the reaction system with slight restrictions even without adjustments. Limitations if no adjustments are made to the existing code # If no adjustments are made to the existing code, the following functions are not available: * Notifications about reactions/likes * Recent Activity Events for reactions/likes Migration # Notifications # Mark notification as compatible # Since there are no more likes with the new version, it makes no sense to send notifications about it. Instead of notifications about likes, notifications about reactions are now sent. However, this only changes the notification text and not the notification itself. To update the notification, we first add the interface \\wcf\\data\\reaction\\object\\IReactionObject to the \\wcf\\data\\like\\object\\ILikeObject object (e.g. in WoltLab Suite Forum we added the interface to the class \\wbb\\data\\post\\LikeablePost ). After that the object is marked as \"compatible with WoltLab Suite Core 5.2\" and notifications about reactions are sent again. Language Variables # Next, to display all reactions for the current notification in the notification text, we include the trait \\wcf\\system\\user\\notification\\event\\TReactionUserNotificationEvent in the user notification event class (typically named like *LikeUserNotificationEvent ). These trait provides a new function that reads out and groups the reactions. The result of this function must now only be passed to the language variable. The name \"reactions\" is typically used as the variable name for the language variable. As a final step, we only need to change the language variables themselves. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core. English # {prefix}.like.title 1 Reaction to a {objectName} {prefix}.like.title.stacked 1 {#$count} users reacted to your {objectName} {prefix}.like.message 1 {@$author->getAnchorTag()} reacted to your {objectName} ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}\u00d7{#$count}{/implode}). {prefix}.like.message.stacked 1 {if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} and {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} and {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} and {#$others} others{/if} reacted to your {objectName} ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}\u00d7{#$count}{/implode}). wcf.user.notification.{objectTypeName}.like.notification.like 1 Notify me when someone reacted to my {objectName} German # {prefix}.like.title 1 Reaktion auf einen {objectName} {prefix}.like.title.stacked 1 {#$count} Benutzern haben auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert {prefix}.like.message 1 {@$author->getAnchorTag()} hat auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}\u00d7{#$count}{/implode}). {prefix}.like.message.stacked 1 {if $count < 4}{@$authors[0]->getAnchorTag()}{if $count == 2} und {else}, {/if}{@$authors[1]->getAnchorTag()}{if $count == 3} und {@$authors[2]->getAnchorTag()}{/if}{else}{@$authors[0]->getAnchorTag()} und {#$others} weitere{/if} haben auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert ({implode from=$reactions key=reactionID item=count}{@$__wcf->getReactionHandler()->getReactionTypeByID($reactionID)->renderIcon()}\u00d7{#$count}{/implode}). wcf.user.notification.{object_type_name}.like.notification.like 1 Jemandem hat auf {if LANGUAGE_USE_INFORMAL_VARIANT}dein(en){else}Ihr(en){/if} {objectName} reagiert Recent Activity # To adjust entries in the Recent Activity, only three small steps are necessary. First we pass the concrete reaction to the language variable, so that we can use the reaction object there. To do this, we add the following variable to the text of the \\wcf\\system\\user\\activity\\event\\IUserActivityEvent object: $event->reactionType . Typically we name the variable reactionType . In the second step, we mark the event as compatible. Therefore we set the parameter supportsReactions in the objectType.xml to 1 . So for example the entry looks like this: 1 2 3 4 5 6 com.woltlab.example.likeableObject.recentActivityEvent com.woltlab.wcf.user.recentActivityEvent wcf\\system\\user\\activity\\event\\LikeableObjectUserActivityEvent 1 Finally we modify our language variable. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core. English # wcf.user.recentActivity.{object_type_name}.recentActivityEvent 1 Reaction ({objectName}) Your language variable for the recent activity text 1 Reacted with getTitle()}\" class=\"jsTooltip\">{@$reactionType->renderIcon()} to the {objectName}. German # wcf.user.recentActivity.{objectTypeName}.recentActivityEvent 1 Reaktion ({objectName}) Your language variable for the recent activity text 1 Hat mit getTitle()}\" class=\"jsTooltip\">{@$reactionType->renderIcon()} auf {objectName} reagiert. Comments # If comments send notifications, they must also be updated. The language variables are changed in the same way as described in the section Notifications / Language . After that comment must be marked as compatible. Therefore we set the parameter supportsReactions in the objectType.xml to 1 . So for example the entry looks like this: 1 2 3 4 5 6 7 com.woltlab.wcf.objectComment.response.like.notification com.woltlab.wcf.notification.objectType wcf\\system\\user\\notification\\object\\type\\LikeUserNotificationObjectType com.woltlab.example 1 Forward Compatibility # So that these changes also work in older versions of WoltLab Suite Core, the used classes and traits were backported with WoltLab Suite Core 3.0.22 and WoltLab Suite Core 3.1.10.","title":"Migrating from WSC 3.1 - Like System"},{"location":"migration/wsc31/like/#migrating-from-wsc-31-like-system","text":"","title":"Migrating from WSC 3.1 - Like System"},{"location":"migration/wsc31/like/#introduction","text":"With version 5.2 of WoltLab Suite Core the like system was completely replaced by the new reactions system. This makes it necessary to make some adjustments to existing code so that your plugin integrates completely into the new system. However, we have kept these adjustments as small as possible so that it is possible to use the reaction system with slight restrictions even without adjustments.","title":"Introduction"},{"location":"migration/wsc31/like/#limitations-if-no-adjustments-are-made-to-the-existing-code","text":"If no adjustments are made to the existing code, the following functions are not available: * Notifications about reactions/likes * Recent Activity Events for reactions/likes","title":"Limitations if no adjustments are made to the existing code"},{"location":"migration/wsc31/like/#migration","text":"","title":"Migration"},{"location":"migration/wsc31/like/#notifications","text":"","title":"Notifications"},{"location":"migration/wsc31/like/#mark-notification-as-compatible","text":"Since there are no more likes with the new version, it makes no sense to send notifications about it. Instead of notifications about likes, notifications about reactions are now sent. However, this only changes the notification text and not the notification itself. To update the notification, we first add the interface \\wcf\\data\\reaction\\object\\IReactionObject to the \\wcf\\data\\like\\object\\ILikeObject object (e.g. in WoltLab Suite Forum we added the interface to the class \\wbb\\data\\post\\LikeablePost ). After that the object is marked as \"compatible with WoltLab Suite Core 5.2\" and notifications about reactions are sent again.","title":"Mark notification as compatible"},{"location":"migration/wsc31/like/#language-variables","text":"Next, to display all reactions for the current notification in the notification text, we include the trait \\wcf\\system\\user\\notification\\event\\TReactionUserNotificationEvent in the user notification event class (typically named like *LikeUserNotificationEvent ). These trait provides a new function that reads out and groups the reactions. The result of this function must now only be passed to the language variable. The name \"reactions\" is typically used as the variable name for the language variable. As a final step, we only need to change the language variables themselves. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.","title":"Language Variables"},{"location":"migration/wsc31/like/#recent-activity","text":"To adjust entries in the Recent Activity, only three small steps are necessary. First we pass the concrete reaction to the language variable, so that we can use the reaction object there. To do this, we add the following variable to the text of the \\wcf\\system\\user\\activity\\event\\IUserActivityEvent object: $event->reactionType . Typically we name the variable reactionType . In the second step, we mark the event as compatible. Therefore we set the parameter supportsReactions in the objectType.xml to 1 . So for example the entry looks like this: 1 2 3 4 5 6 com.woltlab.example.likeableObject.recentActivityEvent com.woltlab.wcf.user.recentActivityEvent wcf\\system\\user\\activity\\event\\LikeableObjectUserActivityEvent 1 Finally we modify our language variable. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.","title":"Recent Activity"},{"location":"migration/wsc31/like/#english_1","text":"wcf.user.recentActivity.{object_type_name}.recentActivityEvent 1 Reaction ({objectName}) Your language variable for the recent activity text 1 Reacted with getTitle()}\" class=\"jsTooltip\">{@$reactionType->renderIcon()} to the {objectName}.","title":"English"},{"location":"migration/wsc31/like/#german_1","text":"wcf.user.recentActivity.{objectTypeName}.recentActivityEvent 1 Reaktion ({objectName}) Your language variable for the recent activity text 1 Hat mit getTitle()}\" class=\"jsTooltip\">{@$reactionType->renderIcon()} auf {objectName} reagiert.","title":"German"},{"location":"migration/wsc31/like/#comments","text":"If comments send notifications, they must also be updated. The language variables are changed in the same way as described in the section Notifications / Language . After that comment must be marked as compatible. Therefore we set the parameter supportsReactions in the objectType.xml to 1 . So for example the entry looks like this: 1 2 3 4 5 6 7 com.woltlab.wcf.objectComment.response.like.notification com.woltlab.wcf.notification.objectType wcf\\system\\user\\notification\\object\\type\\LikeUserNotificationObjectType com.woltlab.example 1 ","title":"Comments"},{"location":"migration/wsc31/like/#forward-compatibility","text":"So that these changes also work in older versions of WoltLab Suite Core, the used classes and traits were backported with WoltLab Suite Core 3.0.22 and WoltLab Suite Core 3.1.10.","title":"Forward Compatibility"},{"location":"migration/wsc31/php/","text":"Migrating from WSC 3.1 - PHP # Form Builder # WoltLab Suite Core 5.2 introduces a new, simpler and quicker way of creating forms: form builder . You can find examples of how to migrate existing forms to form builder here . In the near future, to ensure backwards compatibility within WoltLab packages, we will only use form builder for new forms or for major rewrites of existing forms that would break backwards compatibility anyway. Like System # WoltLab Suite Core 5.2 replaced the like system with the reaction system. You can find the migration guide here . User Content Providers # User content providers help the WoltLab Suite to find user generated content. They provide a class with which you can find content from a particular user and delete objects. PHP Class # First, we create the PHP class that provides our interface to provide the data. The class must implement interface wcf\\system\\user\\content\\provider\\IUserContentProvider in any case. Mostly we process data which is based on wcf\\data\\DatabaseObject . In this case, the WoltLab Suite provides an abstract class wcf\\system\\user\\content\\provider\\AbstractDatabaseUserContentProvider that can be used to automatically generates the standardized classes to generate the list and deletes objects via the DatabaseObjectAction. For example, if we would create a content provider for comments, the class would look like this: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\User\\Content\\Provider * @since 5.2 */ class CommentUserContentProvider extends AbstractDatabaseUserContentProvider { /** * @inheritdoc */ public static function getDatabaseObjectClass () { return Comment :: class ; } } Object Type # Now the appropriate object type must be created for the class. This object type must be from the definition com.woltlab.wcf.content.userContentProvider and include the previous created class as FQN in the parameter classname . Also the following parameters can be used in the object type: nicevalue # Optional The nice value is used to determine the order in which the remove content worker are execute the provider. Content provider with lower nice values are executed first. hidden # Optional Specifies whether or not this content provider can be actively selected in the Content Remove Worker. If it cannot be selected, it will not be executed automatically! requiredobjecttype # Optional The specified list of comma-separated object types are automatically removed during content removal when this object type is being removed. Attention : The order of removal is undefined by default, specify a nicevalue if the order is important. PHP Database API # WoltLab Suite 5.2 introduces a new way to update the database scheme: database PHP API .","title":"PHP API"},{"location":"migration/wsc31/php/#migrating-from-wsc-31-php","text":"","title":"Migrating from WSC 3.1 - PHP"},{"location":"migration/wsc31/php/#form-builder","text":"WoltLab Suite Core 5.2 introduces a new, simpler and quicker way of creating forms: form builder . You can find examples of how to migrate existing forms to form builder here . In the near future, to ensure backwards compatibility within WoltLab packages, we will only use form builder for new forms or for major rewrites of existing forms that would break backwards compatibility anyway.","title":"Form Builder"},{"location":"migration/wsc31/php/#like-system","text":"WoltLab Suite Core 5.2 replaced the like system with the reaction system. You can find the migration guide here .","title":"Like System"},{"location":"migration/wsc31/php/#user-content-providers","text":"User content providers help the WoltLab Suite to find user generated content. They provide a class with which you can find content from a particular user and delete objects.","title":"User Content Providers"},{"location":"migration/wsc31/php/#php-class","text":"First, we create the PHP class that provides our interface to provide the data. The class must implement interface wcf\\system\\user\\content\\provider\\IUserContentProvider in any case. Mostly we process data which is based on wcf\\data\\DatabaseObject . In this case, the WoltLab Suite provides an abstract class wcf\\system\\user\\content\\provider\\AbstractDatabaseUserContentProvider that can be used to automatically generates the standardized classes to generate the list and deletes objects via the DatabaseObjectAction. For example, if we would create a content provider for comments, the class would look like this: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * @package WoltLabSuite\\Core\\System\\User\\Content\\Provider * @since 5.2 */ class CommentUserContentProvider extends AbstractDatabaseUserContentProvider { /** * @inheritdoc */ public static function getDatabaseObjectClass () { return Comment :: class ; } }","title":"PHP Class"},{"location":"migration/wsc31/php/#object-type","text":"Now the appropriate object type must be created for the class. This object type must be from the definition com.woltlab.wcf.content.userContentProvider and include the previous created class as FQN in the parameter classname . Also the following parameters can be used in the object type:","title":"Object Type"},{"location":"migration/wsc31/php/#nicevalue","text":"Optional The nice value is used to determine the order in which the remove content worker are execute the provider. Content provider with lower nice values are executed first.","title":"nicevalue"},{"location":"migration/wsc31/php/#hidden","text":"Optional Specifies whether or not this content provider can be actively selected in the Content Remove Worker. If it cannot be selected, it will not be executed automatically!","title":"hidden"},{"location":"migration/wsc31/php/#requiredobjecttype","text":"Optional The specified list of comma-separated object types are automatically removed during content removal when this object type is being removed. Attention : The order of removal is undefined by default, specify a nicevalue if the order is important.","title":"requiredobjecttype"},{"location":"migration/wsc31/php/#php-database-api","text":"WoltLab Suite 5.2 introduces a new way to update the database scheme: database PHP API .","title":"PHP Database API"},{"location":"migration/wsc52/libraries/","text":"Migrating from WSC 5.2 - Third Party Libraries # SCSS Compiler # WoltLab Suite Core 5.3 upgrades the bundled SCSS compiler from leafo/scssphp 0.7.x to scssphp/scssphp 1.1.x. With the updated composer package name the SCSS compiler also received updated namespaces. WoltLab Suite Core adds a compatibility layer that maps the old namespace to the new namespace. The classes themselves appear to be drop-in compatible. Exceptions cannot be mapped using this compatibility layer, any catch blocks catching a specific Exception within the Leafo namespace will need to be adjusted. More details can be found in the Pull Request WoltLab/WCF#3415 . Guzzle # WoltLab Suite Core 5.3 ships with a bundled version of Guzzle 6 . Going forward using Guzzle is the recommended way to perform HTTP requests. The \\wcf\\util\\HTTPRequest class should no longer be used and transparently uses Guzzle under the hood. Use \\wcf\\system\\io\\HttpFactory to retrieve a correctly configured GuzzleHttp\\ClientInterface . Please note that it is recommended to explicitely specify a sink when making requests, due to a PHP / Guzzle bug. Have a look at the implementation in WoltLab/WCF for an example.","title":"Third Party Libraries"},{"location":"migration/wsc52/libraries/#migrating-from-wsc-52-third-party-libraries","text":"","title":"Migrating from WSC 5.2 - Third Party Libraries"},{"location":"migration/wsc52/libraries/#scss-compiler","text":"WoltLab Suite Core 5.3 upgrades the bundled SCSS compiler from leafo/scssphp 0.7.x to scssphp/scssphp 1.1.x. With the updated composer package name the SCSS compiler also received updated namespaces. WoltLab Suite Core adds a compatibility layer that maps the old namespace to the new namespace. The classes themselves appear to be drop-in compatible. Exceptions cannot be mapped using this compatibility layer, any catch blocks catching a specific Exception within the Leafo namespace will need to be adjusted. More details can be found in the Pull Request WoltLab/WCF#3415 .","title":"SCSS Compiler"},{"location":"migration/wsc52/libraries/#guzzle","text":"WoltLab Suite Core 5.3 ships with a bundled version of Guzzle 6 . Going forward using Guzzle is the recommended way to perform HTTP requests. The \\wcf\\util\\HTTPRequest class should no longer be used and transparently uses Guzzle under the hood. Use \\wcf\\system\\io\\HttpFactory to retrieve a correctly configured GuzzleHttp\\ClientInterface . Please note that it is recommended to explicitely specify a sink when making requests, due to a PHP / Guzzle bug. Have a look at the implementation in WoltLab/WCF for an example.","title":"Guzzle"},{"location":"migration/wsc52/php/","text":"Migrating from WSC 5.2 - PHP # Comments # The ICommentManager::isContentAuthor(Comment|CommentResponse): bool method was added. A default implementation that always returns false is available when inheriting from AbstractCommentManager . It is strongly recommended to implement isContentAuthor within your custom comment manager. An example implementation can be found in ArticleCommentManager . Event Listeners # The AbstractEventListener class was added. AbstractEventListener contains an implementation of execute() that will dispatch the event handling to dedicated methods based on the $eventName and, in case of the event object being an AbstractDatabaseObjectAction , the action name. Find the details of the dispatch behavior within the class comment of AbstractEventListener . Email Activation # Starting with WoltLab Suite 5.3 the user activation status is independent of the email activation status. A user can be activated even though their email address has not been confirmed, preventing emails being sent to these users. Going forward the new User::isEmailConfirmed() method should be used to check whether sending automated emails to this user is acceptable. If you need to check the user's activation status you should use the new method User::pendingActivation() instead of relying on activationCode . To check, which type of activation is missing, you can use the new methods User::requiresEmailActivation() and User::requiresAdminActivation() . *AddForm # WoltLab Suite 5.3 provides a new framework to allow the administrator to easily edit newly created objects by adding an edit link to the success message. To support this edit link two small changes are required within your *AddForm . Update the template. Replace: 1 2 3 4 5 { include file = 'formError' } { if $success | isset }

    { lang } wcf.global.success. { $action }{ /lang }

    { /if } With: 1 { include file = 'formNotice' } Expose objectEditLink to the template. Example ( $object being the newly created object): 1 2 3 4 WCF :: getTPL () -> assign ([ 'success' => true , 'objectEditLink' => LinkHandler :: getInstance () -> getControllerLink ( ObjectEditForm :: class , [ 'id' => $object -> objectID ]), ]); User Generated Links # It is recommended by search engines to mark up links within user generated content using the rel=\"ugc\" attribute to indicate that they might be less trustworthy or spammy. WoltLab Suite 5.3 will automatically sets that attribute on external links during message output processing. Set the new HtmlOutputProcessor::$enableUgc property to false if the type of message is not user-generated content, but restricted to a set of trustworthy users. An example of such a type of message would be official news articles. If you manually generate links based off user input you need to specify the attribute yourself. The $isUgc attribute was added to StringUtil::getAnchorTag(string, string, bool, bool): string , allowing you to easily generate a correct anchor tag. If you need to specify additional HTML attributes for the anchor tag you can use the new StringUtil::getAnchorTagAttributes(string, bool): string method to generate the anchor attributes that are dependent on the target URL. Specifically the attributes returned are the class=\"externalURL\" attribute, the rel=\"\u2026\" attribute and the target=\"\u2026\" attribute. Within the template the {anchorAttributes} template plugin is newly available. Resource Management When Scaling Images # It was discovered that the code holds references to scaled image resources for an unnecessarily long time, taking up memory. This becomes especially apparent when multiple images are scaled within a loop, reusing the same variable name for consecutive images. Unless the destination variable is explicitely cleared before processing the next image up to two images will be stored in memory concurrently. This possibly causes the request to exceed the memory limit or ImageMagick's internal resource limits, even if sufficient resources would have been available to scale the current image. Starting with WoltLab Suite 5.3 it is recommended to clear image handles as early as possible. The usual pattern of creating a thumbnail for an existing image would then look like this: 1 2 3 4 5 6 7 8 9 10 11 12 13 getAdapter (); $adapter -> loadFile ( $src ); $thumbnail = $adapter -> createThumbnail ( $size , $size , true ); $adapter -> writeImage ( $thumbnail , $destination ); // New: Clear thumbnail as soon as possible to free up the memory. $thumbnail = null ; } Refer to WoltLab/WCF#3505 for additional details. Toggle for Accelerated Mobile Pages (AMP) # Controllers delivering AMP versions of pages have to check for the new option MODULE_AMP and the templates of the non-AMP versions have to also check if the option is enabled before outputting the element.","title":"PHP API"},{"location":"migration/wsc52/php/#migrating-from-wsc-52-php","text":"","title":"Migrating from WSC 5.2 - PHP"},{"location":"migration/wsc52/php/#comments","text":"The ICommentManager::isContentAuthor(Comment|CommentResponse): bool method was added. A default implementation that always returns false is available when inheriting from AbstractCommentManager . It is strongly recommended to implement isContentAuthor within your custom comment manager. An example implementation can be found in ArticleCommentManager .","title":"Comments"},{"location":"migration/wsc52/php/#event-listeners","text":"The AbstractEventListener class was added. AbstractEventListener contains an implementation of execute() that will dispatch the event handling to dedicated methods based on the $eventName and, in case of the event object being an AbstractDatabaseObjectAction , the action name. Find the details of the dispatch behavior within the class comment of AbstractEventListener .","title":"Event Listeners"},{"location":"migration/wsc52/php/#email-activation","text":"Starting with WoltLab Suite 5.3 the user activation status is independent of the email activation status. A user can be activated even though their email address has not been confirmed, preventing emails being sent to these users. Going forward the new User::isEmailConfirmed() method should be used to check whether sending automated emails to this user is acceptable. If you need to check the user's activation status you should use the new method User::pendingActivation() instead of relying on activationCode . To check, which type of activation is missing, you can use the new methods User::requiresEmailActivation() and User::requiresAdminActivation() .","title":"Email Activation"},{"location":"migration/wsc52/php/#addform","text":"WoltLab Suite 5.3 provides a new framework to allow the administrator to easily edit newly created objects by adding an edit link to the success message. To support this edit link two small changes are required within your *AddForm . Update the template. Replace: 1 2 3 4 5 { include file = 'formError' } { if $success | isset }

    { lang } wcf.global.success. { $action }{ /lang }

    { /if } With: 1 { include file = 'formNotice' } Expose objectEditLink to the template. Example ( $object being the newly created object): 1 2 3 4 WCF :: getTPL () -> assign ([ 'success' => true , 'objectEditLink' => LinkHandler :: getInstance () -> getControllerLink ( ObjectEditForm :: class , [ 'id' => $object -> objectID ]), ]);","title":"*AddForm"},{"location":"migration/wsc52/php/#user-generated-links","text":"It is recommended by search engines to mark up links within user generated content using the rel=\"ugc\" attribute to indicate that they might be less trustworthy or spammy. WoltLab Suite 5.3 will automatically sets that attribute on external links during message output processing. Set the new HtmlOutputProcessor::$enableUgc property to false if the type of message is not user-generated content, but restricted to a set of trustworthy users. An example of such a type of message would be official news articles. If you manually generate links based off user input you need to specify the attribute yourself. The $isUgc attribute was added to StringUtil::getAnchorTag(string, string, bool, bool): string , allowing you to easily generate a correct anchor tag. If you need to specify additional HTML attributes for the anchor tag you can use the new StringUtil::getAnchorTagAttributes(string, bool): string method to generate the anchor attributes that are dependent on the target URL. Specifically the attributes returned are the class=\"externalURL\" attribute, the rel=\"\u2026\" attribute and the target=\"\u2026\" attribute. Within the template the {anchorAttributes} template plugin is newly available.","title":"User Generated Links"},{"location":"migration/wsc52/php/#resource-management-when-scaling-images","text":"It was discovered that the code holds references to scaled image resources for an unnecessarily long time, taking up memory. This becomes especially apparent when multiple images are scaled within a loop, reusing the same variable name for consecutive images. Unless the destination variable is explicitely cleared before processing the next image up to two images will be stored in memory concurrently. This possibly causes the request to exceed the memory limit or ImageMagick's internal resource limits, even if sufficient resources would have been available to scale the current image. Starting with WoltLab Suite 5.3 it is recommended to clear image handles as early as possible. The usual pattern of creating a thumbnail for an existing image would then look like this: 1 2 3 4 5 6 7 8 9 10 11 12 13 getAdapter (); $adapter -> loadFile ( $src ); $thumbnail = $adapter -> createThumbnail ( $size , $size , true ); $adapter -> writeImage ( $thumbnail , $destination ); // New: Clear thumbnail as soon as possible to free up the memory. $thumbnail = null ; } Refer to WoltLab/WCF#3505 for additional details.","title":"Resource Management When Scaling Images"},{"location":"migration/wsc52/php/#toggle-for-accelerated-mobile-pages-amp","text":"Controllers delivering AMP versions of pages have to check for the new option MODULE_AMP and the templates of the non-AMP versions have to also check if the option is enabled before outputting the element.","title":"Toggle for Accelerated Mobile Pages (AMP)"},{"location":"migration/wsc52/templates/","text":"Migrating from WSC 5.2 - Templates and Languages # {jslang} # Starting with WoltLab Suite 5.3 the {jslang} template plugin is available. {jslang} works like {lang} , with the difference that the result is automatically encoded for use within a single quoted JavaScript string. Before: 1 2 3 4 5 6 7 8 9 After: 1 2 3 4 5 6 7 8 9 Template Plugins # The {anchor} , {plural} , and {user} template plugins have been added. Notification Language Items # In addition to using the new template plugins mentioned above, language items for notifications have been further simplified. As the whole notification is clickable now, all a elements have been replaced with strong elements in notification messages. The template code to output reactions has been simplified by introducing helper methods: 1 2 3 4 5 6 7 8 9 { * old * } { implode from = $reactions key = reactionID item = count }{ @ $__wcf -> getReactionHandler ()-> getReactionTypeByID ( $reactionID )-> renderIcon () } \u00d7 { # $count }{ /implode } { * new * } { @ $__wcf -> getReactionHandler ()-> renderInlineList ( $reactions ) } { * old * } getReactionType ()-> getTitle () } \" class=\"jsTooltip\"> { @ $like -> getReactionType ()-> renderIcon () } { * new * } { @ $like -> render () } Similarly, showing labels is now also easier due to the new render method: 1 2 3 4 { * old * } getClassNames () } { $label -> getClassNames () }{ /if } \"> { $label -> getTitle () } { * new * } { @ $label -> render () } The commonly used template code 1 { if $count < 4 }{ @ $authors [ 0 ]-> getAnchorTag () }{ if $count != 1 }{ if $count == 2 && ! $guestTimesTriggered } and { else } , { /if }{ @ $authors [ 1 ]-> getAnchorTag () }{ if $count == 3 }{ if ! $guestTimesTriggered } and { else } , { /if } { @ $authors [ 2 ]-> getAnchorTag () }{ /if }{ /if }{ if $guestTimesTriggered } and { if $guestTimesTriggered == 1 } a guest { else } guests { /if }{ /if }{ else }{ @ $authors [ 0 ]-> getAnchorTag () }{ if $guestTimesTriggered } , { else } and { /if } { # $others } other users { if $guestTimesTriggered } and { if $guestTimesTriggered == 1 } a guest { else } guests { /if }{ /if }{ /if } in stacked notification messages can be replaced with a new language item: 1 { @ 'wcf.user.notification.stacked.authorList' | language } Popovers # Popovers provide additional information of the linked object when a user hovers over a link. We unified the approach for such links: The relevant DBO class implements wcf\\data\\IPopoverObject . The relevant DBO action class implements wcf\\data\\IPopoverAction and the getPopover() method returns an array with popover content. Globally available, WoltLabSuite/Core/Controller/Popover is initialized with the relevant data. Links are created with the anchor template plugin with an additional class attribute whose value is the return value of IPopoverObject::getPopoverLinkClass() . Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Foo extends DatabaseObject implements IPopoverObject { public function getPopoverLinkClass () { return 'fooLink' ; } } class FooAction extends AbstractDatabaseObjectAction implements IPopoverAction { public function validateGetPopover () { // \u2026 } public function getPopover () { return [ 'template' => '\u2026' , ]; } } 1 2 3 4 5 6 7 require ([ 'WoltLabSuite/Core/Controller/Popover' ], function ( ControllerPopover ) { ControllerPopover . init ({ className : 'fooLink' , dboAction : 'wcf\\\\data\\\\foo\\\\FooAction' , identifier : 'com.woltlab.wcf.foo' }); }); 1 { anchor object = $foo class = 'fooLink' }","title":"Templates and Languages"},{"location":"migration/wsc52/templates/#migrating-from-wsc-52-templates-and-languages","text":"","title":"Migrating from WSC 5.2 - Templates and Languages"},{"location":"migration/wsc52/templates/#jslang","text":"Starting with WoltLab Suite 5.3 the {jslang} template plugin is available. {jslang} works like {lang} , with the difference that the result is automatically encoded for use within a single quoted JavaScript string. Before: 1 2 3 4 5 6 7 8 9 After: 1 2 3 4 5 6 7 8 9 ","title":"{jslang}"},{"location":"migration/wsc52/templates/#template-plugins","text":"The {anchor} , {plural} , and {user} template plugins have been added.","title":"Template Plugins"},{"location":"migration/wsc52/templates/#notification-language-items","text":"In addition to using the new template plugins mentioned above, language items for notifications have been further simplified. As the whole notification is clickable now, all a elements have been replaced with strong elements in notification messages. The template code to output reactions has been simplified by introducing helper methods: 1 2 3 4 5 6 7 8 9 { * old * } { implode from = $reactions key = reactionID item = count }{ @ $__wcf -> getReactionHandler ()-> getReactionTypeByID ( $reactionID )-> renderIcon () } \u00d7 { # $count }{ /implode } { * new * } { @ $__wcf -> getReactionHandler ()-> renderInlineList ( $reactions ) } { * old * } getReactionType ()-> getTitle () } \" class=\"jsTooltip\"> { @ $like -> getReactionType ()-> renderIcon () } { * new * } { @ $like -> render () } Similarly, showing labels is now also easier due to the new render method: 1 2 3 4 { * old * } getClassNames () } { $label -> getClassNames () }{ /if } \"> { $label -> getTitle () } { * new * } { @ $label -> render () } The commonly used template code 1 { if $count < 4 }{ @ $authors [ 0 ]-> getAnchorTag () }{ if $count != 1 }{ if $count == 2 && ! $guestTimesTriggered } and { else } , { /if }{ @ $authors [ 1 ]-> getAnchorTag () }{ if $count == 3 }{ if ! $guestTimesTriggered } and { else } , { /if } { @ $authors [ 2 ]-> getAnchorTag () }{ /if }{ /if }{ if $guestTimesTriggered } and { if $guestTimesTriggered == 1 } a guest { else } guests { /if }{ /if }{ else }{ @ $authors [ 0 ]-> getAnchorTag () }{ if $guestTimesTriggered } , { else } and { /if } { # $others } other users { if $guestTimesTriggered } and { if $guestTimesTriggered == 1 } a guest { else } guests { /if }{ /if }{ /if } in stacked notification messages can be replaced with a new language item: 1 { @ 'wcf.user.notification.stacked.authorList' | language }","title":"Notification Language Items"},{"location":"migration/wsc52/templates/#popovers","text":"Popovers provide additional information of the linked object when a user hovers over a link. We unified the approach for such links: The relevant DBO class implements wcf\\data\\IPopoverObject . The relevant DBO action class implements wcf\\data\\IPopoverAction and the getPopover() method returns an array with popover content. Globally available, WoltLabSuite/Core/Controller/Popover is initialized with the relevant data. Links are created with the anchor template plugin with an additional class attribute whose value is the return value of IPopoverObject::getPopoverLinkClass() . Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Foo extends DatabaseObject implements IPopoverObject { public function getPopoverLinkClass () { return 'fooLink' ; } } class FooAction extends AbstractDatabaseObjectAction implements IPopoverAction { public function validateGetPopover () { // \u2026 } public function getPopover () { return [ 'template' => '\u2026' , ]; } } 1 2 3 4 5 6 7 require ([ 'WoltLabSuite/Core/Controller/Popover' ], function ( ControllerPopover ) { ControllerPopover . init ({ className : 'fooLink' , dboAction : 'wcf\\\\data\\\\foo\\\\FooAction' , identifier : 'com.woltlab.wcf.foo' }); }); 1 { anchor object = $foo class = 'fooLink' }","title":"Popovers"},{"location":"migration/wsc53/javascript/","text":"Migrating from WSC 5.3 - TypeScript and JavaScript # TypeScript # WoltLab Suite 5.4 introduces TypeScript support. Learn about consuming WoltLab Suite\u2019s types in the TypeScript section of the JavaScript API documentation. The JavaScript API documentation will be updated to properly take into account the changes that came with the new TypeScript support in the future. Existing AMD based modules have been migrated to TypeScript, but will expose the existing and known API. It is recommended that you migrate your custom packages to make use of TypeScript. It will make consuming newly written modules that properly leverage TypeScript\u2019s features much more pleasant and will also ease using existing modules due to proper autocompletion and type checking. WCF_CLICK_EVENT # For event listeners on click events, WCF_CLICK_EVENT is deprecated and should no longer be used. Instead, use click directly: 1 2 3 4 5 // before element . addEventListener ( WCF_CLICK_EVENT , this . _click . bind ( this )); // after element . addEventListener ( 'click' , ( ev ) => this . _click ( ev )); WCF.Action.Delete and WCF.Action.Toggle # WCF.Action.Delete and WCF.Action.Toggle were used for buttons to delete or enable/disable objects via JavaScript. In each template, WCF.Action.Delete or WCF.Action.Toggle instances had to be manually created for each object listing. With version 5.4 of WoltLab Suite, we have added a CSS selector-based global TypeScript module that only requires specific CSS classes to be added to the HTML structure for these buttons to work. Additionally, we have added a new {objectAction} template plugin, which generates these buttons reducing the amount of boilerplate template code. The required base HTML structure is as follows: A .jsObjectActionContainer element with a data-object-action-class-name attribute that contains the name of PHP class that executes the actions. .jsObjectActionObject elements within .jsObjectActionContainer that represent the objects for which actions can be executed. Each .jsObjectActionObject element must have a data-object-id attribute with the id of the object. .jsObjectAction elements within .jsObjectActionObject for each action with a data-object-action attribute with the name of the action. These elements can be generated with the {objectAction} template plugin for the delete and toggle action. Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { * \u2026 * } { foreach from = $objects item = foo } getObjectID () } \"> { * \u2026 * } { /foreach }
    { objectAction action = \"toggle\" isDisabled = $foo -> isDisabled } { objectAction action = \"delete\" objectTitle = $foo -> getTitle () } { * \u2026 * }
    Please refer to the documentation in ObjectActionFunctionTemplatePlugin for details and examples on how to use this template plugin. The relevant TypeScript module registering the event listeners on the object action buttons is Ui/Object/Action . When an action button is clicked, an AJAX request is sent using the PHP class name and action name. After the successful execution of the action, the page is either reloaded if the action button has a data-object-action-success=\"reload\" attribute or an event using the EventHandler module is fired using WoltLabSuite/Core/Ui/Object/Action as the identifier and the object action name. Ui/Object/Action/Delete and Ui/Object/Action/Toggle listen to these events and update the user interface depending on the execute action by removing the object or updating the toggle button, respectively. Converting from WCF.Action.* to the new approach requires minimal changes per template, as shown in the relevant pull request #4080 . WCF.Table.EmptyTableHandler # When all objects in a table or list are deleted via their delete button or clipboard actions, an empty table or list can remain. Previously, WCF.Table.EmptyTableHandler had to be explicitly used in each template for these tables and lists to reload the page. As a TypeScript-based replacement for WCF.Table.EmptyTableHandler that is only initialized once globally, WoltLabSuite/Core/Ui/Empty was added. To use this new module, you only have to add the CSS class jsReloadPageWhenEmpty to the relevant HTML element. Once this HTML element no longer has child elements, the page is reloaded. To also cover scenarios in which there are fixed child elements that should not be considered when determining if there are no child elements, the data-reload-page-when-empty=\"ignore\" can be set for these elements. Examples: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { * \u2026 * } { foreach from = $objects item = object } { * \u2026 * } { /foreach }
    1 2 3 4 5 6 7 8 9 10 11 12 13
    1. { * \u2026 * }
    2. { foreach from = $objects item = object }
    3. { * \u2026 * }
    4. { /foreach }
    ","title":"TypeScript and JavaScript"},{"location":"migration/wsc53/javascript/#migrating-from-wsc-53-typescript-and-javascript","text":"","title":"Migrating from WSC 5.3 - TypeScript and JavaScript"},{"location":"migration/wsc53/javascript/#typescript","text":"WoltLab Suite 5.4 introduces TypeScript support. Learn about consuming WoltLab Suite\u2019s types in the TypeScript section of the JavaScript API documentation. The JavaScript API documentation will be updated to properly take into account the changes that came with the new TypeScript support in the future. Existing AMD based modules have been migrated to TypeScript, but will expose the existing and known API. It is recommended that you migrate your custom packages to make use of TypeScript. It will make consuming newly written modules that properly leverage TypeScript\u2019s features much more pleasant and will also ease using existing modules due to proper autocompletion and type checking.","title":"TypeScript"},{"location":"migration/wsc53/javascript/#wcf_click_event","text":"For event listeners on click events, WCF_CLICK_EVENT is deprecated and should no longer be used. Instead, use click directly: 1 2 3 4 5 // before element . addEventListener ( WCF_CLICK_EVENT , this . _click . bind ( this )); // after element . addEventListener ( 'click' , ( ev ) => this . _click ( ev ));","title":"WCF_CLICK_EVENT"},{"location":"migration/wsc53/javascript/#wcfactiondelete-and-wcfactiontoggle","text":"WCF.Action.Delete and WCF.Action.Toggle were used for buttons to delete or enable/disable objects via JavaScript. In each template, WCF.Action.Delete or WCF.Action.Toggle instances had to be manually created for each object listing. With version 5.4 of WoltLab Suite, we have added a CSS selector-based global TypeScript module that only requires specific CSS classes to be added to the HTML structure for these buttons to work. Additionally, we have added a new {objectAction} template plugin, which generates these buttons reducing the amount of boilerplate template code. The required base HTML structure is as follows: A .jsObjectActionContainer element with a data-object-action-class-name attribute that contains the name of PHP class that executes the actions. .jsObjectActionObject elements within .jsObjectActionContainer that represent the objects for which actions can be executed. Each .jsObjectActionObject element must have a data-object-id attribute with the id of the object. .jsObjectAction elements within .jsObjectActionObject for each action with a data-object-action attribute with the name of the action. These elements can be generated with the {objectAction} template plugin for the delete and toggle action. Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { * \u2026 * } { foreach from = $objects item = foo } getObjectID () } \"> { * \u2026 * } { /foreach }
    { objectAction action = \"toggle\" isDisabled = $foo -> isDisabled } { objectAction action = \"delete\" objectTitle = $foo -> getTitle () } { * \u2026 * }
    Please refer to the documentation in ObjectActionFunctionTemplatePlugin for details and examples on how to use this template plugin. The relevant TypeScript module registering the event listeners on the object action buttons is Ui/Object/Action . When an action button is clicked, an AJAX request is sent using the PHP class name and action name. After the successful execution of the action, the page is either reloaded if the action button has a data-object-action-success=\"reload\" attribute or an event using the EventHandler module is fired using WoltLabSuite/Core/Ui/Object/Action as the identifier and the object action name. Ui/Object/Action/Delete and Ui/Object/Action/Toggle listen to these events and update the user interface depending on the execute action by removing the object or updating the toggle button, respectively. Converting from WCF.Action.* to the new approach requires minimal changes per template, as shown in the relevant pull request #4080 .","title":"WCF.Action.Delete and WCF.Action.Toggle"},{"location":"migration/wsc53/javascript/#wcftableemptytablehandler","text":"When all objects in a table or list are deleted via their delete button or clipboard actions, an empty table or list can remain. Previously, WCF.Table.EmptyTableHandler had to be explicitly used in each template for these tables and lists to reload the page. As a TypeScript-based replacement for WCF.Table.EmptyTableHandler that is only initialized once globally, WoltLabSuite/Core/Ui/Empty was added. To use this new module, you only have to add the CSS class jsReloadPageWhenEmpty to the relevant HTML element. Once this HTML element no longer has child elements, the page is reloaded. To also cover scenarios in which there are fixed child elements that should not be considered when determining if there are no child elements, the data-reload-page-when-empty=\"ignore\" can be set for these elements. Examples: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { * \u2026 * } { foreach from = $objects item = object } { * \u2026 * } { /foreach }
    1 2 3 4 5 6 7 8 9 10 11 12 13
    1. { * \u2026 * }
    2. { foreach from = $objects item = object }
    3. { * \u2026 * }
    4. { /foreach }
    ","title":"WCF.Table.EmptyTableHandler"},{"location":"migration/wsc53/libraries/","text":"Migrating from WSC 5.3 - Third Party Libraries # Guzzle # The bundled Guzzle version was updated to Guzzle 7. No breaking changes are expected for simple uses. A detailed Guzzle migration guide can be found in the Guzzle documentation. The explicit sink that was recommended in the migration guide for WSC 5.2 can now be removed, as the Guzzle issue #2735 was fixed in Guzzle 7. Emogrifier / CSS Inliner # The Emogrifier library was updated from version 2.2 to 5.0. This update comes with a breaking change, as the Emogrifier class was removed. With the updated Emogrifier library, the CssInliner class must be used instead. No compatibility layer was added for the Emogrifier class, as the Emogrifier library's purpose was to be used within the email subsystem of WoltLab Suite. In case you use Emogrifier directly within your own code, you will need to adjust the usage. Refer to the Emogrifier CHANGELOG and WoltLab/WCF #3738 if you need help making the necessary adjustments. If you only use Emogrifier indirectly by sending HTML mail via the email subsystem then you might notice unexpected visual changes due to the improved CSS support. Double check your CSS declarations and particularly the specificity of your selectors in these cases. scssphp # scssphp was updated from version 1.1 to 1.4. If you interact with scssphp only by deploying .scss files, then you should not experience any breaking changes, except when the improved SCSS compatibility interprets your SCSS code differently. If you happen to directly use scssphp in your PHP code, you should be aware that scssphp deprecated the use of output formatters in favor of a simple output style enum. Refer to WoltLab/WCF #3851 and the scssphp releases for details. Constant Time Encoder # WoltLab Suite 5.4 ships the paragonie/constant_time_encoding library . It is recommended to use this library to perform encoding and decoding of secrets to prevent leaks via cache timing attacks. Refer to the library author\u2019s blog post for more background detail. For the common case of encoding the bytes taken from a CSPRNG in hexadecimal form, the required change would look like the following: Previously: 1 2 registerContent ( 'com.example.foo.myContent' ); You should only call this method if the user creates the content themselves. If the content is automatically created by the system, for example when copying / duplicating existing content, no activity should be registered. To check the last time when the active user created content of the relevant type, use 1 FloodControl :: getInstance () -> getLastTime ( 'com.example.foo.myContent' ); If you want to limit the number of content items created within a certain period of time, for example within one day, use 1 2 3 4 5 $data = FloodControl :: getInstance () -> countContent ( 'com.example.foo.myContent' , new \\DateInterval ( 'P1D' )); // number of content items created within the last day $count = $data [ 'count' ]; // timestamp when the earliest content item was created within the last day $earliestTime = $data [ 'earliestTime' ]; The method also returns earliestTime so that you can tell the user in the error message when they are able again to create new content of the relevant type. Flood control entries are only stored for 31 days and older entries are cleaned up daily. The previously mentioned methods of FloodControl use the active user and the current timestamp as reference point. FloodControl also provides methods to register content or check flood control for other registered users or for guests via their IP address. For further details on these methods, please refer to the documentation in the FloodControl class . Do not interact directly with the flood control database table but only via the FloodControl class! DatabasePackageInstallationPlugin # DatabasePackageInstallationPlugin is a new idempotent package installation plugin (thus it is available in the sync function in the devtools) to update the database schema using the PHP-based database API. DatabasePackageInstallationPlugin is similar to ScriptPackageInstallationPlugin by requiring a PHP script that is included during the execution of the script. The script is expected to return an array of DatabaseTable objects representing the schema changes so that in contrast to using ScriptPackageInstallationPlugin , no DatabaseTableChangeProcessor object has to be created. The PHP file must be located in the acp/database/ directory for the devtools sync function to recognize the file. PHP Database API # The PHP API to add and change database tables during package installations and updates in the wcf\\system\\database\\table namespace now also supports renaming existing table columns with the new IDatabaseTableColumn::renameTo() method: 1 2 3 4 5 PartialDatabaseTable :: create ( 'wcf1_test' ) -> columns ([ NotNullInt10DatabaseTableColumn :: create ( 'oldName' ) -> renameTo ( 'newName' ) ]); Like with every change to existing database tables, packages can only rename columns that they installed. Captcha # The reCAPTCHA v1 implementation was completely removed. This includes the \\wcf\\system\\recaptcha\\RecaptchaHandler class (not to be confused with the one in the captcha namespace). The reCAPTCHA v1 endpoints have already been turned off by Google and always return a HTTP 404. Thus the implementation was completely non-functional even before this change. See WoltLab/WCF#3781 for details. Search # The generic implementation in the AbstractSearchEngine::parseSearchQuery() method was dangerous, because it did not have knowledge about the search engine\u2019s specifics. The implementation was completely removed: AbstractSearchEngine::parseSearchQuery() now always throws a \\BadMethodCallException . If you implemented a custom search engine and relied on this method, you can inline the previous implementation to preserve existing behavior. You should take the time to verify the rewritten queries against the manual of the search engine to make sure it cannot generate malformed queries or security issues. See WoltLab/WCF#3815 for details. Styles # The StyleCompiler class is marked final now. The internal SCSS compiler object being stored in the $compiler property was a design issue that leaked compiler state across multiple compiled styles, possibly causing misgenerated stylesheets. As the removal of the $compiler property effectively broke compatibility within the StyleCompiler and as the StyleCompiler never was meant to be extended, it was marked final. See WoltLab/WCF#3929 for details. Tags # Use of the wcf1_tag_to_object.languageID column is deprecated. The languageID column is redundant, because its value can be derived from the tagID . With WoltLab Suite 5.4, it will no longer be part of any indices, allowing more efficient index usage in the general case. If you need to filter the contents of wcf1_tag_to_object by language, you should perform an INNER JOIN wcf1_tag tag ON tag.tagID = tag_to_object.tagID and filter on wcf1_tag.languageID . See WoltLab/WCF#3904 for details. Avatars # The ISafeFormatAvatar interface was added to properly support fallback image types for use in emails. If your custom IUserAvatar implementation supports image types without broad support (i.e. anything other than PNG, JPEG, and GIF), then you should implement the ISafeFormatAvatar interface to return a fallback PNG, JPEG, or GIF image. See WoltLab/WCF#4001 for details. lineBreakSeparatedText Option Type # Currently, several of the (user group) options installed by our packages use the textarea option type and split its value by linebreaks to get a list of items, for example for allowed file extensions. To improve the user interface when setting up the value of such options, we have added the lineBreakSeparatedText option type as a drop-in replacement where the individual items are explicitly represented as distinct items in the user interface.","title":"PHP API"},{"location":"migration/wsc53/php/#migrating-from-wsc-53-php","text":"","title":"Migrating from WSC 5.3 - PHP"},{"location":"migration/wsc53/php/#minimum-requirements","text":"The minimum requirements have been increased to the following: PHP: 7.2.24 MySQL: 5.7.31 or 8.0.19 MariaDB: 10.1.44 Most notably PHP 7.2 contains usable support for scalar types by the addition of nullable types in PHP 7.1 and parameter type widening in PHP 7.2. It is recommended to make use of scalar types and other newly introduced features whereever possible. Please refer to the PHP documentation for details.","title":"Minimum requirements"},{"location":"migration/wsc53/php/#flood-control","text":"To prevent users from creating massive amounts of contents in short periods of time, i.e., spam, existing systems already use flood control mechanisms to limit the amount of contents created within a certain period of time. With WoltLab Suite 5.4, we have added a general API that manages such rate limiting. Leveraging this API is easily done. Register an object type for the definition com.woltlab.wcf.floodControl : com.example.foo.myContent . Whenever the active user creates content of this type, call 1 FloodControl :: getInstance () -> registerContent ( 'com.example.foo.myContent' ); You should only call this method if the user creates the content themselves. If the content is automatically created by the system, for example when copying / duplicating existing content, no activity should be registered. To check the last time when the active user created content of the relevant type, use 1 FloodControl :: getInstance () -> getLastTime ( 'com.example.foo.myContent' ); If you want to limit the number of content items created within a certain period of time, for example within one day, use 1 2 3 4 5 $data = FloodControl :: getInstance () -> countContent ( 'com.example.foo.myContent' , new \\DateInterval ( 'P1D' )); // number of content items created within the last day $count = $data [ 'count' ]; // timestamp when the earliest content item was created within the last day $earliestTime = $data [ 'earliestTime' ]; The method also returns earliestTime so that you can tell the user in the error message when they are able again to create new content of the relevant type. Flood control entries are only stored for 31 days and older entries are cleaned up daily. The previously mentioned methods of FloodControl use the active user and the current timestamp as reference point. FloodControl also provides methods to register content or check flood control for other registered users or for guests via their IP address. For further details on these methods, please refer to the documentation in the FloodControl class . Do not interact directly with the flood control database table but only via the FloodControl class!","title":"Flood Control"},{"location":"migration/wsc53/php/#databasepackageinstallationplugin","text":"DatabasePackageInstallationPlugin is a new idempotent package installation plugin (thus it is available in the sync function in the devtools) to update the database schema using the PHP-based database API. DatabasePackageInstallationPlugin is similar to ScriptPackageInstallationPlugin by requiring a PHP script that is included during the execution of the script. The script is expected to return an array of DatabaseTable objects representing the schema changes so that in contrast to using ScriptPackageInstallationPlugin , no DatabaseTableChangeProcessor object has to be created. The PHP file must be located in the acp/database/ directory for the devtools sync function to recognize the file.","title":"DatabasePackageInstallationPlugin"},{"location":"migration/wsc53/php/#php-database-api","text":"The PHP API to add and change database tables during package installations and updates in the wcf\\system\\database\\table namespace now also supports renaming existing table columns with the new IDatabaseTableColumn::renameTo() method: 1 2 3 4 5 PartialDatabaseTable :: create ( 'wcf1_test' ) -> columns ([ NotNullInt10DatabaseTableColumn :: create ( 'oldName' ) -> renameTo ( 'newName' ) ]); Like with every change to existing database tables, packages can only rename columns that they installed.","title":"PHP Database API"},{"location":"migration/wsc53/php/#captcha","text":"The reCAPTCHA v1 implementation was completely removed. This includes the \\wcf\\system\\recaptcha\\RecaptchaHandler class (not to be confused with the one in the captcha namespace). The reCAPTCHA v1 endpoints have already been turned off by Google and always return a HTTP 404. Thus the implementation was completely non-functional even before this change. See WoltLab/WCF#3781 for details.","title":"Captcha"},{"location":"migration/wsc53/php/#search","text":"The generic implementation in the AbstractSearchEngine::parseSearchQuery() method was dangerous, because it did not have knowledge about the search engine\u2019s specifics. The implementation was completely removed: AbstractSearchEngine::parseSearchQuery() now always throws a \\BadMethodCallException . If you implemented a custom search engine and relied on this method, you can inline the previous implementation to preserve existing behavior. You should take the time to verify the rewritten queries against the manual of the search engine to make sure it cannot generate malformed queries or security issues. See WoltLab/WCF#3815 for details.","title":"Search"},{"location":"migration/wsc53/php/#styles","text":"The StyleCompiler class is marked final now. The internal SCSS compiler object being stored in the $compiler property was a design issue that leaked compiler state across multiple compiled styles, possibly causing misgenerated stylesheets. As the removal of the $compiler property effectively broke compatibility within the StyleCompiler and as the StyleCompiler never was meant to be extended, it was marked final. See WoltLab/WCF#3929 for details.","title":"Styles"},{"location":"migration/wsc53/php/#tags","text":"Use of the wcf1_tag_to_object.languageID column is deprecated. The languageID column is redundant, because its value can be derived from the tagID . With WoltLab Suite 5.4, it will no longer be part of any indices, allowing more efficient index usage in the general case. If you need to filter the contents of wcf1_tag_to_object by language, you should perform an INNER JOIN wcf1_tag tag ON tag.tagID = tag_to_object.tagID and filter on wcf1_tag.languageID . See WoltLab/WCF#3904 for details.","title":"Tags"},{"location":"migration/wsc53/php/#avatars","text":"The ISafeFormatAvatar interface was added to properly support fallback image types for use in emails. If your custom IUserAvatar implementation supports image types without broad support (i.e. anything other than PNG, JPEG, and GIF), then you should implement the ISafeFormatAvatar interface to return a fallback PNG, JPEG, or GIF image. See WoltLab/WCF#4001 for details.","title":"Avatars"},{"location":"migration/wsc53/php/#linebreakseparatedtext-option-type","text":"Currently, several of the (user group) options installed by our packages use the textarea option type and split its value by linebreaks to get a list of items, for example for allowed file extensions. To improve the user interface when setting up the value of such options, we have added the lineBreakSeparatedText option type as a drop-in replacement where the individual items are explicitly represented as distinct items in the user interface.","title":"lineBreakSeparatedText Option Type"},{"location":"migration/wsc53/session/","text":"Migrating from WSC 5.3 - Session Handling and Authentication # WoltLab Suite 5.4 includes a completely refactored session handling. As long as you only interact with sessions via WCF::getSession() , especially when you perform read-only accesses, you should not notice any breaking changes. You might appreciate some of the new session methods if you process security sensitive data. Summary and Concepts # Most of the changes revolve around the removal of the legacy persistent login functionality and the assumption that every user has a single session only. Both aspects are related to each other. Legacy Persistent Login # The legacy persistent login was rather an automated login. Upon bootstrapping a session, it was checked whether the user had a cookie pair storing the user\u2019s userID and (a single BCrypt hash of) the user\u2019s password. If such a cookie pair exists and the BCrypt hash within the cookie matches the user\u2019s password hash when hashed again, the session would immediately changeUser() to the respective user. This legacy persistent login was completely removed. Instead, any sessions that belong to an authenticated user will automatically be long-lived. These long-lived sessions expire no sooner than 14 days after the last activity, ensuring that the user continously stays logged in, provided that they visit the page at least once per fortnight. Multiple Sessions # To allow for a proper separation of these long-lived user sessions, WoltLab Suite now allows for multiple sessions per user. These sessions are completely unrelated to each other. Specifically, they do not share session variables and they expire independently. As the existing wcf1_session table is also used for the online lists and location tracking, it will be maintained on a best effort basis. It no longer stores any private session data. The actual sessions storing security sensitive information are in an unrelated location. They must only be accessed via the PHP API exposed by the SessionHandler . Merged ACP and Frontend Sessions # WoltLab Suite 5.4 shares a single session across both the frontend, as well as the ACP. When a user logs in to the frontend, they will also be logged into the ACP and vice versa. Actual access to the ACP is controlled via the new reauthentication mechanism . The session variable store is scoped: Session variables set within the frontend are not available within the ACP and vice versa. Improved Authentication and Reauthentication # WoltLab Suite 5.4 ships with multi-factor authentication support and a generic re-authentication implementation that can be used to verify the account owner\u2019s presence. Additions and Changes # Password Hashing # WoltLab Suite 5.4 includes a new object-oriented password hashing framework that is modeled after PHP\u2019s password_* API. Check PasswordAlgorithmManager and IPasswordAlgorithm for details. The new default password hash is a standard BCrypt hash. All newly generated hashes in wcf1_user.password will now include a type prefix, instead of just passwords imported from other systems. Session Storage # The wcf1_session table will no longer be used for session storage. Instead, it is maintained for compatibility with existing online lists. The actual session storage is considered an implementation detail and you must not directly interact with the session tables. Future versions might support alternative session backends, such as Redis. Do not interact directly with the session database tables but only via the SessionHandler class! Reauthentication # For security sensitive processing, you might want to ensure that the account owner is actually present instead of a third party accessing a session that was accidentally left logged in. WoltLab Suite 5.4 ships with a generic reauthentication framework. To request reauthentication within your controller you need to: Use the wcf\\system\\user\\authentication\\TReauthenticationCheck trait. Call: 1 2 3 $this -> requestReauthentication ( LinkHandler :: getInstance () -> getControllerLink ( static :: class , [ /* additional parameters */ ])); requestReauthentication() will check if the user has recently authenticated themselves. If they did, the request proceeds as usual. Otherwise, they will be asked to reauthenticate themselves. After the successful authentication, they will be redirected to the URL that was passed as the first parameter (the current controller within the example). Details can be found in WoltLab/WCF#3775 . Multi-factor Authentication # To implement multi-factor authentication securely, WoltLab Suite 5.4 implements the concept of a \u201cpending user change\u201d. The user will not be logged in (i.e. WCF::getUser()->userID returns null ) until they authenticate themselves with their second factor. Requesting multi-factor authentication is done on an opt-in basis for compatibility reasons. If you perform authentication yourself and do not trust the authentication source to perform multi-factor authentication itself, you will need to adjust your logic to request multi-factor authentication from WoltLab Suite: Previously: 1 WCF :: getSession () -> changeUser ( $targetUser ); Now: 1 2 3 4 5 6 7 8 9 10 $isPending = WCF :: getSession () -> changeUserAfterMultifactorAuthentication ( $targetUser ); if ( $isPending ) { // Redirect to the authentication form. The user will not be logged in. // Note: Do not use `getControllerLink` to support both the frontend as well as the ACP. HeaderUtil :: redirect ( LinkHandler :: getInstance () -> getLink ( 'MultifactorAuthentication' , [ 'url' => /* Return To */ , ])); exit ; } // Proceed as usual. The user will be logged in. Adding Multi-factor Methods # Adding your own multi-factor method requires the implementation of a single object type: 1 2 3 4 5 6 7 com.example.multifactor.foobar com.woltlab.wcf.multifactor wcf\\system\\user\\multifactor\\FoobarMultifactorMethod The given classname must implement the IMultifactorMethod interface. As a self-contained example, you can find the initial implementation of the email multi-factor method in WoltLab/WCF#3729 . Please check the version history of the PHP class to make sure you do not miss important changes that were added later. Multi-factor authentication is security sensitive. Make sure to carefully read the remarks in IMultifactorMethod for possible issues. Also make sure to carefully test your implementation against all sorts of incorrect input and consider attack vectors such as race conditions. It is strongly recommended to generously check the current state by leveraging assertions and exceptions. Deprecations and Removals # SessionHandler # Most of the changes with regard to the new session handling happened in SessionHandler . Most notably, SessionHandler now is marked final to ensure proper encapsulation of data. A number of methods in SessionHandler are now deprecated and result in a noop. This change mostly affects methods that have been used to bootstrap the session, such as setHasValidCookie() . Additionally, accessing the following keys on the session is deprecated. They directly map to an existing method in another class and any uses can easily be updated: - ipAddress - userAgent - requestURI - requestMethod - lastActivityTime Refer to the implementation for details. ACP Sessions # The database tables related to ACP sessions have been removed. The PHP classes have been preserved due to being used within the class hierarchy of the legacy sessions. Cookies # The _userID , _password , _cookieHash and _cookieHash_acp cookies will no longer be created nor consumed. Virtual Sessions # The virtual session logic existed to support multiple devices per single session in wcf1_session . Virtual sessions are no longer required with the refactored session handling. Anything related to virtual sessions has been completely removed as they are considered an implementation detail. This removal includes PHP classes and database tables. Security Token Constants # The security token constants are deprecated. Instead, the methods of SessionHandler should be used (e.g. ->getSecurityToken() ). Within templates, you should migrate to the {csrfToken} tag in place of {@SECURITY_TOKEN_INPUT_TAG} . The {csrfToken} tag is a drop-in replacement and was backported to WoltLab Suite 5.2+, allowing you to maintain compatibility across a broad range of versions. PasswordUtil and Double BCrypt Hashes # Most of the methods in PasswordUtil are deprecated in favor of the new password hashing framework.","title":"Session Handling and Authentication"},{"location":"migration/wsc53/session/#migrating-from-wsc-53-session-handling-and-authentication","text":"WoltLab Suite 5.4 includes a completely refactored session handling. As long as you only interact with sessions via WCF::getSession() , especially when you perform read-only accesses, you should not notice any breaking changes. You might appreciate some of the new session methods if you process security sensitive data.","title":"Migrating from WSC 5.3 - Session Handling and Authentication"},{"location":"migration/wsc53/session/#summary-and-concepts","text":"Most of the changes revolve around the removal of the legacy persistent login functionality and the assumption that every user has a single session only. Both aspects are related to each other.","title":"Summary and Concepts"},{"location":"migration/wsc53/session/#legacy-persistent-login","text":"The legacy persistent login was rather an automated login. Upon bootstrapping a session, it was checked whether the user had a cookie pair storing the user\u2019s userID and (a single BCrypt hash of) the user\u2019s password. If such a cookie pair exists and the BCrypt hash within the cookie matches the user\u2019s password hash when hashed again, the session would immediately changeUser() to the respective user. This legacy persistent login was completely removed. Instead, any sessions that belong to an authenticated user will automatically be long-lived. These long-lived sessions expire no sooner than 14 days after the last activity, ensuring that the user continously stays logged in, provided that they visit the page at least once per fortnight.","title":"Legacy Persistent Login"},{"location":"migration/wsc53/session/#multiple-sessions","text":"To allow for a proper separation of these long-lived user sessions, WoltLab Suite now allows for multiple sessions per user. These sessions are completely unrelated to each other. Specifically, they do not share session variables and they expire independently. As the existing wcf1_session table is also used for the online lists and location tracking, it will be maintained on a best effort basis. It no longer stores any private session data. The actual sessions storing security sensitive information are in an unrelated location. They must only be accessed via the PHP API exposed by the SessionHandler .","title":"Multiple Sessions"},{"location":"migration/wsc53/session/#merged-acp-and-frontend-sessions","text":"WoltLab Suite 5.4 shares a single session across both the frontend, as well as the ACP. When a user logs in to the frontend, they will also be logged into the ACP and vice versa. Actual access to the ACP is controlled via the new reauthentication mechanism . The session variable store is scoped: Session variables set within the frontend are not available within the ACP and vice versa.","title":"Merged ACP and Frontend Sessions"},{"location":"migration/wsc53/session/#improved-authentication-and-reauthentication","text":"WoltLab Suite 5.4 ships with multi-factor authentication support and a generic re-authentication implementation that can be used to verify the account owner\u2019s presence.","title":"Improved Authentication and Reauthentication"},{"location":"migration/wsc53/session/#additions-and-changes","text":"","title":"Additions and Changes"},{"location":"migration/wsc53/session/#password-hashing","text":"WoltLab Suite 5.4 includes a new object-oriented password hashing framework that is modeled after PHP\u2019s password_* API. Check PasswordAlgorithmManager and IPasswordAlgorithm for details. The new default password hash is a standard BCrypt hash. All newly generated hashes in wcf1_user.password will now include a type prefix, instead of just passwords imported from other systems.","title":"Password Hashing"},{"location":"migration/wsc53/session/#session-storage","text":"The wcf1_session table will no longer be used for session storage. Instead, it is maintained for compatibility with existing online lists. The actual session storage is considered an implementation detail and you must not directly interact with the session tables. Future versions might support alternative session backends, such as Redis. Do not interact directly with the session database tables but only via the SessionHandler class!","title":"Session Storage"},{"location":"migration/wsc53/session/#reauthentication","text":"For security sensitive processing, you might want to ensure that the account owner is actually present instead of a third party accessing a session that was accidentally left logged in. WoltLab Suite 5.4 ships with a generic reauthentication framework. To request reauthentication within your controller you need to: Use the wcf\\system\\user\\authentication\\TReauthenticationCheck trait. Call: 1 2 3 $this -> requestReauthentication ( LinkHandler :: getInstance () -> getControllerLink ( static :: class , [ /* additional parameters */ ])); requestReauthentication() will check if the user has recently authenticated themselves. If they did, the request proceeds as usual. Otherwise, they will be asked to reauthenticate themselves. After the successful authentication, they will be redirected to the URL that was passed as the first parameter (the current controller within the example). Details can be found in WoltLab/WCF#3775 .","title":"Reauthentication"},{"location":"migration/wsc53/session/#multi-factor-authentication","text":"To implement multi-factor authentication securely, WoltLab Suite 5.4 implements the concept of a \u201cpending user change\u201d. The user will not be logged in (i.e. WCF::getUser()->userID returns null ) until they authenticate themselves with their second factor. Requesting multi-factor authentication is done on an opt-in basis for compatibility reasons. If you perform authentication yourself and do not trust the authentication source to perform multi-factor authentication itself, you will need to adjust your logic to request multi-factor authentication from WoltLab Suite: Previously: 1 WCF :: getSession () -> changeUser ( $targetUser ); Now: 1 2 3 4 5 6 7 8 9 10 $isPending = WCF :: getSession () -> changeUserAfterMultifactorAuthentication ( $targetUser ); if ( $isPending ) { // Redirect to the authentication form. The user will not be logged in. // Note: Do not use `getControllerLink` to support both the frontend as well as the ACP. HeaderUtil :: redirect ( LinkHandler :: getInstance () -> getLink ( 'MultifactorAuthentication' , [ 'url' => /* Return To */ , ])); exit ; } // Proceed as usual. The user will be logged in.","title":"Multi-factor Authentication"},{"location":"migration/wsc53/session/#adding-multi-factor-methods","text":"Adding your own multi-factor method requires the implementation of a single object type: 1 2 3 4 5 6 7 com.example.multifactor.foobar com.woltlab.wcf.multifactor wcf\\system\\user\\multifactor\\FoobarMultifactorMethod The given classname must implement the IMultifactorMethod interface. As a self-contained example, you can find the initial implementation of the email multi-factor method in WoltLab/WCF#3729 . Please check the version history of the PHP class to make sure you do not miss important changes that were added later. Multi-factor authentication is security sensitive. Make sure to carefully read the remarks in IMultifactorMethod for possible issues. Also make sure to carefully test your implementation against all sorts of incorrect input and consider attack vectors such as race conditions. It is strongly recommended to generously check the current state by leveraging assertions and exceptions.","title":"Adding Multi-factor Methods"},{"location":"migration/wsc53/session/#deprecations-and-removals","text":"","title":"Deprecations and Removals"},{"location":"migration/wsc53/session/#sessionhandler","text":"Most of the changes with regard to the new session handling happened in SessionHandler . Most notably, SessionHandler now is marked final to ensure proper encapsulation of data. A number of methods in SessionHandler are now deprecated and result in a noop. This change mostly affects methods that have been used to bootstrap the session, such as setHasValidCookie() . Additionally, accessing the following keys on the session is deprecated. They directly map to an existing method in another class and any uses can easily be updated: - ipAddress - userAgent - requestURI - requestMethod - lastActivityTime Refer to the implementation for details.","title":"SessionHandler"},{"location":"migration/wsc53/session/#acp-sessions","text":"The database tables related to ACP sessions have been removed. The PHP classes have been preserved due to being used within the class hierarchy of the legacy sessions.","title":"ACP Sessions"},{"location":"migration/wsc53/session/#cookies","text":"The _userID , _password , _cookieHash and _cookieHash_acp cookies will no longer be created nor consumed.","title":"Cookies"},{"location":"migration/wsc53/session/#virtual-sessions","text":"The virtual session logic existed to support multiple devices per single session in wcf1_session . Virtual sessions are no longer required with the refactored session handling. Anything related to virtual sessions has been completely removed as they are considered an implementation detail. This removal includes PHP classes and database tables.","title":"Virtual Sessions"},{"location":"migration/wsc53/session/#security-token-constants","text":"The security token constants are deprecated. Instead, the methods of SessionHandler should be used (e.g. ->getSecurityToken() ). Within templates, you should migrate to the {csrfToken} tag in place of {@SECURITY_TOKEN_INPUT_TAG} . The {csrfToken} tag is a drop-in replacement and was backported to WoltLab Suite 5.2+, allowing you to maintain compatibility across a broad range of versions.","title":"Security Token Constants"},{"location":"migration/wsc53/session/#passwordutil-and-double-bcrypt-hashes","text":"Most of the methods in PasswordUtil are deprecated in favor of the new password hashing framework.","title":"PasswordUtil and Double BCrypt Hashes"},{"location":"migration/wsc53/templates/","text":"Migrating from WSC 5.3 - Templates and Languages # {csrfToken} # Going forward, any uses of the SECURITY_TOKEN_* constants should be avoided. To reference the CSRF token (\u201cSecurity Token\u201d) within templates, the {csrfToken} template plugin was added. Before: 1 2 { @ SECURITY_TOKEN_INPUT_TAG } { link controller = \"Foo\" } t= { @ SECURITY_TOKEN }{ /link } After: 1 2 3 { csrfToken } { link controller = \"Foo\" } t= { csrfToken type = url }{ /link } { * The use of the CSRF token in URLs is discouraged. Modifications should happen by means of a POST request. * } The {csrfToken} plugin was backported to WoltLab Suite 5.2 and higher, allowing compatibility with a large range of WoltLab Suite branches. See WoltLab/WCF#3612 for details. RSS Feed Links # Prior to version 5.4 of WoltLab Suite, all RSS feed links contained the access token for logged-in users so that the feed shows all contents the specific user has access to. With version 5.4, links with the CSS class rssFeed will open a dialog when clicked that offers the feed link with the access token for personal use and an anonymous feed link that can be shared with others. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { * before * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • { * after * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"rssFeed jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • ","title":"Templates"},{"location":"migration/wsc53/templates/#migrating-from-wsc-53-templates-and-languages","text":"","title":"Migrating from WSC 5.3 - Templates and Languages"},{"location":"migration/wsc53/templates/#csrftoken","text":"Going forward, any uses of the SECURITY_TOKEN_* constants should be avoided. To reference the CSRF token (\u201cSecurity Token\u201d) within templates, the {csrfToken} template plugin was added. Before: 1 2 { @ SECURITY_TOKEN_INPUT_TAG } { link controller = \"Foo\" } t= { @ SECURITY_TOKEN }{ /link } After: 1 2 3 { csrfToken } { link controller = \"Foo\" } t= { csrfToken type = url }{ /link } { * The use of the CSRF token in URLs is discouraged. Modifications should happen by means of a POST request. * } The {csrfToken} plugin was backported to WoltLab Suite 5.2 and higher, allowing compatibility with a large range of WoltLab Suite branches. See WoltLab/WCF#3612 for details.","title":"{csrfToken}"},{"location":"migration/wsc53/templates/#rss-feed-links","text":"Prior to version 5.4 of WoltLab Suite, all RSS feed links contained the access token for logged-in users so that the feed shows all contents the specific user has access to. With version 5.4, links with the CSS class rssFeed will open a dialog when clicked that offers the feed link with the access token for personal use and an anonymous feed link that can be shared with others. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { * before * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • { * after * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"rssFeed jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • ","title":"RSS Feed Links"},{"location":"package/database-php-api/","text":"Database PHP API # Available since WoltLab Suite 5.2. While the sql package installation plugin supports adding and removing tables, columns, and indices, it is not able to handle cases where the added table, column, or index already exist. We have added a new PHP-based API to manipulate the database scheme which can be used in combination with the script package installation plugin that skips parts that already exist: 1 2 3 4 5 6 7 8 9 10 $tables = [ // list of `DatabaseTable` objects ]; ( new DatabaseTableChangeProcessor ( /** @var ScriptPackageInstallationPlugin $this */ $this -> installation -> getPackage (), $tables , WCF :: getDB () -> getEditor ()) ) -> process (); All of the relevant components can be found in the wcf\\system\\database\\table namespace. With WoltLab Suite 5.4, you should use the new database package installation plugin for which you only have to return the array of affected database tables: 1 2 3 return [ // list of `DatabaseTable` objects ]; Database Tables # There are two classes representing database tables: DatabaseTable and PartialDatabaseTable . If a new table should be created, use DatabaseTable . In all other cases, PartialDatabaseTable should be used as it provides an additional save-guard against accidentally creating a new table by having a typo in the table name: If the tables does not already exist, a table represented by PartialDatabaseTable will cause an exception (while a DatabaseTable table will simply be created). To create a table, a DatabaseTable object with the table's name as to be created and table's columns, foreign keys and indices have to be specified: 1 2 3 4 5 6 7 8 9 10 DatabaseTable :: create ( 'foo1_bar' ) -> columns ([ // columns ]) -> foreignKeys ([ // foreign keys ]) -> indices ([ // indices ]) To update a table, the same code as above can be used, except for PartialDatabaseTable being used instead of DatabaseTable . To drop a table, only the drop() method has to be called: 1 2 PartialDatabaseTable :: create ( 'foo1_bar' ) -> drop () Columns # To represent a column of a database table, you have to create an instance of the relevant column class found in the wcf\\system\\database\\table\\column namespace. Such instances are created similarly to database table objects using the create() factory method and passing the column name as the parameter. Every column type supports the following methods: defaultValue($defaultValue) sets the default value of the column (default: none). drop() to drop the column. notNull($notNull = true) sets if the value of the column can be NULL (default: false ). Depending on the specific column class implementing additional interfaces, the following methods are also available: IAutoIncrementDatabaseTableColumn::autoIncrement($autoIncrement = true) sets if the value of the colum is auto-incremented. IDecimalsDatabaseTableColumn::decimals($decimals) sets the number of decimals the column supports. IEnumDatabaseTableColumn::enumValues(array $values) sets the predetermined set of valid values of the column. ILengthDatabaseTableColumn::length($length) sets the (maximum) length of the column. Additionally, there are some additionally classes of commonly used columns with specific properties: DefaultFalseBooleanDatabaseTableColumn (a tinyint column with length 1 , default value 0 and whose values cannot be null ) DefaultTrueBooleanDatabaseTableColumn (a tinyint column with length 0 , default value 0 and whose values cannot be null ) NotNullInt10DatabaseTableColumn (a int column with length 10 and whose values cannot be null ) NotNullVarchar191DatabaseTableColumn (a varchar column with length 191 and whose values cannot be null ) NotNullVarchar255DatabaseTableColumn (a varchar column with length 255 and whose values cannot be null ) ObjectIdDatabaseTableColumn (a int column with length 10 , whose values cannot be null , and whose values are auto-incremented) Examples: 1 2 3 4 5 6 7 DefaultFalseBooleanDatabaseTableColumn :: create ( 'isDisabled' ) NotNullInt10DatabaseTableColumn :: create ( 'fooTypeID' ) SmallintDatabaseTableColumn :: create ( 'bar' ) -> length ( 5 ) -> notNull () Foreign Keys # Foreign keys are represented by DatabaseTableForeignKey objects: 1 2 3 4 5 DatabaseTableForeignKey :: create () -> columns ([ 'fooID' ]) -> referencedTable ( 'wcf1_foo' ) -> referencedColumns ([ 'fooID' ]) -> onDelete ( 'CASCADE' ) The supported actions for onDelete() and onUpdate() are CASCADE , NO ACTION , and SET NULL . To drop a foreign key, all of the relevant data to create the foreign key has to be present and the drop() method has to be called. DatabaseTableForeignKey::create() also supports the foreign key name as a parameter. If it is not present, DatabaseTable::foreignKeys() will automatically set one based on the foreign key's data. Indices # Indices are represented by DatabaseTableIndex objects: 1 2 3 DatabaseTableIndex :: create () -> type ( DatabaseTableIndex :: UNIQUE_TYPE ) -> columns ([ 'fooID' ]) There are four different types: DatabaseTableIndex::DEFAULT_TYPE (default), DatabaseTableIndex::PRIMARY_TYPE , DatabaseTableIndex::UNIQUE_TYPE , and DatabaseTableIndex::FULLTEXT_TYPE . For primary keys, there is also the DatabaseTablePrimaryIndex class which automatically sets the type to DatabaseTableIndex::PRIMARY_TYPE . To drop a index, all of the relevant data to create the index has to be present and the drop() method has to be called. DatabaseTableIndex::create() also supports the index name as a parameter. If it is not present, DatabaseTable::indices() will automatically set one based on the index data.","title":"Database PHP API"},{"location":"package/database-php-api/#database-php-api","text":"Available since WoltLab Suite 5.2. While the sql package installation plugin supports adding and removing tables, columns, and indices, it is not able to handle cases where the added table, column, or index already exist. We have added a new PHP-based API to manipulate the database scheme which can be used in combination with the script package installation plugin that skips parts that already exist: 1 2 3 4 5 6 7 8 9 10 $tables = [ // list of `DatabaseTable` objects ]; ( new DatabaseTableChangeProcessor ( /** @var ScriptPackageInstallationPlugin $this */ $this -> installation -> getPackage (), $tables , WCF :: getDB () -> getEditor ()) ) -> process (); All of the relevant components can be found in the wcf\\system\\database\\table namespace. With WoltLab Suite 5.4, you should use the new database package installation plugin for which you only have to return the array of affected database tables: 1 2 3 return [ // list of `DatabaseTable` objects ];","title":"Database PHP API"},{"location":"package/database-php-api/#database-tables","text":"There are two classes representing database tables: DatabaseTable and PartialDatabaseTable . If a new table should be created, use DatabaseTable . In all other cases, PartialDatabaseTable should be used as it provides an additional save-guard against accidentally creating a new table by having a typo in the table name: If the tables does not already exist, a table represented by PartialDatabaseTable will cause an exception (while a DatabaseTable table will simply be created). To create a table, a DatabaseTable object with the table's name as to be created and table's columns, foreign keys and indices have to be specified: 1 2 3 4 5 6 7 8 9 10 DatabaseTable :: create ( 'foo1_bar' ) -> columns ([ // columns ]) -> foreignKeys ([ // foreign keys ]) -> indices ([ // indices ]) To update a table, the same code as above can be used, except for PartialDatabaseTable being used instead of DatabaseTable . To drop a table, only the drop() method has to be called: 1 2 PartialDatabaseTable :: create ( 'foo1_bar' ) -> drop ()","title":"Database Tables"},{"location":"package/database-php-api/#columns","text":"To represent a column of a database table, you have to create an instance of the relevant column class found in the wcf\\system\\database\\table\\column namespace. Such instances are created similarly to database table objects using the create() factory method and passing the column name as the parameter. Every column type supports the following methods: defaultValue($defaultValue) sets the default value of the column (default: none). drop() to drop the column. notNull($notNull = true) sets if the value of the column can be NULL (default: false ). Depending on the specific column class implementing additional interfaces, the following methods are also available: IAutoIncrementDatabaseTableColumn::autoIncrement($autoIncrement = true) sets if the value of the colum is auto-incremented. IDecimalsDatabaseTableColumn::decimals($decimals) sets the number of decimals the column supports. IEnumDatabaseTableColumn::enumValues(array $values) sets the predetermined set of valid values of the column. ILengthDatabaseTableColumn::length($length) sets the (maximum) length of the column. Additionally, there are some additionally classes of commonly used columns with specific properties: DefaultFalseBooleanDatabaseTableColumn (a tinyint column with length 1 , default value 0 and whose values cannot be null ) DefaultTrueBooleanDatabaseTableColumn (a tinyint column with length 0 , default value 0 and whose values cannot be null ) NotNullInt10DatabaseTableColumn (a int column with length 10 and whose values cannot be null ) NotNullVarchar191DatabaseTableColumn (a varchar column with length 191 and whose values cannot be null ) NotNullVarchar255DatabaseTableColumn (a varchar column with length 255 and whose values cannot be null ) ObjectIdDatabaseTableColumn (a int column with length 10 , whose values cannot be null , and whose values are auto-incremented) Examples: 1 2 3 4 5 6 7 DefaultFalseBooleanDatabaseTableColumn :: create ( 'isDisabled' ) NotNullInt10DatabaseTableColumn :: create ( 'fooTypeID' ) SmallintDatabaseTableColumn :: create ( 'bar' ) -> length ( 5 ) -> notNull ()","title":"Columns"},{"location":"package/database-php-api/#foreign-keys","text":"Foreign keys are represented by DatabaseTableForeignKey objects: 1 2 3 4 5 DatabaseTableForeignKey :: create () -> columns ([ 'fooID' ]) -> referencedTable ( 'wcf1_foo' ) -> referencedColumns ([ 'fooID' ]) -> onDelete ( 'CASCADE' ) The supported actions for onDelete() and onUpdate() are CASCADE , NO ACTION , and SET NULL . To drop a foreign key, all of the relevant data to create the foreign key has to be present and the drop() method has to be called. DatabaseTableForeignKey::create() also supports the foreign key name as a parameter. If it is not present, DatabaseTable::foreignKeys() will automatically set one based on the foreign key's data.","title":"Foreign Keys"},{"location":"package/database-php-api/#indices","text":"Indices are represented by DatabaseTableIndex objects: 1 2 3 DatabaseTableIndex :: create () -> type ( DatabaseTableIndex :: UNIQUE_TYPE ) -> columns ([ 'fooID' ]) There are four different types: DatabaseTableIndex::DEFAULT_TYPE (default), DatabaseTableIndex::PRIMARY_TYPE , DatabaseTableIndex::UNIQUE_TYPE , and DatabaseTableIndex::FULLTEXT_TYPE . For primary keys, there is also the DatabaseTablePrimaryIndex class which automatically sets the type to DatabaseTableIndex::PRIMARY_TYPE . To drop a index, all of the relevant data to create the index has to be present and the drop() method has to be called. DatabaseTableIndex::create() also supports the index name as a parameter. If it is not present, DatabaseTable::indices() will automatically set one based on the index data.","title":"Indices"},{"location":"package/package-xml/","text":"package.xml # The package.xml is the core component of every package. It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version. Example # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Simple Package A simple package to demonstrate the package system of WoltLab Suite Core 1.0.0 2016-12-18 YOUR NAME http://www.example.com com.woltlab.wcf com.woltlab.wcf templates.tar Elements # # The root node of every package.xml it contains the reference to the namespace and the location of the XML Schema Definition (XSD). The attribute name is the most important part, it holds the unique package identifier and is mandatory. It is based upon your domain name and the package name of your choice. For example WoltLab Suite Forum (formerly know an WoltLab Burning Board and usually abbreviated as wbb ) is created by WoltLab which owns the domain woltlab.com . The resulting package identifier is com.woltlab.wbb ( .. ). # Holds the entire meta data of the package. # This is the actual package name displayed to the end user, this can be anything you want, try to keep it short. It supports the attribute languagecode which allows you to provide the package name in different languages, please be aware that if it is not present, en (English) is assumed: 1 2 3 4 Simple Package Einfaches Paket # Brief summary of the package, use it to explain what it does since the package name might not always be clear enough. The attribute languagecode is available here too, please reference to for details. # The package's version number, this is a string consisting of three numbers separated with a dot and optionally followed by a keyword (must be followed with another number). The possible keywords are: Alpha/dev (both is regarded to be the same) Beta RC (release candidate) pl (patch level) Valid examples: 1.0.0 1.12.13 Alpha 19 7.0.0 pl 3 Invalid examples: 1.0.0 Beta (keyword Beta must be followed by a number) 2.0 RC 3 (version number must consists of 3 blocks of numbers) 1.2.3 dev 4.5 (4.5 is not an integer, 4 or 5 would be valid but not the fraction) # Must be a valid ISO 8601 date, e.g. 2013-12-27 . # Holds meta data regarding the package's author. # Can be anything you want. # (optional) URL to the author's website. # A list of packages including their version required for this package to work. # Example: 1 com.woltlab.wcf The attribute minversion must be a valid version number as described in . The file attribute is optional and specifies the location of the required package's archive relative to the package.xml . # A list of optional packages which can be selected by the user at the very end of the installation process. # Example: 1 com.woltlab.wcf.moderatedUserGroup The file attribute specifies the location of the optional package's archive relative to the package.xml . # List of packages which conflict with this package. It is not possible to install it if any of the specified packages is installed. In return you cannot install an excluded package if this package is installed. # Example: 1 com.woltlab.wcf The attribute version must be a valid version number as described in the \\ section. In the example above it will be impossible to install this package in WoltLab Suite Core 3.1.0 Alpha 1 or higher. # Available since WoltLab Suite 3.1 With the release of WoltLab Suite 5.2 the API versions were abolished. Instead of using API versions packages should exclude version 6.0.0 Alpha 1 of com.woltlab.wcf going forward. WoltLab Suite 3.1 introduced a new versioning system that focused around the API compatibility and is intended to replace the instruction for the Core for most plugins. The -tag holds a list of compatible API versions, and while only a single version is available at the time of writing, future versions will add more versions with backwards-compatibility in mind. Example: 1 2 3 Existing API versions # WoltLab Suite Core API-Version Backwards-Compatible to API-Version 3.1 2018 n/a # List of instructions to be executed upon install or update. The order is important, the topmost will be executed first. # List of instructions for a new installation of this package. # The attribute fromversion must be a valid version number as described in the \\ section and specifies a possible update from that very version to the package's version. The installation process will pick exactly one update instruction, ignoring everything else. Please read the explanation below! Example: Installed version: 1.0.0 Package version: 1.0.2 1 2 3 4 5 6 In this example WoltLab Suite Core will pick the first update block since it allows an update from 1.0.0 -> 1.0.2 . The other block is not considered, since the currently installed version is 1.0.0 . After applying the update block ( fromversion=\"1.0.0\" ), the version now reads 1.0.2 . # Example: 1 objectTypeDefinition.xml The attribute type specifies the instruction type which is used to determine the package installation plugin (PIP) invoked to handle its value. The value must be a valid file relative to the location of package.xml . Many PIPs provide default file names which are used if no value is given: 1 There is a list of all default PIPs available. Both the type -attribute and the element value are case-sensitive. Windows does not care if the file is called objecttypedefinition.xml but was referenced as objectTypeDefinition.xml , but both Linux and Mac systems will be unable to find the file. In addition to the type attribute, an optional run attribute (with standalone as the only valid value) is supported which forces the installation to execute this PIP in an isolated request, allowing a single, resource-heavy PIP to execute without encountering restrictions such as PHP\u2019s memory_limit or max_execution_time : 1 # Sometimes a package update should only adjust the metadata of the package, for example, an optional package was added. However, WoltLab Suite Core requires that the list of is non-empty. Instead of using a dummy that idempotently updates some PIP, the tag can be used for this use-case. Using the tag is only valid for and must not be accompanied by other tags. Example: 1 2 3 ","title":"package.xml"},{"location":"package/package-xml/#packagexml","text":"The package.xml is the core component of every package. It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version.","title":"package.xml"},{"location":"package/package-xml/#example","text":"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Simple Package A simple package to demonstrate the package system of WoltLab Suite Core 1.0.0 2016-12-18 YOUR NAME http://www.example.com com.woltlab.wcf com.woltlab.wcf templates.tar ","title":"Example"},{"location":"package/package-xml/#elements","text":"","title":"Elements"},{"location":"package/package-xml/#package","text":"The root node of every package.xml it contains the reference to the namespace and the location of the XML Schema Definition (XSD). The attribute name is the most important part, it holds the unique package identifier and is mandatory. It is based upon your domain name and the package name of your choice. For example WoltLab Suite Forum (formerly know an WoltLab Burning Board and usually abbreviated as wbb ) is created by WoltLab which owns the domain woltlab.com . The resulting package identifier is com.woltlab.wbb ( .. ).","title":"<package>"},{"location":"package/package-xml/#packageinformation","text":"Holds the entire meta data of the package.","title":"<packageinformation>"},{"location":"package/package-xml/#packagename","text":"This is the actual package name displayed to the end user, this can be anything you want, try to keep it short. It supports the attribute languagecode which allows you to provide the package name in different languages, please be aware that if it is not present, en (English) is assumed: 1 2 3 4 Simple Package Einfaches Paket ","title":"<packagename>"},{"location":"package/package-xml/#packagedescription","text":"Brief summary of the package, use it to explain what it does since the package name might not always be clear enough. The attribute languagecode is available here too, please reference to for details.","title":"<packagedescription>"},{"location":"package/package-xml/#version","text":"The package's version number, this is a string consisting of three numbers separated with a dot and optionally followed by a keyword (must be followed with another number). The possible keywords are: Alpha/dev (both is regarded to be the same) Beta RC (release candidate) pl (patch level) Valid examples: 1.0.0 1.12.13 Alpha 19 7.0.0 pl 3 Invalid examples: 1.0.0 Beta (keyword Beta must be followed by a number) 2.0 RC 3 (version number must consists of 3 blocks of numbers) 1.2.3 dev 4.5 (4.5 is not an integer, 4 or 5 would be valid but not the fraction)","title":"<version>"},{"location":"package/package-xml/#date","text":"Must be a valid ISO 8601 date, e.g. 2013-12-27 .","title":"<date>"},{"location":"package/package-xml/#authorinformation","text":"Holds meta data regarding the package's author.","title":"<authorinformation>"},{"location":"package/package-xml/#author","text":"Can be anything you want.","title":"<author>"},{"location":"package/package-xml/#authorurl","text":"(optional) URL to the author's website.","title":"<authorurl>"},{"location":"package/package-xml/#requiredpackages","text":"A list of packages including their version required for this package to work.","title":"<requiredpackages>"},{"location":"package/package-xml/#requiredpackage","text":"Example: 1 com.woltlab.wcf The attribute minversion must be a valid version number as described in . The file attribute is optional and specifies the location of the required package's archive relative to the package.xml .","title":"<requiredpackage>"},{"location":"package/package-xml/#optionalpackage","text":"A list of optional packages which can be selected by the user at the very end of the installation process.","title":"<optionalpackage>"},{"location":"package/package-xml/#optionalpackage_1","text":"Example: 1 com.woltlab.wcf.moderatedUserGroup The file attribute specifies the location of the optional package's archive relative to the package.xml .","title":"<optionalpackage>"},{"location":"package/package-xml/#excludedpackages","text":"List of packages which conflict with this package. It is not possible to install it if any of the specified packages is installed. In return you cannot install an excluded package if this package is installed.","title":"<excludedpackages>"},{"location":"package/package-xml/#excludedpackage","text":"Example: 1 com.woltlab.wcf The attribute version must be a valid version number as described in the \\ section. In the example above it will be impossible to install this package in WoltLab Suite Core 3.1.0 Alpha 1 or higher.","title":"<excludedpackage>"},{"location":"package/package-xml/#compatibility","text":"Available since WoltLab Suite 3.1 With the release of WoltLab Suite 5.2 the API versions were abolished. Instead of using API versions packages should exclude version 6.0.0 Alpha 1 of com.woltlab.wcf going forward. WoltLab Suite 3.1 introduced a new versioning system that focused around the API compatibility and is intended to replace the instruction for the Core for most plugins. The -tag holds a list of compatible API versions, and while only a single version is available at the time of writing, future versions will add more versions with backwards-compatibility in mind. Example: 1 2 3 ","title":"<compatibility>"},{"location":"package/package-xml/#existing-api-versions","text":"WoltLab Suite Core API-Version Backwards-Compatible to API-Version 3.1 2018 n/a","title":"Existing API versions"},{"location":"package/package-xml/#instructions","text":"List of instructions to be executed upon install or update. The order is important, the topmost will be executed first.","title":"<instructions>"},{"location":"package/package-xml/#instructions-typeinstall","text":"List of instructions for a new installation of this package.","title":"<instructions type=\"install\">"},{"location":"package/package-xml/#instructions-typeupdate-fromversion","text":"The attribute fromversion must be a valid version number as described in the \\ section and specifies a possible update from that very version to the package's version. The installation process will pick exactly one update instruction, ignoring everything else. Please read the explanation below! Example: Installed version: 1.0.0 Package version: 1.0.2 1 2 3 4 5 6 In this example WoltLab Suite Core will pick the first update block since it allows an update from 1.0.0 -> 1.0.2 . The other block is not considered, since the currently installed version is 1.0.0 . After applying the update block ( fromversion=\"1.0.0\" ), the version now reads 1.0.2 .","title":"<instructions type=\"update\" fromversion=\"\u2026\">"},{"location":"package/package-xml/#instruction","text":"Example: 1 objectTypeDefinition.xml The attribute type specifies the instruction type which is used to determine the package installation plugin (PIP) invoked to handle its value. The value must be a valid file relative to the location of package.xml . Many PIPs provide default file names which are used if no value is given: 1 There is a list of all default PIPs available. Both the type -attribute and the element value are case-sensitive. Windows does not care if the file is called objecttypedefinition.xml but was referenced as objectTypeDefinition.xml , but both Linux and Mac systems will be unable to find the file. In addition to the type attribute, an optional run attribute (with standalone as the only valid value) is supported which forces the installation to execute this PIP in an isolated request, allowing a single, resource-heavy PIP to execute without encountering restrictions such as PHP\u2019s memory_limit or max_execution_time : 1 ","title":"<instruction>"},{"location":"package/package-xml/#void","text":"Sometimes a package update should only adjust the metadata of the package, for example, an optional package was added. However, WoltLab Suite Core requires that the list of is non-empty. Instead of using a dummy that idempotently updates some PIP, the tag can be used for this use-case. Using the tag is only valid for and must not be accompanied by other tags. Example: 1 2 3 ","title":"<void/>"},{"location":"package/pip/","text":"Package Installation Plugins # Package Installation Plugins (PIPs) are interfaces to deploy and edit content as well as components. For XML-based PIPs: must be used for language items and page contents. In all other cases it may only be used when necessary. Built-In PIPs # Name Description aclOption Customizable permissions for individual objects acpMenu Admin panel menu categories and items acpSearchProvider Data provider for the admin panel search acpTemplate Admin panel templates bbcode BBCodes for rich message formatting box Boxes that can be placed anywhere on a page clipboardAction Perform bulk operations on marked objects coreObject Access Singletons from within the template cronjob Periodically execute code with customizable intervals database Updates the database layout using the PHP API eventListener Register listeners for the event system file Deploy any type of files with the exception of templates language Language items mediaProvider Detect and convert links to media providers menu Side-wide and custom per-page menus menuItem Menu items for menus created through the menu PIP objectType Flexible type registry based on definitions objectTypeDefinition Groups objects and classes by functionality option Side-wide configuration options page Register page controllers and text-based pages pip Package Installation Plugins script Execute arbitrary PHP code during installation, update and uninstallation smiley Smileys sql Execute SQL instructions using a MySQL-flavored syntax (also see database PHP API ) style Style template Frontend templates templateListener Embed template code into templates without altering the original userGroupOption Permissions for user groups userMenu User menu categories and items userNotificationEvent Events of the user notification system userOption User settings userProfileMenu User profile tabs","title":"Overview"},{"location":"package/pip/#package-installation-plugins","text":"Package Installation Plugins (PIPs) are interfaces to deploy and edit content as well as components. For XML-based PIPs: must be used for language items and page contents. In all other cases it may only be used when necessary.","title":"Package Installation Plugins"},{"location":"package/pip/#built-in-pips","text":"Name Description aclOption Customizable permissions for individual objects acpMenu Admin panel menu categories and items acpSearchProvider Data provider for the admin panel search acpTemplate Admin panel templates bbcode BBCodes for rich message formatting box Boxes that can be placed anywhere on a page clipboardAction Perform bulk operations on marked objects coreObject Access Singletons from within the template cronjob Periodically execute code with customizable intervals database Updates the database layout using the PHP API eventListener Register listeners for the event system file Deploy any type of files with the exception of templates language Language items mediaProvider Detect and convert links to media providers menu Side-wide and custom per-page menus menuItem Menu items for menus created through the menu PIP objectType Flexible type registry based on definitions objectTypeDefinition Groups objects and classes by functionality option Side-wide configuration options page Register page controllers and text-based pages pip Package Installation Plugins script Execute arbitrary PHP code during installation, update and uninstallation smiley Smileys sql Execute SQL instructions using a MySQL-flavored syntax (also see database PHP API ) style Style template Frontend templates templateListener Embed template code into templates without altering the original userGroupOption Permissions for user groups userMenu User menu categories and items userNotificationEvent Events of the user notification system userOption User settings userProfileMenu User profile tabs","title":"Built-In PIPs"},{"location":"package/pip/acl-option/","text":"ACL Option Package Installation Plugin # Add customizable permissions for individual objects. Option Components # Each acl option is described as an
    { lang } wcf.person.birthday { /lang } { if $person -> birthday }{ @ $person -> birthday | strtotime | date }{ /if } { lang } wcf.person.birthday { /lang } { if $person -> birthday }{ @ $person -> birthday | strtotime | date }{ /if }
    { * \u2026 * } { foreach from = $objects item = foo } getObjectID () } \"> { * \u2026 * } { /foreach }
    { objectAction action = \"toggle\" isDisabled = $foo -> isDisabled } { objectAction action = \"delete\" objectTitle = $foo -> getTitle () } { * \u2026 * }
    Please refer to the documentation in ObjectActionFunctionTemplatePlugin for details and examples on how to use this template plugin. The relevant TypeScript module registering the event listeners on the object action buttons is Ui/Object/Action . When an action button is clicked, an AJAX request is sent using the PHP class name and action name. After the successful execution of the action, the page is either reloaded if the action button has a data-object-action-success=\"reload\" attribute or an event using the EventHandler module is fired using WoltLabSuite/Core/Ui/Object/Action as the identifier and the object action name. Ui/Object/Action/Delete and Ui/Object/Action/Toggle listen to these events and update the user interface depending on the execute action by removing the object or updating the toggle button, respectively. Converting from WCF.Action.* to the new approach requires minimal changes per template, as shown in the relevant pull request #4080 . WCF.Table.EmptyTableHandler # When all objects in a table or list are deleted via their delete button or clipboard actions, an empty table or list can remain. Previously, WCF.Table.EmptyTableHandler had to be explicitly used in each template for these tables and lists to reload the page. As a TypeScript-based replacement for WCF.Table.EmptyTableHandler that is only initialized once globally, WoltLabSuite/Core/Ui/Empty was added. To use this new module, you only have to add the CSS class jsReloadPageWhenEmpty to the relevant HTML element. Once this HTML element no longer has child elements, the page is reloaded. To also cover scenarios in which there are fixed child elements that should not be considered when determining if there are no child elements, the data-reload-page-when-empty=\"ignore\" can be set for these elements. Examples: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { * \u2026 * } { foreach from = $objects item = object } { * \u2026 * } { /foreach }
    1 2 3 4 5 6 7 8 9 10 11 12 13
    1. { * \u2026 * }
    2. { foreach from = $objects item = object }
    3. { * \u2026 * }
    4. { /foreach }
    ","title":"TypeScript and JavaScript"},{"location":"migration/wsc53/javascript/#migrating-from-wsc-53-typescript-and-javascript","text":"","title":"Migrating from WSC 5.3 - TypeScript and JavaScript"},{"location":"migration/wsc53/javascript/#typescript","text":"WoltLab Suite 5.4 introduces TypeScript support. Learn about consuming WoltLab Suite\u2019s types in the TypeScript section of the JavaScript API documentation. The JavaScript API documentation will be updated to properly take into account the changes that came with the new TypeScript support in the future. Existing AMD based modules have been migrated to TypeScript, but will expose the existing and known API. It is recommended that you migrate your custom packages to make use of TypeScript. It will make consuming newly written modules that properly leverage TypeScript\u2019s features much more pleasant and will also ease using existing modules due to proper autocompletion and type checking.","title":"TypeScript"},{"location":"migration/wsc53/javascript/#wcf_click_event","text":"For event listeners on click events, WCF_CLICK_EVENT is deprecated and should no longer be used. Instead, use click directly: 1 2 3 4 5 // before element . addEventListener ( WCF_CLICK_EVENT , this . _click . bind ( this )); // after element . addEventListener ( 'click' , ( ev ) => this . _click ( ev ));","title":"WCF_CLICK_EVENT"},{"location":"migration/wsc53/javascript/#wcfactiondelete-and-wcfactiontoggle","text":"WCF.Action.Delete and WCF.Action.Toggle were used for buttons to delete or enable/disable objects via JavaScript. In each template, WCF.Action.Delete or WCF.Action.Toggle instances had to be manually created for each object listing. With version 5.4 of WoltLab Suite, we have added a CSS selector-based global TypeScript module that only requires specific CSS classes to be added to the HTML structure for these buttons to work. Additionally, we have added a new {objectAction} template plugin, which generates these buttons reducing the amount of boilerplate template code. The required base HTML structure is as follows: A .jsObjectActionContainer element with a data-object-action-class-name attribute that contains the name of PHP class that executes the actions. .jsObjectActionObject elements within .jsObjectActionContainer that represent the objects for which actions can be executed. Each .jsObjectActionObject element must have a data-object-id attribute with the id of the object. .jsObjectAction elements within .jsObjectActionObject for each action with a data-object-action attribute with the name of the action. These elements can be generated with the {objectAction} template plugin for the delete and toggle action. Example: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { * \u2026 * } { foreach from = $objects item = foo } getObjectID () } \"> { * \u2026 * } { /foreach }
    { objectAction action = \"toggle\" isDisabled = $foo -> isDisabled } { objectAction action = \"delete\" objectTitle = $foo -> getTitle () } { * \u2026 * }
    Please refer to the documentation in ObjectActionFunctionTemplatePlugin for details and examples on how to use this template plugin. The relevant TypeScript module registering the event listeners on the object action buttons is Ui/Object/Action . When an action button is clicked, an AJAX request is sent using the PHP class name and action name. After the successful execution of the action, the page is either reloaded if the action button has a data-object-action-success=\"reload\" attribute or an event using the EventHandler module is fired using WoltLabSuite/Core/Ui/Object/Action as the identifier and the object action name. Ui/Object/Action/Delete and Ui/Object/Action/Toggle listen to these events and update the user interface depending on the execute action by removing the object or updating the toggle button, respectively. Converting from WCF.Action.* to the new approach requires minimal changes per template, as shown in the relevant pull request #4080 .","title":"WCF.Action.Delete and WCF.Action.Toggle"},{"location":"migration/wsc53/javascript/#wcftableemptytablehandler","text":"When all objects in a table or list are deleted via their delete button or clipboard actions, an empty table or list can remain. Previously, WCF.Table.EmptyTableHandler had to be explicitly used in each template for these tables and lists to reload the page. As a TypeScript-based replacement for WCF.Table.EmptyTableHandler that is only initialized once globally, WoltLabSuite/Core/Ui/Empty was added. To use this new module, you only have to add the CSS class jsReloadPageWhenEmpty to the relevant HTML element. Once this HTML element no longer has child elements, the page is reloaded. To also cover scenarios in which there are fixed child elements that should not be considered when determining if there are no child elements, the data-reload-page-when-empty=\"ignore\" can be set for these elements. Examples: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { * \u2026 * } { foreach from = $objects item = object } { * \u2026 * } { /foreach }
    1 2 3 4 5 6 7 8 9 10 11 12 13
    1. { * \u2026 * }
    2. { foreach from = $objects item = object }
    3. { * \u2026 * }
    4. { /foreach }
    ","title":"WCF.Table.EmptyTableHandler"},{"location":"migration/wsc53/libraries/","text":"Migrating from WSC 5.3 - Third Party Libraries # Guzzle # The bundled Guzzle version was updated to Guzzle 7. No breaking changes are expected for simple uses. A detailed Guzzle migration guide can be found in the Guzzle documentation. The explicit sink that was recommended in the migration guide for WSC 5.2 can now be removed, as the Guzzle issue #2735 was fixed in Guzzle 7. Emogrifier / CSS Inliner # The Emogrifier library was updated from version 2.2 to 5.0. This update comes with a breaking change, as the Emogrifier class was removed. With the updated Emogrifier library, the CssInliner class must be used instead. No compatibility layer was added for the Emogrifier class, as the Emogrifier library's purpose was to be used within the email subsystem of WoltLab Suite. In case you use Emogrifier directly within your own code, you will need to adjust the usage. Refer to the Emogrifier CHANGELOG and WoltLab/WCF #3738 if you need help making the necessary adjustments. If you only use Emogrifier indirectly by sending HTML mail via the email subsystem then you might notice unexpected visual changes due to the improved CSS support. Double check your CSS declarations and particularly the specificity of your selectors in these cases. scssphp # scssphp was updated from version 1.1 to 1.4. If you interact with scssphp only by deploying .scss files, then you should not experience any breaking changes, except when the improved SCSS compatibility interprets your SCSS code differently. If you happen to directly use scssphp in your PHP code, you should be aware that scssphp deprecated the use of output formatters in favor of a simple output style enum. Refer to WoltLab/WCF #3851 and the scssphp releases for details. Constant Time Encoder # WoltLab Suite 5.4 ships the paragonie/constant_time_encoding library . It is recommended to use this library to perform encoding and decoding of secrets to prevent leaks via cache timing attacks. Refer to the library author\u2019s blog post for more background detail. For the common case of encoding the bytes taken from a CSPRNG in hexadecimal form, the required change would look like the following: Previously: 1 2 registerContent ( 'com.example.foo.myContent' ); You should only call this method if the user creates the content themselves. If the content is automatically created by the system, for example when copying / duplicating existing content, no activity should be registered. To check the last time when the active user created content of the relevant type, use 1 FloodControl :: getInstance () -> getLastTime ( 'com.example.foo.myContent' ); If you want to limit the number of content items created within a certain period of time, for example within one day, use 1 2 3 4 5 $data = FloodControl :: getInstance () -> countContent ( 'com.example.foo.myContent' , new \\DateInterval ( 'P1D' )); // number of content items created within the last day $count = $data [ 'count' ]; // timestamp when the earliest content item was created within the last day $earliestTime = $data [ 'earliestTime' ]; The method also returns earliestTime so that you can tell the user in the error message when they are able again to create new content of the relevant type. Flood control entries are only stored for 31 days and older entries are cleaned up daily. The previously mentioned methods of FloodControl use the active user and the current timestamp as reference point. FloodControl also provides methods to register content or check flood control for other registered users or for guests via their IP address. For further details on these methods, please refer to the documentation in the FloodControl class . Do not interact directly with the flood control database table but only via the FloodControl class! DatabasePackageInstallationPlugin # DatabasePackageInstallationPlugin is a new idempotent package installation plugin (thus it is available in the sync function in the devtools) to update the database schema using the PHP-based database API. DatabasePackageInstallationPlugin is similar to ScriptPackageInstallationPlugin by requiring a PHP script that is included during the execution of the script. The script is expected to return an array of DatabaseTable objects representing the schema changes so that in contrast to using ScriptPackageInstallationPlugin , no DatabaseTableChangeProcessor object has to be created. The PHP file must be located in the acp/database/ directory for the devtools sync function to recognize the file. PHP Database API # The PHP API to add and change database tables during package installations and updates in the wcf\\system\\database\\table namespace now also supports renaming existing table columns with the new IDatabaseTableColumn::renameTo() method: 1 2 3 4 5 PartialDatabaseTable :: create ( 'wcf1_test' ) -> columns ([ NotNullInt10DatabaseTableColumn :: create ( 'oldName' ) -> renameTo ( 'newName' ) ]); Like with every change to existing database tables, packages can only rename columns that they installed. Captcha # The reCAPTCHA v1 implementation was completely removed. This includes the \\wcf\\system\\recaptcha\\RecaptchaHandler class (not to be confused with the one in the captcha namespace). The reCAPTCHA v1 endpoints have already been turned off by Google and always return a HTTP 404. Thus the implementation was completely non-functional even before this change. See WoltLab/WCF#3781 for details. Search # The generic implementation in the AbstractSearchEngine::parseSearchQuery() method was dangerous, because it did not have knowledge about the search engine\u2019s specifics. The implementation was completely removed: AbstractSearchEngine::parseSearchQuery() now always throws a \\BadMethodCallException . If you implemented a custom search engine and relied on this method, you can inline the previous implementation to preserve existing behavior. You should take the time to verify the rewritten queries against the manual of the search engine to make sure it cannot generate malformed queries or security issues. See WoltLab/WCF#3815 for details. Styles # The StyleCompiler class is marked final now. The internal SCSS compiler object being stored in the $compiler property was a design issue that leaked compiler state across multiple compiled styles, possibly causing misgenerated stylesheets. As the removal of the $compiler property effectively broke compatibility within the StyleCompiler and as the StyleCompiler never was meant to be extended, it was marked final. See WoltLab/WCF#3929 for details. Tags # Use of the wcf1_tag_to_object.languageID column is deprecated. The languageID column is redundant, because its value can be derived from the tagID . With WoltLab Suite 5.4, it will no longer be part of any indices, allowing more efficient index usage in the general case. If you need to filter the contents of wcf1_tag_to_object by language, you should perform an INNER JOIN wcf1_tag tag ON tag.tagID = tag_to_object.tagID and filter on wcf1_tag.languageID . See WoltLab/WCF#3904 for details. Avatars # The ISafeFormatAvatar interface was added to properly support fallback image types for use in emails. If your custom IUserAvatar implementation supports image types without broad support (i.e. anything other than PNG, JPEG, and GIF), then you should implement the ISafeFormatAvatar interface to return a fallback PNG, JPEG, or GIF image. See WoltLab/WCF#4001 for details. lineBreakSeparatedText Option Type # Currently, several of the (user group) options installed by our packages use the textarea option type and split its value by linebreaks to get a list of items, for example for allowed file extensions. To improve the user interface when setting up the value of such options, we have added the lineBreakSeparatedText option type as a drop-in replacement where the individual items are explicitly represented as distinct items in the user interface.","title":"PHP API"},{"location":"migration/wsc53/php/#migrating-from-wsc-53-php","text":"","title":"Migrating from WSC 5.3 - PHP"},{"location":"migration/wsc53/php/#minimum-requirements","text":"The minimum requirements have been increased to the following: PHP: 7.2.24 MySQL: 5.7.31 or 8.0.19 MariaDB: 10.1.44 Most notably PHP 7.2 contains usable support for scalar types by the addition of nullable types in PHP 7.1 and parameter type widening in PHP 7.2. It is recommended to make use of scalar types and other newly introduced features whereever possible. Please refer to the PHP documentation for details.","title":"Minimum requirements"},{"location":"migration/wsc53/php/#flood-control","text":"To prevent users from creating massive amounts of contents in short periods of time, i.e., spam, existing systems already use flood control mechanisms to limit the amount of contents created within a certain period of time. With WoltLab Suite 5.4, we have added a general API that manages such rate limiting. Leveraging this API is easily done. Register an object type for the definition com.woltlab.wcf.floodControl : com.example.foo.myContent . Whenever the active user creates content of this type, call 1 FloodControl :: getInstance () -> registerContent ( 'com.example.foo.myContent' ); You should only call this method if the user creates the content themselves. If the content is automatically created by the system, for example when copying / duplicating existing content, no activity should be registered. To check the last time when the active user created content of the relevant type, use 1 FloodControl :: getInstance () -> getLastTime ( 'com.example.foo.myContent' ); If you want to limit the number of content items created within a certain period of time, for example within one day, use 1 2 3 4 5 $data = FloodControl :: getInstance () -> countContent ( 'com.example.foo.myContent' , new \\DateInterval ( 'P1D' )); // number of content items created within the last day $count = $data [ 'count' ]; // timestamp when the earliest content item was created within the last day $earliestTime = $data [ 'earliestTime' ]; The method also returns earliestTime so that you can tell the user in the error message when they are able again to create new content of the relevant type. Flood control entries are only stored for 31 days and older entries are cleaned up daily. The previously mentioned methods of FloodControl use the active user and the current timestamp as reference point. FloodControl also provides methods to register content or check flood control for other registered users or for guests via their IP address. For further details on these methods, please refer to the documentation in the FloodControl class . Do not interact directly with the flood control database table but only via the FloodControl class!","title":"Flood Control"},{"location":"migration/wsc53/php/#databasepackageinstallationplugin","text":"DatabasePackageInstallationPlugin is a new idempotent package installation plugin (thus it is available in the sync function in the devtools) to update the database schema using the PHP-based database API. DatabasePackageInstallationPlugin is similar to ScriptPackageInstallationPlugin by requiring a PHP script that is included during the execution of the script. The script is expected to return an array of DatabaseTable objects representing the schema changes so that in contrast to using ScriptPackageInstallationPlugin , no DatabaseTableChangeProcessor object has to be created. The PHP file must be located in the acp/database/ directory for the devtools sync function to recognize the file.","title":"DatabasePackageInstallationPlugin"},{"location":"migration/wsc53/php/#php-database-api","text":"The PHP API to add and change database tables during package installations and updates in the wcf\\system\\database\\table namespace now also supports renaming existing table columns with the new IDatabaseTableColumn::renameTo() method: 1 2 3 4 5 PartialDatabaseTable :: create ( 'wcf1_test' ) -> columns ([ NotNullInt10DatabaseTableColumn :: create ( 'oldName' ) -> renameTo ( 'newName' ) ]); Like with every change to existing database tables, packages can only rename columns that they installed.","title":"PHP Database API"},{"location":"migration/wsc53/php/#captcha","text":"The reCAPTCHA v1 implementation was completely removed. This includes the \\wcf\\system\\recaptcha\\RecaptchaHandler class (not to be confused with the one in the captcha namespace). The reCAPTCHA v1 endpoints have already been turned off by Google and always return a HTTP 404. Thus the implementation was completely non-functional even before this change. See WoltLab/WCF#3781 for details.","title":"Captcha"},{"location":"migration/wsc53/php/#search","text":"The generic implementation in the AbstractSearchEngine::parseSearchQuery() method was dangerous, because it did not have knowledge about the search engine\u2019s specifics. The implementation was completely removed: AbstractSearchEngine::parseSearchQuery() now always throws a \\BadMethodCallException . If you implemented a custom search engine and relied on this method, you can inline the previous implementation to preserve existing behavior. You should take the time to verify the rewritten queries against the manual of the search engine to make sure it cannot generate malformed queries or security issues. See WoltLab/WCF#3815 for details.","title":"Search"},{"location":"migration/wsc53/php/#styles","text":"The StyleCompiler class is marked final now. The internal SCSS compiler object being stored in the $compiler property was a design issue that leaked compiler state across multiple compiled styles, possibly causing misgenerated stylesheets. As the removal of the $compiler property effectively broke compatibility within the StyleCompiler and as the StyleCompiler never was meant to be extended, it was marked final. See WoltLab/WCF#3929 for details.","title":"Styles"},{"location":"migration/wsc53/php/#tags","text":"Use of the wcf1_tag_to_object.languageID column is deprecated. The languageID column is redundant, because its value can be derived from the tagID . With WoltLab Suite 5.4, it will no longer be part of any indices, allowing more efficient index usage in the general case. If you need to filter the contents of wcf1_tag_to_object by language, you should perform an INNER JOIN wcf1_tag tag ON tag.tagID = tag_to_object.tagID and filter on wcf1_tag.languageID . See WoltLab/WCF#3904 for details.","title":"Tags"},{"location":"migration/wsc53/php/#avatars","text":"The ISafeFormatAvatar interface was added to properly support fallback image types for use in emails. If your custom IUserAvatar implementation supports image types without broad support (i.e. anything other than PNG, JPEG, and GIF), then you should implement the ISafeFormatAvatar interface to return a fallback PNG, JPEG, or GIF image. See WoltLab/WCF#4001 for details.","title":"Avatars"},{"location":"migration/wsc53/php/#linebreakseparatedtext-option-type","text":"Currently, several of the (user group) options installed by our packages use the textarea option type and split its value by linebreaks to get a list of items, for example for allowed file extensions. To improve the user interface when setting up the value of such options, we have added the lineBreakSeparatedText option type as a drop-in replacement where the individual items are explicitly represented as distinct items in the user interface.","title":"lineBreakSeparatedText Option Type"},{"location":"migration/wsc53/session/","text":"Migrating from WSC 5.3 - Session Handling and Authentication # WoltLab Suite 5.4 includes a completely refactored session handling. As long as you only interact with sessions via WCF::getSession() , especially when you perform read-only accesses, you should not notice any breaking changes. You might appreciate some of the new session methods if you process security sensitive data. Summary and Concepts # Most of the changes revolve around the removal of the legacy persistent login functionality and the assumption that every user has a single session only. Both aspects are related to each other. Legacy Persistent Login # The legacy persistent login was rather an automated login. Upon bootstrapping a session, it was checked whether the user had a cookie pair storing the user\u2019s userID and (a single BCrypt hash of) the user\u2019s password. If such a cookie pair exists and the BCrypt hash within the cookie matches the user\u2019s password hash when hashed again, the session would immediately changeUser() to the respective user. This legacy persistent login was completely removed. Instead, any sessions that belong to an authenticated user will automatically be long-lived. These long-lived sessions expire no sooner than 14 days after the last activity, ensuring that the user continously stays logged in, provided that they visit the page at least once per fortnight. Multiple Sessions # To allow for a proper separation of these long-lived user sessions, WoltLab Suite now allows for multiple sessions per user. These sessions are completely unrelated to each other. Specifically, they do not share session variables and they expire independently. As the existing wcf1_session table is also used for the online lists and location tracking, it will be maintained on a best effort basis. It no longer stores any private session data. The actual sessions storing security sensitive information are in an unrelated location. They must only be accessed via the PHP API exposed by the SessionHandler . Merged ACP and Frontend Sessions # WoltLab Suite 5.4 shares a single session across both the frontend, as well as the ACP. When a user logs in to the frontend, they will also be logged into the ACP and vice versa. Actual access to the ACP is controlled via the new reauthentication mechanism . The session variable store is scoped: Session variables set within the frontend are not available within the ACP and vice versa. Improved Authentication and Reauthentication # WoltLab Suite 5.4 ships with multi-factor authentication support and a generic re-authentication implementation that can be used to verify the account owner\u2019s presence. Additions and Changes # Password Hashing # WoltLab Suite 5.4 includes a new object-oriented password hashing framework that is modeled after PHP\u2019s password_* API. Check PasswordAlgorithmManager and IPasswordAlgorithm for details. The new default password hash is a standard BCrypt hash. All newly generated hashes in wcf1_user.password will now include a type prefix, instead of just passwords imported from other systems. Session Storage # The wcf1_session table will no longer be used for session storage. Instead, it is maintained for compatibility with existing online lists. The actual session storage is considered an implementation detail and you must not directly interact with the session tables. Future versions might support alternative session backends, such as Redis. Do not interact directly with the session database tables but only via the SessionHandler class! Reauthentication # For security sensitive processing, you might want to ensure that the account owner is actually present instead of a third party accessing a session that was accidentally left logged in. WoltLab Suite 5.4 ships with a generic reauthentication framework. To request reauthentication within your controller you need to: Use the wcf\\system\\user\\authentication\\TReauthenticationCheck trait. Call: 1 2 3 $this -> requestReauthentication ( LinkHandler :: getInstance () -> getControllerLink ( static :: class , [ /* additional parameters */ ])); requestReauthentication() will check if the user has recently authenticated themselves. If they did, the request proceeds as usual. Otherwise, they will be asked to reauthenticate themselves. After the successful authentication, they will be redirected to the URL that was passed as the first parameter (the current controller within the example). Details can be found in WoltLab/WCF#3775 . Multi-factor Authentication # To implement multi-factor authentication securely, WoltLab Suite 5.4 implements the concept of a \u201cpending user change\u201d. The user will not be logged in (i.e. WCF::getUser()->userID returns null ) until they authenticate themselves with their second factor. Requesting multi-factor authentication is done on an opt-in basis for compatibility reasons. If you perform authentication yourself and do not trust the authentication source to perform multi-factor authentication itself, you will need to adjust your logic to request multi-factor authentication from WoltLab Suite: Previously: 1 WCF :: getSession () -> changeUser ( $targetUser ); Now: 1 2 3 4 5 6 7 8 9 10 $isPending = WCF :: getSession () -> changeUserAfterMultifactorAuthentication ( $targetUser ); if ( $isPending ) { // Redirect to the authentication form. The user will not be logged in. // Note: Do not use `getControllerLink` to support both the frontend as well as the ACP. HeaderUtil :: redirect ( LinkHandler :: getInstance () -> getLink ( 'MultifactorAuthentication' , [ 'url' => /* Return To */ , ])); exit ; } // Proceed as usual. The user will be logged in. Adding Multi-factor Methods # Adding your own multi-factor method requires the implementation of a single object type: 1 2 3 4 5 6 7 com.example.multifactor.foobar com.woltlab.wcf.multifactor wcf\\system\\user\\multifactor\\FoobarMultifactorMethod The given classname must implement the IMultifactorMethod interface. As a self-contained example, you can find the initial implementation of the email multi-factor method in WoltLab/WCF#3729 . Please check the version history of the PHP class to make sure you do not miss important changes that were added later. Multi-factor authentication is security sensitive. Make sure to carefully read the remarks in IMultifactorMethod for possible issues. Also make sure to carefully test your implementation against all sorts of incorrect input and consider attack vectors such as race conditions. It is strongly recommended to generously check the current state by leveraging assertions and exceptions. Deprecations and Removals # SessionHandler # Most of the changes with regard to the new session handling happened in SessionHandler . Most notably, SessionHandler now is marked final to ensure proper encapsulation of data. A number of methods in SessionHandler are now deprecated and result in a noop. This change mostly affects methods that have been used to bootstrap the session, such as setHasValidCookie() . Additionally, accessing the following keys on the session is deprecated. They directly map to an existing method in another class and any uses can easily be updated: - ipAddress - userAgent - requestURI - requestMethod - lastActivityTime Refer to the implementation for details. ACP Sessions # The database tables related to ACP sessions have been removed. The PHP classes have been preserved due to being used within the class hierarchy of the legacy sessions. Cookies # The _userID , _password , _cookieHash and _cookieHash_acp cookies will no longer be created nor consumed. Virtual Sessions # The virtual session logic existed to support multiple devices per single session in wcf1_session . Virtual sessions are no longer required with the refactored session handling. Anything related to virtual sessions has been completely removed as they are considered an implementation detail. This removal includes PHP classes and database tables. Security Token Constants # The security token constants are deprecated. Instead, the methods of SessionHandler should be used (e.g. ->getSecurityToken() ). Within templates, you should migrate to the {csrfToken} tag in place of {@SECURITY_TOKEN_INPUT_TAG} . The {csrfToken} tag is a drop-in replacement and was backported to WoltLab Suite 5.2+, allowing you to maintain compatibility across a broad range of versions. PasswordUtil and Double BCrypt Hashes # Most of the methods in PasswordUtil are deprecated in favor of the new password hashing framework.","title":"Session Handling and Authentication"},{"location":"migration/wsc53/session/#migrating-from-wsc-53-session-handling-and-authentication","text":"WoltLab Suite 5.4 includes a completely refactored session handling. As long as you only interact with sessions via WCF::getSession() , especially when you perform read-only accesses, you should not notice any breaking changes. You might appreciate some of the new session methods if you process security sensitive data.","title":"Migrating from WSC 5.3 - Session Handling and Authentication"},{"location":"migration/wsc53/session/#summary-and-concepts","text":"Most of the changes revolve around the removal of the legacy persistent login functionality and the assumption that every user has a single session only. Both aspects are related to each other.","title":"Summary and Concepts"},{"location":"migration/wsc53/session/#legacy-persistent-login","text":"The legacy persistent login was rather an automated login. Upon bootstrapping a session, it was checked whether the user had a cookie pair storing the user\u2019s userID and (a single BCrypt hash of) the user\u2019s password. If such a cookie pair exists and the BCrypt hash within the cookie matches the user\u2019s password hash when hashed again, the session would immediately changeUser() to the respective user. This legacy persistent login was completely removed. Instead, any sessions that belong to an authenticated user will automatically be long-lived. These long-lived sessions expire no sooner than 14 days after the last activity, ensuring that the user continously stays logged in, provided that they visit the page at least once per fortnight.","title":"Legacy Persistent Login"},{"location":"migration/wsc53/session/#multiple-sessions","text":"To allow for a proper separation of these long-lived user sessions, WoltLab Suite now allows for multiple sessions per user. These sessions are completely unrelated to each other. Specifically, they do not share session variables and they expire independently. As the existing wcf1_session table is also used for the online lists and location tracking, it will be maintained on a best effort basis. It no longer stores any private session data. The actual sessions storing security sensitive information are in an unrelated location. They must only be accessed via the PHP API exposed by the SessionHandler .","title":"Multiple Sessions"},{"location":"migration/wsc53/session/#merged-acp-and-frontend-sessions","text":"WoltLab Suite 5.4 shares a single session across both the frontend, as well as the ACP. When a user logs in to the frontend, they will also be logged into the ACP and vice versa. Actual access to the ACP is controlled via the new reauthentication mechanism . The session variable store is scoped: Session variables set within the frontend are not available within the ACP and vice versa.","title":"Merged ACP and Frontend Sessions"},{"location":"migration/wsc53/session/#improved-authentication-and-reauthentication","text":"WoltLab Suite 5.4 ships with multi-factor authentication support and a generic re-authentication implementation that can be used to verify the account owner\u2019s presence.","title":"Improved Authentication and Reauthentication"},{"location":"migration/wsc53/session/#additions-and-changes","text":"","title":"Additions and Changes"},{"location":"migration/wsc53/session/#password-hashing","text":"WoltLab Suite 5.4 includes a new object-oriented password hashing framework that is modeled after PHP\u2019s password_* API. Check PasswordAlgorithmManager and IPasswordAlgorithm for details. The new default password hash is a standard BCrypt hash. All newly generated hashes in wcf1_user.password will now include a type prefix, instead of just passwords imported from other systems.","title":"Password Hashing"},{"location":"migration/wsc53/session/#session-storage","text":"The wcf1_session table will no longer be used for session storage. Instead, it is maintained for compatibility with existing online lists. The actual session storage is considered an implementation detail and you must not directly interact with the session tables. Future versions might support alternative session backends, such as Redis. Do not interact directly with the session database tables but only via the SessionHandler class!","title":"Session Storage"},{"location":"migration/wsc53/session/#reauthentication","text":"For security sensitive processing, you might want to ensure that the account owner is actually present instead of a third party accessing a session that was accidentally left logged in. WoltLab Suite 5.4 ships with a generic reauthentication framework. To request reauthentication within your controller you need to: Use the wcf\\system\\user\\authentication\\TReauthenticationCheck trait. Call: 1 2 3 $this -> requestReauthentication ( LinkHandler :: getInstance () -> getControllerLink ( static :: class , [ /* additional parameters */ ])); requestReauthentication() will check if the user has recently authenticated themselves. If they did, the request proceeds as usual. Otherwise, they will be asked to reauthenticate themselves. After the successful authentication, they will be redirected to the URL that was passed as the first parameter (the current controller within the example). Details can be found in WoltLab/WCF#3775 .","title":"Reauthentication"},{"location":"migration/wsc53/session/#multi-factor-authentication","text":"To implement multi-factor authentication securely, WoltLab Suite 5.4 implements the concept of a \u201cpending user change\u201d. The user will not be logged in (i.e. WCF::getUser()->userID returns null ) until they authenticate themselves with their second factor. Requesting multi-factor authentication is done on an opt-in basis for compatibility reasons. If you perform authentication yourself and do not trust the authentication source to perform multi-factor authentication itself, you will need to adjust your logic to request multi-factor authentication from WoltLab Suite: Previously: 1 WCF :: getSession () -> changeUser ( $targetUser ); Now: 1 2 3 4 5 6 7 8 9 10 $isPending = WCF :: getSession () -> changeUserAfterMultifactorAuthentication ( $targetUser ); if ( $isPending ) { // Redirect to the authentication form. The user will not be logged in. // Note: Do not use `getControllerLink` to support both the frontend as well as the ACP. HeaderUtil :: redirect ( LinkHandler :: getInstance () -> getLink ( 'MultifactorAuthentication' , [ 'url' => /* Return To */ , ])); exit ; } // Proceed as usual. The user will be logged in.","title":"Multi-factor Authentication"},{"location":"migration/wsc53/session/#adding-multi-factor-methods","text":"Adding your own multi-factor method requires the implementation of a single object type: 1 2 3 4 5 6 7 com.example.multifactor.foobar com.woltlab.wcf.multifactor wcf\\system\\user\\multifactor\\FoobarMultifactorMethod The given classname must implement the IMultifactorMethod interface. As a self-contained example, you can find the initial implementation of the email multi-factor method in WoltLab/WCF#3729 . Please check the version history of the PHP class to make sure you do not miss important changes that were added later. Multi-factor authentication is security sensitive. Make sure to carefully read the remarks in IMultifactorMethod for possible issues. Also make sure to carefully test your implementation against all sorts of incorrect input and consider attack vectors such as race conditions. It is strongly recommended to generously check the current state by leveraging assertions and exceptions.","title":"Adding Multi-factor Methods"},{"location":"migration/wsc53/session/#deprecations-and-removals","text":"","title":"Deprecations and Removals"},{"location":"migration/wsc53/session/#sessionhandler","text":"Most of the changes with regard to the new session handling happened in SessionHandler . Most notably, SessionHandler now is marked final to ensure proper encapsulation of data. A number of methods in SessionHandler are now deprecated and result in a noop. This change mostly affects methods that have been used to bootstrap the session, such as setHasValidCookie() . Additionally, accessing the following keys on the session is deprecated. They directly map to an existing method in another class and any uses can easily be updated: - ipAddress - userAgent - requestURI - requestMethod - lastActivityTime Refer to the implementation for details.","title":"SessionHandler"},{"location":"migration/wsc53/session/#acp-sessions","text":"The database tables related to ACP sessions have been removed. The PHP classes have been preserved due to being used within the class hierarchy of the legacy sessions.","title":"ACP Sessions"},{"location":"migration/wsc53/session/#cookies","text":"The _userID , _password , _cookieHash and _cookieHash_acp cookies will no longer be created nor consumed.","title":"Cookies"},{"location":"migration/wsc53/session/#virtual-sessions","text":"The virtual session logic existed to support multiple devices per single session in wcf1_session . Virtual sessions are no longer required with the refactored session handling. Anything related to virtual sessions has been completely removed as they are considered an implementation detail. This removal includes PHP classes and database tables.","title":"Virtual Sessions"},{"location":"migration/wsc53/session/#security-token-constants","text":"The security token constants are deprecated. Instead, the methods of SessionHandler should be used (e.g. ->getSecurityToken() ). Within templates, you should migrate to the {csrfToken} tag in place of {@SECURITY_TOKEN_INPUT_TAG} . The {csrfToken} tag is a drop-in replacement and was backported to WoltLab Suite 5.2+, allowing you to maintain compatibility across a broad range of versions.","title":"Security Token Constants"},{"location":"migration/wsc53/session/#passwordutil-and-double-bcrypt-hashes","text":"Most of the methods in PasswordUtil are deprecated in favor of the new password hashing framework.","title":"PasswordUtil and Double BCrypt Hashes"},{"location":"migration/wsc53/templates/","text":"Migrating from WSC 5.3 - Templates and Languages # {csrfToken} # Going forward, any uses of the SECURITY_TOKEN_* constants should be avoided. To reference the CSRF token (\u201cSecurity Token\u201d) within templates, the {csrfToken} template plugin was added. Before: 1 2 { @ SECURITY_TOKEN_INPUT_TAG } { link controller = \"Foo\" } t= { @ SECURITY_TOKEN }{ /link } After: 1 2 3 { csrfToken } { link controller = \"Foo\" } t= { csrfToken type = url }{ /link } { * The use of the CSRF token in URLs is discouraged. Modifications should happen by means of a POST request. * } The {csrfToken} plugin was backported to WoltLab Suite 5.2 and higher, allowing compatibility with a large range of WoltLab Suite branches. See WoltLab/WCF#3612 for details. RSS Feed Links # Prior to version 5.4 of WoltLab Suite, all RSS feed links contained the access token for logged-in users so that the feed shows all contents the specific user has access to. With version 5.4, links with the CSS class rssFeed will open a dialog when clicked that offers the feed link with the access token for personal use and an anonymous feed link that can be shared with others. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { * before * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • { * after * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"rssFeed jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • ","title":"Templates"},{"location":"migration/wsc53/templates/#migrating-from-wsc-53-templates-and-languages","text":"","title":"Migrating from WSC 5.3 - Templates and Languages"},{"location":"migration/wsc53/templates/#csrftoken","text":"Going forward, any uses of the SECURITY_TOKEN_* constants should be avoided. To reference the CSRF token (\u201cSecurity Token\u201d) within templates, the {csrfToken} template plugin was added. Before: 1 2 { @ SECURITY_TOKEN_INPUT_TAG } { link controller = \"Foo\" } t= { @ SECURITY_TOKEN }{ /link } After: 1 2 3 { csrfToken } { link controller = \"Foo\" } t= { csrfToken type = url }{ /link } { * The use of the CSRF token in URLs is discouraged. Modifications should happen by means of a POST request. * } The {csrfToken} plugin was backported to WoltLab Suite 5.2 and higher, allowing compatibility with a large range of WoltLab Suite branches. See WoltLab/WCF#3612 for details.","title":"{csrfToken}"},{"location":"migration/wsc53/templates/#rss-feed-links","text":"Prior to version 5.4 of WoltLab Suite, all RSS feed links contained the access token for logged-in users so that the feed shows all contents the specific user has access to. With version 5.4, links with the CSS class rssFeed will open a dialog when clicked that offers the feed link with the access token for personal use and an anonymous feed link that can be shared with others. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { * before * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • { * after * }
  • getUser ()-> userID }{ link controller = 'ArticleFeed' } at= { @ $__wcf -> getUser ()-> userID } - { @ $__wcf -> getUser ()-> accessToken }{ /link }{ else }{ link controller = 'ArticleFeed' }{ /link }{ /if } \" { * * } title=\" { lang } wcf.global.button.rss { /lang } \" { * * } class=\"rssFeed jsTooltip\" { * * } > { lang } wcf.global.button.rss { /lang }
  • ","title":"RSS Feed Links"},{"location":"package/database-php-api/","text":"Database PHP API # Available since WoltLab Suite 5.2. While the sql package installation plugin supports adding and removing tables, columns, and indices, it is not able to handle cases where the added table, column, or index already exist. We have added a new PHP-based API to manipulate the database scheme which can be used in combination with the script package installation plugin that skips parts that already exist: 1 2 3 4 5 6 7 8 9 10 $tables = [ // list of `DatabaseTable` objects ]; ( new DatabaseTableChangeProcessor ( /** @var ScriptPackageInstallationPlugin $this */ $this -> installation -> getPackage (), $tables , WCF :: getDB () -> getEditor ()) ) -> process (); All of the relevant components can be found in the wcf\\system\\database\\table namespace. With WoltLab Suite 5.4, you should use the new database package installation plugin for which you only have to return the array of affected database tables: 1 2 3 return [ // list of `DatabaseTable` objects ]; Database Tables # There are two classes representing database tables: DatabaseTable and PartialDatabaseTable . If a new table should be created, use DatabaseTable . In all other cases, PartialDatabaseTable should be used as it provides an additional save-guard against accidentally creating a new table by having a typo in the table name: If the tables does not already exist, a table represented by PartialDatabaseTable will cause an exception (while a DatabaseTable table will simply be created). To create a table, a DatabaseTable object with the table's name as to be created and table's columns, foreign keys and indices have to be specified: 1 2 3 4 5 6 7 8 9 10 DatabaseTable :: create ( 'foo1_bar' ) -> columns ([ // columns ]) -> foreignKeys ([ // foreign keys ]) -> indices ([ // indices ]) To update a table, the same code as above can be used, except for PartialDatabaseTable being used instead of DatabaseTable . To drop a table, only the drop() method has to be called: 1 2 PartialDatabaseTable :: create ( 'foo1_bar' ) -> drop () Columns # To represent a column of a database table, you have to create an instance of the relevant column class found in the wcf\\system\\database\\table\\column namespace. Such instances are created similarly to database table objects using the create() factory method and passing the column name as the parameter. Every column type supports the following methods: defaultValue($defaultValue) sets the default value of the column (default: none). drop() to drop the column. notNull($notNull = true) sets if the value of the column can be NULL (default: false ). Depending on the specific column class implementing additional interfaces, the following methods are also available: IAutoIncrementDatabaseTableColumn::autoIncrement($autoIncrement = true) sets if the value of the colum is auto-incremented. IDecimalsDatabaseTableColumn::decimals($decimals) sets the number of decimals the column supports. IEnumDatabaseTableColumn::enumValues(array $values) sets the predetermined set of valid values of the column. ILengthDatabaseTableColumn::length($length) sets the (maximum) length of the column. Additionally, there are some additionally classes of commonly used columns with specific properties: DefaultFalseBooleanDatabaseTableColumn (a tinyint column with length 1 , default value 0 and whose values cannot be null ) DefaultTrueBooleanDatabaseTableColumn (a tinyint column with length 0 , default value 0 and whose values cannot be null ) NotNullInt10DatabaseTableColumn (a int column with length 10 and whose values cannot be null ) NotNullVarchar191DatabaseTableColumn (a varchar column with length 191 and whose values cannot be null ) NotNullVarchar255DatabaseTableColumn (a varchar column with length 255 and whose values cannot be null ) ObjectIdDatabaseTableColumn (a int column with length 10 , whose values cannot be null , and whose values are auto-incremented) Examples: 1 2 3 4 5 6 7 DefaultFalseBooleanDatabaseTableColumn :: create ( 'isDisabled' ) NotNullInt10DatabaseTableColumn :: create ( 'fooTypeID' ) SmallintDatabaseTableColumn :: create ( 'bar' ) -> length ( 5 ) -> notNull () Foreign Keys # Foreign keys are represented by DatabaseTableForeignKey objects: 1 2 3 4 5 DatabaseTableForeignKey :: create () -> columns ([ 'fooID' ]) -> referencedTable ( 'wcf1_foo' ) -> referencedColumns ([ 'fooID' ]) -> onDelete ( 'CASCADE' ) The supported actions for onDelete() and onUpdate() are CASCADE , NO ACTION , and SET NULL . To drop a foreign key, all of the relevant data to create the foreign key has to be present and the drop() method has to be called. DatabaseTableForeignKey::create() also supports the foreign key name as a parameter. If it is not present, DatabaseTable::foreignKeys() will automatically set one based on the foreign key's data. Indices # Indices are represented by DatabaseTableIndex objects: 1 2 3 DatabaseTableIndex :: create () -> type ( DatabaseTableIndex :: UNIQUE_TYPE ) -> columns ([ 'fooID' ]) There are four different types: DatabaseTableIndex::DEFAULT_TYPE (default), DatabaseTableIndex::PRIMARY_TYPE , DatabaseTableIndex::UNIQUE_TYPE , and DatabaseTableIndex::FULLTEXT_TYPE . For primary keys, there is also the DatabaseTablePrimaryIndex class which automatically sets the type to DatabaseTableIndex::PRIMARY_TYPE . To drop a index, all of the relevant data to create the index has to be present and the drop() method has to be called. DatabaseTableIndex::create() also supports the index name as a parameter. If it is not present, DatabaseTable::indices() will automatically set one based on the index data.","title":"Database PHP API"},{"location":"package/database-php-api/#database-php-api","text":"Available since WoltLab Suite 5.2. While the sql package installation plugin supports adding and removing tables, columns, and indices, it is not able to handle cases where the added table, column, or index already exist. We have added a new PHP-based API to manipulate the database scheme which can be used in combination with the script package installation plugin that skips parts that already exist: 1 2 3 4 5 6 7 8 9 10 $tables = [ // list of `DatabaseTable` objects ]; ( new DatabaseTableChangeProcessor ( /** @var ScriptPackageInstallationPlugin $this */ $this -> installation -> getPackage (), $tables , WCF :: getDB () -> getEditor ()) ) -> process (); All of the relevant components can be found in the wcf\\system\\database\\table namespace. With WoltLab Suite 5.4, you should use the new database package installation plugin for which you only have to return the array of affected database tables: 1 2 3 return [ // list of `DatabaseTable` objects ];","title":"Database PHP API"},{"location":"package/database-php-api/#database-tables","text":"There are two classes representing database tables: DatabaseTable and PartialDatabaseTable . If a new table should be created, use DatabaseTable . In all other cases, PartialDatabaseTable should be used as it provides an additional save-guard against accidentally creating a new table by having a typo in the table name: If the tables does not already exist, a table represented by PartialDatabaseTable will cause an exception (while a DatabaseTable table will simply be created). To create a table, a DatabaseTable object with the table's name as to be created and table's columns, foreign keys and indices have to be specified: 1 2 3 4 5 6 7 8 9 10 DatabaseTable :: create ( 'foo1_bar' ) -> columns ([ // columns ]) -> foreignKeys ([ // foreign keys ]) -> indices ([ // indices ]) To update a table, the same code as above can be used, except for PartialDatabaseTable being used instead of DatabaseTable . To drop a table, only the drop() method has to be called: 1 2 PartialDatabaseTable :: create ( 'foo1_bar' ) -> drop ()","title":"Database Tables"},{"location":"package/database-php-api/#columns","text":"To represent a column of a database table, you have to create an instance of the relevant column class found in the wcf\\system\\database\\table\\column namespace. Such instances are created similarly to database table objects using the create() factory method and passing the column name as the parameter. Every column type supports the following methods: defaultValue($defaultValue) sets the default value of the column (default: none). drop() to drop the column. notNull($notNull = true) sets if the value of the column can be NULL (default: false ). Depending on the specific column class implementing additional interfaces, the following methods are also available: IAutoIncrementDatabaseTableColumn::autoIncrement($autoIncrement = true) sets if the value of the colum is auto-incremented. IDecimalsDatabaseTableColumn::decimals($decimals) sets the number of decimals the column supports. IEnumDatabaseTableColumn::enumValues(array $values) sets the predetermined set of valid values of the column. ILengthDatabaseTableColumn::length($length) sets the (maximum) length of the column. Additionally, there are some additionally classes of commonly used columns with specific properties: DefaultFalseBooleanDatabaseTableColumn (a tinyint column with length 1 , default value 0 and whose values cannot be null ) DefaultTrueBooleanDatabaseTableColumn (a tinyint column with length 0 , default value 0 and whose values cannot be null ) NotNullInt10DatabaseTableColumn (a int column with length 10 and whose values cannot be null ) NotNullVarchar191DatabaseTableColumn (a varchar column with length 191 and whose values cannot be null ) NotNullVarchar255DatabaseTableColumn (a varchar column with length 255 and whose values cannot be null ) ObjectIdDatabaseTableColumn (a int column with length 10 , whose values cannot be null , and whose values are auto-incremented) Examples: 1 2 3 4 5 6 7 DefaultFalseBooleanDatabaseTableColumn :: create ( 'isDisabled' ) NotNullInt10DatabaseTableColumn :: create ( 'fooTypeID' ) SmallintDatabaseTableColumn :: create ( 'bar' ) -> length ( 5 ) -> notNull ()","title":"Columns"},{"location":"package/database-php-api/#foreign-keys","text":"Foreign keys are represented by DatabaseTableForeignKey objects: 1 2 3 4 5 DatabaseTableForeignKey :: create () -> columns ([ 'fooID' ]) -> referencedTable ( 'wcf1_foo' ) -> referencedColumns ([ 'fooID' ]) -> onDelete ( 'CASCADE' ) The supported actions for onDelete() and onUpdate() are CASCADE , NO ACTION , and SET NULL . To drop a foreign key, all of the relevant data to create the foreign key has to be present and the drop() method has to be called. DatabaseTableForeignKey::create() also supports the foreign key name as a parameter. If it is not present, DatabaseTable::foreignKeys() will automatically set one based on the foreign key's data.","title":"Foreign Keys"},{"location":"package/database-php-api/#indices","text":"Indices are represented by DatabaseTableIndex objects: 1 2 3 DatabaseTableIndex :: create () -> type ( DatabaseTableIndex :: UNIQUE_TYPE ) -> columns ([ 'fooID' ]) There are four different types: DatabaseTableIndex::DEFAULT_TYPE (default), DatabaseTableIndex::PRIMARY_TYPE , DatabaseTableIndex::UNIQUE_TYPE , and DatabaseTableIndex::FULLTEXT_TYPE . For primary keys, there is also the DatabaseTablePrimaryIndex class which automatically sets the type to DatabaseTableIndex::PRIMARY_TYPE . To drop a index, all of the relevant data to create the index has to be present and the drop() method has to be called. DatabaseTableIndex::create() also supports the index name as a parameter. If it is not present, DatabaseTable::indices() will automatically set one based on the index data.","title":"Indices"},{"location":"package/package-xml/","text":"package.xml # The package.xml is the core component of every package. It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version. Example # package.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Simple Package A simple package to demonstrate the package system of WoltLab Suite Core 1.0.0 2016-12-18 YOUR NAME http://www.example.com com.woltlab.wcf com.woltlab.wcf templates.tar Elements # # The root node of every package.xml it contains the reference to the namespace and the location of the XML Schema Definition (XSD). The attribute name is the most important part, it holds the unique package identifier and is mandatory. It is based upon your domain name and the package name of your choice. For example WoltLab Suite Forum (formerly know an WoltLab Burning Board and usually abbreviated as wbb ) is created by WoltLab which owns the domain woltlab.com . The resulting package identifier is com.woltlab.wbb ( .. ). # Holds the entire meta data of the package. # This is the actual package name displayed to the end user, this can be anything you want, try to keep it short. It supports the attribute languagecode which allows you to provide the package name in different languages, please be aware that if it is not present, en (English) is assumed: 1 2 3 4 Simple Package Einfaches Paket # Brief summary of the package, use it to explain what it does since the package name might not always be clear enough. The attribute languagecode is available here too, please reference to for details. # The package's version number, this is a string consisting of three numbers separated with a dot and optionally followed by a keyword (must be followed with another number). The possible keywords are: Alpha/dev (both is regarded to be the same) Beta RC (release candidate) pl (patch level) Valid examples: 1.0.0 1.12.13 Alpha 19 7.0.0 pl 3 Invalid examples: 1.0.0 Beta (keyword Beta must be followed by a number) 2.0 RC 3 (version number must consists of 3 blocks of numbers) 1.2.3 dev 4.5 (4.5 is not an integer, 4 or 5 would be valid but not the fraction) # Must be a valid ISO 8601 date, e.g. 2013-12-27 . # Holds meta data regarding the package's author. # Can be anything you want. # (optional) URL to the author's website. # A list of packages including their version required for this package to work. # Example: 1 com.woltlab.wcf The attribute minversion must be a valid version number as described in . The file attribute is optional and specifies the location of the required package's archive relative to the package.xml . # A list of optional packages which can be selected by the user at the very end of the installation process. # Example: 1 com.woltlab.wcf.moderatedUserGroup The file attribute specifies the location of the optional package's archive relative to the package.xml . # List of packages which conflict with this package. It is not possible to install it if any of the specified packages is installed. In return you cannot install an excluded package if this package is installed. # Example: 1 com.woltlab.wcf The attribute version must be a valid version number as described in the \\ section. In the example above it will be impossible to install this package in WoltLab Suite Core 3.1.0 Alpha 1 or higher. # Available since WoltLab Suite 3.1 With the release of WoltLab Suite 5.2 the API versions were abolished. Instead of using API versions packages should exclude version 6.0.0 Alpha 1 of com.woltlab.wcf going forward. WoltLab Suite 3.1 introduced a new versioning system that focused around the API compatibility and is intended to replace the instruction for the Core for most plugins. The -tag holds a list of compatible API versions, and while only a single version is available at the time of writing, future versions will add more versions with backwards-compatibility in mind. Example: 1 2 3 Existing API versions # WoltLab Suite Core API-Version Backwards-Compatible to API-Version 3.1 2018 n/a # List of instructions to be executed upon install or update. The order is important, the topmost will be executed first. # List of instructions for a new installation of this package. # The attribute fromversion must be a valid version number as described in the \\ section and specifies a possible update from that very version to the package's version. The installation process will pick exactly one update instruction, ignoring everything else. Please read the explanation below! Example: Installed version: 1.0.0 Package version: 1.0.2 1 2 3 4 5 6 In this example WoltLab Suite Core will pick the first update block since it allows an update from 1.0.0 -> 1.0.2 . The other block is not considered, since the currently installed version is 1.0.0 . After applying the update block ( fromversion=\"1.0.0\" ), the version now reads 1.0.2 . # Example: 1 objectTypeDefinition.xml The attribute type specifies the instruction type which is used to determine the package installation plugin (PIP) invoked to handle its value. The value must be a valid file relative to the location of package.xml . Many PIPs provide default file names which are used if no value is given: 1 There is a list of all default PIPs available. Both the type -attribute and the element value are case-sensitive. Windows does not care if the file is called objecttypedefinition.xml but was referenced as objectTypeDefinition.xml , but both Linux and Mac systems will be unable to find the file. In addition to the type attribute, an optional run attribute (with standalone as the only valid value) is supported which forces the installation to execute this PIP in an isolated request, allowing a single, resource-heavy PIP to execute without encountering restrictions such as PHP\u2019s memory_limit or max_execution_time : 1 # Sometimes a package update should only adjust the metadata of the package, for example, an optional package was added. However, WoltLab Suite Core requires that the list of is non-empty. Instead of using a dummy that idempotently updates some PIP, the tag can be used for this use-case. Using the tag is only valid for and must not be accompanied by other tags. Example: 1 2 3 ","title":"package.xml"},{"location":"package/package-xml/#packagexml","text":"The package.xml is the core component of every package. It provides the meta data (e.g. package name, description, author) and the instruction set for a new installation and/or updating from a previous version.","title":"package.xml"},{"location":"package/package-xml/#example","text":"package.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Simple Package A simple package to demonstrate the package system of WoltLab Suite Core 1.0.0 2016-12-18 YOUR NAME http://www.example.com com.woltlab.wcf com.woltlab.wcf templates.tar ","title":"Example"},{"location":"package/package-xml/#elements","text":"","title":"Elements"},{"location":"package/package-xml/#package","text":"The root node of every package.xml it contains the reference to the namespace and the location of the XML Schema Definition (XSD). The attribute name is the most important part, it holds the unique package identifier and is mandatory. It is based upon your domain name and the package name of your choice. For example WoltLab Suite Forum (formerly know an WoltLab Burning Board and usually abbreviated as wbb ) is created by WoltLab which owns the domain woltlab.com . The resulting package identifier is com.woltlab.wbb ( .. ).","title":"<package>"},{"location":"package/package-xml/#packageinformation","text":"Holds the entire meta data of the package.","title":"<packageinformation>"},{"location":"package/package-xml/#packagename","text":"This is the actual package name displayed to the end user, this can be anything you want, try to keep it short. It supports the attribute languagecode which allows you to provide the package name in different languages, please be aware that if it is not present, en (English) is assumed: 1 2 3 4 Simple Package Einfaches Paket ","title":"<packagename>"},{"location":"package/package-xml/#packagedescription","text":"Brief summary of the package, use it to explain what it does since the package name might not always be clear enough. The attribute languagecode is available here too, please reference to for details.","title":"<packagedescription>"},{"location":"package/package-xml/#version","text":"The package's version number, this is a string consisting of three numbers separated with a dot and optionally followed by a keyword (must be followed with another number). The possible keywords are: Alpha/dev (both is regarded to be the same) Beta RC (release candidate) pl (patch level) Valid examples: 1.0.0 1.12.13 Alpha 19 7.0.0 pl 3 Invalid examples: 1.0.0 Beta (keyword Beta must be followed by a number) 2.0 RC 3 (version number must consists of 3 blocks of numbers) 1.2.3 dev 4.5 (4.5 is not an integer, 4 or 5 would be valid but not the fraction)","title":"<version>"},{"location":"package/package-xml/#date","text":"Must be a valid ISO 8601 date, e.g. 2013-12-27 .","title":"<date>"},{"location":"package/package-xml/#authorinformation","text":"Holds meta data regarding the package's author.","title":"<authorinformation>"},{"location":"package/package-xml/#author","text":"Can be anything you want.","title":"<author>"},{"location":"package/package-xml/#authorurl","text":"(optional) URL to the author's website.","title":"<authorurl>"},{"location":"package/package-xml/#requiredpackages","text":"A list of packages including their version required for this package to work.","title":"<requiredpackages>"},{"location":"package/package-xml/#requiredpackage","text":"Example: 1 com.woltlab.wcf The attribute minversion must be a valid version number as described in . The file attribute is optional and specifies the location of the required package's archive relative to the package.xml .","title":"<requiredpackage>"},{"location":"package/package-xml/#optionalpackage","text":"A list of optional packages which can be selected by the user at the very end of the installation process.","title":"<optionalpackage>"},{"location":"package/package-xml/#optionalpackage_1","text":"Example: 1 com.woltlab.wcf.moderatedUserGroup The file attribute specifies the location of the optional package's archive relative to the package.xml .","title":"<optionalpackage>"},{"location":"package/package-xml/#excludedpackages","text":"List of packages which conflict with this package. It is not possible to install it if any of the specified packages is installed. In return you cannot install an excluded package if this package is installed.","title":"<excludedpackages>"},{"location":"package/package-xml/#excludedpackage","text":"Example: 1 com.woltlab.wcf The attribute version must be a valid version number as described in the \\ section. In the example above it will be impossible to install this package in WoltLab Suite Core 3.1.0 Alpha 1 or higher.","title":"<excludedpackage>"},{"location":"package/package-xml/#compatibility","text":"Available since WoltLab Suite 3.1 With the release of WoltLab Suite 5.2 the API versions were abolished. Instead of using API versions packages should exclude version 6.0.0 Alpha 1 of com.woltlab.wcf going forward. WoltLab Suite 3.1 introduced a new versioning system that focused around the API compatibility and is intended to replace the instruction for the Core for most plugins. The -tag holds a list of compatible API versions, and while only a single version is available at the time of writing, future versions will add more versions with backwards-compatibility in mind. Example: 1 2 3 ","title":"<compatibility>"},{"location":"package/package-xml/#existing-api-versions","text":"WoltLab Suite Core API-Version Backwards-Compatible to API-Version 3.1 2018 n/a","title":"Existing API versions"},{"location":"package/package-xml/#instructions","text":"List of instructions to be executed upon install or update. The order is important, the topmost will be executed first.","title":"<instructions>"},{"location":"package/package-xml/#instructions-typeinstall","text":"List of instructions for a new installation of this package.","title":"<instructions type=\"install\">"},{"location":"package/package-xml/#instructions-typeupdate-fromversion","text":"The attribute fromversion must be a valid version number as described in the \\ section and specifies a possible update from that very version to the package's version. The installation process will pick exactly one update instruction, ignoring everything else. Please read the explanation below! Example: Installed version: 1.0.0 Package version: 1.0.2 1 2 3 4 5 6 In this example WoltLab Suite Core will pick the first update block since it allows an update from 1.0.0 -> 1.0.2 . The other block is not considered, since the currently installed version is 1.0.0 . After applying the update block ( fromversion=\"1.0.0\" ), the version now reads 1.0.2 .","title":"<instructions type=\"update\" fromversion=\"\u2026\">"},{"location":"package/package-xml/#instruction","text":"Example: 1 objectTypeDefinition.xml The attribute type specifies the instruction type which is used to determine the package installation plugin (PIP) invoked to handle its value. The value must be a valid file relative to the location of package.xml . Many PIPs provide default file names which are used if no value is given: 1 There is a list of all default PIPs available. Both the type -attribute and the element value are case-sensitive. Windows does not care if the file is called objecttypedefinition.xml but was referenced as objectTypeDefinition.xml , but both Linux and Mac systems will be unable to find the file. In addition to the type attribute, an optional run attribute (with standalone as the only valid value) is supported which forces the installation to execute this PIP in an isolated request, allowing a single, resource-heavy PIP to execute without encountering restrictions such as PHP\u2019s memory_limit or max_execution_time : 1 ","title":"<instruction>"},{"location":"package/package-xml/#void","text":"Sometimes a package update should only adjust the metadata of the package, for example, an optional package was added. However, WoltLab Suite Core requires that the list of is non-empty. Instead of using a dummy that idempotently updates some PIP, the tag can be used for this use-case. Using the tag is only valid for and must not be accompanied by other tags. Example: 1 2 3 ","title":"<void/>"},{"location":"package/pip/","text":"Package Installation Plugins # Package Installation Plugins (PIPs) are interfaces to deploy and edit content as well as components. For XML-based PIPs: must be used for language items and page contents. In all other cases it may only be used when necessary. Built-In PIPs # Name Description aclOption Customizable permissions for individual objects acpMenu Admin panel menu categories and items acpSearchProvider Data provider for the admin panel search acpTemplate Admin panel templates bbcode BBCodes for rich message formatting box Boxes that can be placed anywhere on a page clipboardAction Perform bulk operations on marked objects coreObject Access Singletons from within the template cronjob Periodically execute code with customizable intervals database Updates the database layout using the PHP API eventListener Register listeners for the event system file Deploy any type of files with the exception of templates language Language items mediaProvider Detect and convert links to media providers menu Side-wide and custom per-page menus menuItem Menu items for menus created through the menu PIP objectType Flexible type registry based on definitions objectTypeDefinition Groups objects and classes by functionality option Side-wide configuration options page Register page controllers and text-based pages pip Package Installation Plugins script Execute arbitrary PHP code during installation, update and uninstallation smiley Smileys sql Execute SQL instructions using a MySQL-flavored syntax (also see database PHP API ) style Style template Frontend templates templateListener Embed template code into templates without altering the original userGroupOption Permissions for user groups userMenu User menu categories and items userNotificationEvent Events of the user notification system userOption User settings userProfileMenu User profile tabs","title":"Overview"},{"location":"package/pip/#package-installation-plugins","text":"Package Installation Plugins (PIPs) are interfaces to deploy and edit content as well as components. For XML-based PIPs: must be used for language items and page contents. In all other cases it may only be used when necessary.","title":"Package Installation Plugins"},{"location":"package/pip/#built-in-pips","text":"Name Description aclOption Customizable permissions for individual objects acpMenu Admin panel menu categories and items acpSearchProvider Data provider for the admin panel search acpTemplate Admin panel templates bbcode BBCodes for rich message formatting box Boxes that can be placed anywhere on a page clipboardAction Perform bulk operations on marked objects coreObject Access Singletons from within the template cronjob Periodically execute code with customizable intervals database Updates the database layout using the PHP API eventListener Register listeners for the event system file Deploy any type of files with the exception of templates language Language items mediaProvider Detect and convert links to media providers menu Side-wide and custom per-page menus menuItem Menu items for menus created through the menu PIP objectType Flexible type registry based on definitions objectTypeDefinition Groups objects and classes by functionality option Side-wide configuration options page Register page controllers and text-based pages pip Package Installation Plugins script Execute arbitrary PHP code during installation, update and uninstallation smiley Smileys sql Execute SQL instructions using a MySQL-flavored syntax (also see database PHP API ) style Style template Frontend templates templateListener Embed template code into templates without altering the original userGroupOption Permissions for user groups userMenu User menu categories and items userNotificationEvent Events of the user notification system userOption User settings userProfileMenu User profile tabs","title":"Built-In PIPs"},{"location":"package/pip/acl-option/","text":"ACL Option Package Installation Plugin # Add customizable permissions for individual objects. Option Components # Each acl option is described as an
    ]]> ","title":"mediaProvider"},{"location":"package/pip/media-provider/#media-provider-package-installation-plugin","text":"Available since WoltLab Suite 3.1 Media providers are responsible to detect and convert links to a 3rd party service inside messages.","title":"Media Provider Package Installation Plugin"},{"location":"package/pip/media-provider/#components","text":"Each item is described as a element with the mandatory attribute name that should equal the lower-cased provider name. If a provider provides multiple components that are (largely) unrelated to each other, it is recommended to use a dash to separate the name and the component, e. g. youtube-playlist .","title":"Components"},{"location":"package/pip/media-provider/#title","text":"The title is displayed in the administration control panel and is only used there, the value is neither localizable nor is it ever exposed to regular users.","title":"<title>"},{"location":"package/pip/media-provider/#regex","text":"The regular expression used to identify links to this provider, it must not contain anchors or delimiters. It is strongly recommended to capture the primary object id using the (?P...) group.","title":"<regex>"},{"location":"package/pip/media-provider/#classname","text":" and are mutually exclusive. PHP-Callback-Class that is invoked to process the matched link in case that additional logic must be applied that cannot be handled through a simple replacement as defined by the element. The callback-class must implement the interface \\wcf\\system\\bbcode\\media\\provider\\IBBCodeMediaProvider .","title":"<className>"},{"location":"package/pip/media-provider/#html","text":" and are mutually exclusive. Replacement HTML that gets populated using the captured matches in , variables are accessed as {$VariableName} . For example, the capture group (?P...) is accessed using {$ID} .","title":"<html>"},{"location":"package/pip/media-provider/#example","text":"mediaProvider.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 YouTube [a-zA-Z0-9_-]+)(?:(?:\\?|&)t=(?P[0-9hms]+)$)?]]> YouTube Playlist [a-zA-Z0-9_-]+)]]> ]]> ","title":"Example"},{"location":"package/pip/menu-item/","text":"Menu Item Package Installation Plugin # Adds menu items to existing menus. Components # Each item is described as an element with the mandatory attribute identifier that should follow the naming pattern . , e.g. com.woltlab.wcf.Dashboard . # The target menu that the item should be added to, requires the internal identifier set by creating a menu through the menu.xml . # The language attribute is required and should specify the ISO-639-1 language code. The title is displayed as the link title of the menu item and can be fully customized by the administrator, thus is immutable after deployment. Supports multiple <title> elements to provide localized values. <page> # The page that the link should point to, requires the internal identifier set by creating a page through the page.xml . Example # menuItem.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?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 Dashboard com.woltlab.wcf.Dashboard ","title":"menuItem"},{"location":"package/pip/menu-item/#menu-item-package-installation-plugin","text":"Adds menu items to existing menus.","title":"Menu Item Package Installation Plugin"},{"location":"package/pip/menu-item/#components","text":"Each item is described as an element with the mandatory attribute identifier that should follow the naming pattern . , e.g. com.woltlab.wcf.Dashboard .","title":"Components"},{"location":"package/pip/menu-item/#menu","text":"The target menu that the item should be added to, requires the internal identifier set by creating a menu through the menu.xml .","title":"<menu>"},{"location":"package/pip/menu-item/#title","text":"The language attribute is required and should specify the ISO-639-1 language code. The title is displayed as the link title of the menu item and can be fully customized by the administrator, thus is immutable after deployment. Supports multiple elements to provide localized values.","title":"<title>"},{"location":"package/pip/menu-item/#page","text":"The page that the link should point to, requires the internal identifier set by creating a page through the page.xml .","title":"<page>"},{"location":"package/pip/menu-item/#example","text":"menuItem.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?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 Dashboard com.woltlab.wcf.Dashboard ","title":"Example"},{"location":"package/pip/menu/","text":"Menu Package Installation Plugin # Deploy and manage menus that can be placed anywhere on the site. Components # Each item is described as a element with the mandatory attribute identifier that should follow the naming pattern . , e.g. com.woltlab.wcf.MainMenu . # The language attribute is required and should specify the ISO-639-1 language code. The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple <title> elements. <box> # The following elements of the box PIP are supported, please refer to the documentation to learn more about them: <position> <showHeader> <visibleEverywhere> <visibilityExceptions> cssClassName Example # menu.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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 Footer Links footer boxMenuLinkGroup 0 1 ","title":"menu"},{"location":"package/pip/menu/#menu-package-installation-plugin","text":"Deploy and manage menus that can be placed anywhere on the site.","title":"Menu Package Installation Plugin"},{"location":"package/pip/menu/#components","text":"Each item is described as a element with the mandatory attribute identifier that should follow the naming pattern . , e.g. com.woltlab.wcf.MainMenu .","title":"Components"},{"location":"package/pip/menu/#title","text":"The language attribute is required and should specify the ISO-639-1 language code. The internal name displayed in the admin panel only, can be fully customized by the administrator and is immutable. Only one value is accepted and will be picked based on the site's default language, but you can provide localized values by including multiple elements.","title":"<title>"},{"location":"package/pip/menu/#box","text":"The following elements of the box PIP are supported, please refer to the documentation to learn more about them: <position> <showHeader> <visibleEverywhere> <visibilityExceptions> cssClassName","title":"<box>"},{"location":"package/pip/menu/#example","text":"menu.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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 Footer Links footer boxMenuLinkGroup 0 1 ","title":"Example"},{"location":"package/pip/object-type-definition/","text":"Object Type Definition Package Installation Plugin # Registers an object type definition. An object type definition is a blueprint for a certain behaviour that is particularized by objectTypes . As an example: Tags can be attached to different types of content (such as forum posts or gallery images). The bulk of the work is implemented in a generalized fashion, with all the tags stored in a single database table. Certain things, such as permission checking, need to be particularized for the specific type of content, though. Thus tags (or rather \u201ctaggable content\u201d) are registered as an object type definition. Posts are then registered as an object type, implementing the \u201ctaggable content\u201d behaviour. Other types of object type definitions include attachments, likes, polls, subscriptions, or even the category system. Components # Each item is described as a element with the mandatory child that should follow the naming pattern . , e.g. com.woltlab.wcf.example . # Optional The name of the PHP interface objectTypes have to implement. Example # objectTypeDefinition.xml 1 2 3 4 5 6 7 8 9 com.woltlab.wcf.example wcf\\system\\example\\IExampleObjectType ","title":"objectTypeDefinition"},{"location":"package/pip/object-type-definition/#object-type-definition-package-installation-plugin","text":"Registers an object type definition. An object type definition is a blueprint for a certain behaviour that is particularized by objectTypes . As an example: Tags can be attached to different types of content (such as forum posts or gallery images). The bulk of the work is implemented in a generalized fashion, with all the tags stored in a single database table. Certain things, such as permission checking, need to be particularized for the specific type of content, though. Thus tags (or rather \u201ctaggable content\u201d) are registered as an object type definition. Posts are then registered as an object type, implementing the \u201ctaggable content\u201d behaviour. Other types of object type definitions include attachments, likes, polls, subscriptions, or even the category system.","title":"Object Type Definition Package Installation Plugin"},{"location":"package/pip/object-type-definition/#components","text":"Each item is described as a element with the mandatory child that should follow the naming pattern . , e.g. com.woltlab.wcf.example .","title":"Components"},{"location":"package/pip/object-type-definition/#interfacename","text":"Optional The name of the PHP interface objectTypes have to implement.","title":"<interfacename>"},{"location":"package/pip/object-type-definition/#example","text":"objectTypeDefinition.xml 1 2 3 4 5 6 7 8 9 com.woltlab.wcf.example wcf\\system\\example\\IExampleObjectType ","title":"Example"},{"location":"package/pip/object-type/","text":"Object Type Package Installation Plugin # Registers an object type. Read about object types in the objectTypeDefinition PIP. Components # Each item is described as a element with the mandatory child that should follow the naming pattern . , e.g. com.woltlab.wcf.example . # The of the objectTypeDefinition . # The name of the class providing the object types's behaviour, the class has to implement the interface of the object type definition. <*> # Optional Additional fields may be defined for specific definitions of object types. Refer to the documentation of these for further explanation. Example # objectType.xml 1 2 3 4 5 6 7 8 9 10 11 com.woltlab.wcf.example com.woltlab.wcf.rebuildData wcf\\system\\worker\\ExampleRebuildWorker 130 ","title":"objectType"},{"location":"package/pip/object-type/#object-type-package-installation-plugin","text":"Registers an object type. Read about object types in the objectTypeDefinition PIP.","title":"Object Type Package Installation Plugin"},{"location":"package/pip/object-type/#components","text":"Each item is described as a element with the mandatory child that should follow the naming pattern . , e.g. com.woltlab.wcf.example .","title":"Components"},{"location":"package/pip/object-type/#definitionname","text":"The of the objectTypeDefinition .","title":"<definitionname>"},{"location":"package/pip/object-type/#classname","text":"The name of the class providing the object types's behaviour, the class has to implement the interface of the object type definition.","title":"<classname>"},{"location":"package/pip/object-type/#_1","text":"Optional Additional fields may be defined for specific definitions of object types. Refer to the documentation of these for further explanation.","title":"<*>"},{"location":"package/pip/object-type/#example","text":"objectType.xml 1 2 3 4 5 6 7 8 9 10 11 com.woltlab.wcf.example com.woltlab.wcf.rebuildData wcf\\system\\worker\\ExampleRebuildWorker 130 ","title":"Example"},{"location":"package/pip/option/","text":"Option Package Installation Plugin # Registers new options. Options allow the administrator to configure the behaviour of installed packages. The specified values are exposed as PHP constants. Category Components # Each category is described as an element with the mandatory attribute name . # Optional The category\u2019s parent category. # Optional Specifies the order of this option within the parent category. # Optional The options element can contain a comma-separated list of options of which at least one needs to be enabled for the category to be shown to the administrator. Option Components # Each option is described as an
    { lang } wcf.person.birthday { /lang } { if $person -> birthday }{ @ $person -> birthday | strtotime | date }{ /if } { lang } wcf.person.birthday { /lang } { if $person -> birthday }{ @ $person -> birthday | strtotime | date }{ /if }
     1
    +
    +
    files/acp/database/install_com.woltlab.wcf.people.php
    +
     1
      2
      3
      4
    @@ -2558,26 +2560,30 @@ Thus, the database table we will store the people in only contains three columns
     11
     12
     13
    -14
    <?php
    +14
    <?php
     
    -use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn;
    -use wcf\system\database\table\column\ObjectIdDatabaseTableColumn;
    -use wcf\system\database\table\DatabaseTable;
    +use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn;
    +use wcf\system\database\table\column\ObjectIdDatabaseTableColumn;
    +use wcf\system\database\table\DatabaseTable;
     
     return [
    -    DatabaseTable::create('wcf1_person')
    -        ->columns([
    -            ObjectIdDatabaseTableColumn::create('personID'),
    -            NotNullVarchar255DatabaseTableColumn::create('firstName'),
    -            NotNullVarchar255DatabaseTableColumn::create('lastName'),
    +    DatabaseTable::create('wcf1_person')
    +        ->columns([
    +            ObjectIdDatabaseTableColumn::create('personID'),
    +            NotNullVarchar255DatabaseTableColumn::create('firstName'),
    +            NotNullVarchar255DatabaseTableColumn::create('lastName'),
             ]),
     ];
     
    +
    +

    Database Object#

    Person#

    In our PHP code, each person will be represented by an object of the following class:

    -
     1
    +
    +
    files/lib/data/person/Person.class.php
    +
     1
      2
      3
      4
    @@ -2656,12 +2662,16 @@ Thus, the database table we will store the people in only contains three columns
     }
     
    +
    +

    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() method for convenience.

    For every database object, you need to implement three additional classes: an action class, an editor class and a list class.

    PersonAction#

    -
     1
    +
    +
    files/lib/data/person/PersonAction.class.php
    +
     1
      2
      3
      4
    @@ -2722,13 +2732,17 @@ an action class, an editor class and a list class.

    }
    +
    +

    This implementation of AbstractDatabaseObjectAction is very basic and only sets the $permissionsDelete and $requireACP properties. This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX. $permissionsDelete has to be set to the permission needed in order to delete a person. We will later use the userGroupOption package installation plugin to create the admin.content.canManagePeople permission. $requireACP restricts deletion of people to the ACP.

    PersonEditor#

    -
     1
    +
    +
    files/lib/data/person/PersonEditor.class.php
    +
     1
      2
      3
      4
    @@ -2779,10 +2793,14 @@ We will later use the userGrou
     }
     
    +
    +

    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#

    -
     1
    +
    +
    files/lib/data/person/PersonList.class.php
    +
     1
      2
      3
      4
    @@ -2827,6 +2845,8 @@ setting the static $baseClass property to the database object class
     }
     
    +
    +

    Due to the default implementation of DatabaseObjectList, our PersonList class just needs to extend it and everything else is either automatically set by the code of DatabaseObjectList or, in the case of properties and methods, provided by that class.

    ACP#

    Next, we will take care of the controllers and views for the ACP. @@ -2844,7 +2864,9 @@ In total, we need three each:

  • a third level menu item for the people list page, and
  • a fourth level menu item for the form to add new people.
  • -
     1
    +
    +
    acpMenu.xml
    +
     1
      2
      3
      4
    @@ -2883,12 +2905,16 @@ In total, we need three each:

    </data>
    +
    +

    We choose wcf.acp.menu.link.content as the parent menu item for the first menu item wcf.acp.menu.link.person because the people we are managing is just one form of content. The fourth level menu item wcf.acp.menu.link.person.add will only be shown as an icon and thus needs an additional element icon which takes a FontAwesome icon class as value.

    People List#

    To list the people in the ACP, we need a PersonListPage class and a personList template.

    PersonListPage#

    -
     1
    +
    +
    files/lib/data/person/PersonListPage.class.php
    +
     1
      2
      3
      4
    @@ -2963,6 +2989,8 @@ The fourth level menu item wcf.acp.menu.link.person.add will only b
     }
     
    +
    +

    As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:

    1. We need to set the active ACP menu item via the $activeMenuItem.
    2. @@ -2972,7 +3000,9 @@ The fourth level menu item wcf.acp.menu.link.person.add will only b
    3. To validate the sort field passed with the request, we set $validSortFields to the available database table columns.

    personList.tpl#

    -
     1
    +
    +
    acptemplates/personList.tpl
    +
     1
      2
      3
      4
    @@ -3123,6 +3153,8 @@ The fourth level menu item wcf.acp.menu.link.person.add will only b
     {include file='footer'}
     
    +
    +

    We will go piece by piece through the template code:

    1. We include the header template and set the page title wcf.acp.person.list. @@ -3148,7 +3180,9 @@ The fourth level menu item wcf.acp.menu.link.person.add will only b

      Person Add Form#

      Like the person list, the form to add new people requires a controller class and a template.

      PersonAddForm#

      -
       1
      +
      +
      files/lib/acp/form/PersonAddForm.class.php
      +
       1
        2
        3
        4
      @@ -3287,6 +3321,8 @@ The fourth level menu item wcf.acp.menu.link.person.add will only b
       }
       
      +
      +

      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, 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 used to generate the form.

      Because of using form builder, we only have to set up the two form fields for entering the first and last name, respectively:

      @@ -3298,7 +3334,9 @@ the “housekeeping” properties $activeMenuItem and $needed
    2. Lastly, to make it easier to fill out the form more quickly, the first field is auto-focused by calling autoFocus().
    3. personAdd.tpl#

      -
       1
      +
      +
      acptemplates/personAdd.tpl
      +
       1
        2
        3
        4
      @@ -3337,6 +3375,8 @@ the “housekeeping” properties $activeMenuItem and $needed
       {include file='footer'}
       
      +
      +

      We will now only concentrate on the new parts compared to personList.tpl:

      1. We use the $action variable to distinguish between the languages items used for adding a person and for creating a person.
      2. @@ -3345,7 +3385,9 @@ the “housekeeping” properties $activeMenuItem and $needed

        Person Edit Form#

        As mentioned before, for the form to edit existing people, we only need a new controller as the template has already been implemented in a way that it handles both, adding and editing.

        PersonEditForm#

        -
         1
        +
        +
        files/lib/acp/form/PersonEditForm.class.php
        +
         1
          2
          3
          4
        @@ -3432,6 +3474,8 @@ the “housekeeping” properties $activeMenuItem and $needed
         }
         
        +
        +

        In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.

        After setting a different active menu item, we have to change the value of $formAction because this form, in contrast to PersonAddForm, does not create but update existing persons.

        As we rely on form builder, the only thing necessary in this controller is to read and validate the edit object, i.e. the edited person, which is done in readParameters().

        @@ -3440,7 +3484,9 @@ the “housekeeping” properties $activeMenuItem and $needed This page should also be directly linked in the main menu.

        page.xml#

        First, let us register the page with the system because every front end page or form needs to be explicitly registered using the page package installation plugin:

        -
         1
        +
        +
        page.xml
        +
         1
          2
          3
          4
        @@ -3477,10 +3523,14 @@ This page should also be directly linked in the main menu.

        </data>
        +
        +

        For more information about what each of the elements means, please refer to the page package installation plugin page.

        Next, we register the menu item using the menuItem package installation plugin:

        -
         1
        +
        +
        menuItem.xml
        +
         1
          2
          3
          4
        @@ -3503,13 +3553,17 @@ This page should also be directly linked in the main menu.

        </data>
        +
        +

        Here, the import parts are that we register the menu item for the main menu com.woltlab.wcf.MainMenu and link the menu item with the page com.woltlab.wcf.people.PersonList, which we just registered.

        People List#

        As in the ACP, we need a controller and a template. You might notice that both the controller’s (unqualified) class name and the template name are the same for the ACP and the front end. This is no problem because the qualified names of the classes differ and the files are stored in different directories and because the templates are installed by different package installation plugins and are also stored in different directories.

        PersonListPage#

        -
         1
        +
        +
        files/lib/page/PersonListPage.class.php
        +
         1
          2
          3
          4
        @@ -3572,12 +3626,16 @@ This is no problem because the qualified names of the classes differ and the fil
         }
         
        +
        +

        This class is almost identical to the ACP version. In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page. Furthermore, $neededPermissions has not been set because in the front end, users do not need any special permission to access the page. In the front end, we explicitly set the $defaultSortField so that the people listed on the page are sorted by their last name (in ascending order) by default.

        personList.tpl#

        -
          1
        +
        +
        templates/personList.tpl
        +
          1
           2
           3
           4
        @@ -3782,6 +3840,8 @@ In the front end, we explicitly set the $defaultSortField so that t
         {include file='footer'}
         
        +
        +

        If you compare this template to the one used in the ACP, you will recognize similar elements like the .paginationTop element, the p.info element if no people exist, and the .contentFooter element. Furthermore, we include a template called header before actually showing any of the page contents and terminate the template by including the footer template.

        Now, let us take a closer look at the differences:

        @@ -3800,7 +3860,9 @@ Furthermore, we include a template called header before actually sh

        userGroupOption.xml#

        We have already used the admin.content.canManagePeople permissions several times, now we need to install it using the userGroupOption package installation plugin:

        -
         1
        +
        +
        userGroupOption.xml
        +
         1
          2
          3
          4
        @@ -3829,6 +3891,8 @@ Furthermore, we include a template called header before actually sh
         </data>
         
        +
        +

        We use the existing admin.content user group option category for the permission as the people are “content” (similar the the ACP menu item). As the permission is for administrators only, we set defaultvalue to 0 and admindefaultvalue to 1. This permission is only relevant for registered users so that it should not be visible when editing the guest user group. @@ -3836,7 +3900,9 @@ This is achieved by setting usersonly to 1.

        package.xml#

        Lastly, we need to create the package.xml file. For more information about this kind of file, please refer to the package.xml page.

        -
         1
        +
        +
        package.xml
        +
         1
          2
          3
          4
        @@ -3907,6 +3973,8 @@ For more information about this kind of file, please refer to </package>
         
        +
        +

        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. Additionally, we disallow installation of the package in the next major version 6.0 by excluding the 6.0.0 Alpha 1 version.

        @@ -3927,7 +3995,7 @@ As the menu item package installation plugin validates the given page and throws
        - Last update: 2021-04-20 + Last update: 2021-04-23
        diff --git a/5.4/tutorial/series/part_2/index.html b/5.4/tutorial/series/part_2/index.html index 55132f45..1364b076 100644 --- a/5.4/tutorial/series/part_2/index.html +++ b/5.4/tutorial/series/part_2/index.html @@ -2165,7 +2165,9 @@ As in the first part, we will not bother with careful validation of the entered

        Extending Person Model#

        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:

        -
         1
        +
        +
        files/acp/database/install_com.woltlab.wcf.people.birthday.php
        +
         1
          2
          3
          4
        @@ -2188,10 +2190,14 @@ To add the birthday to the model, we need to create an additional database table
         ];
         
        +
        +

        If we have a Person object, this new property can be accessed the same way as the personID property, the firstName property, or the lastName property from the base package: $person->birthday.

        Setting Birthday in ACP#

        To set the birthday of a person, we only have to add another form field with an event listener:

        -
         1
        +
        +
        files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php
        +
         1
          2
          3
          4
        @@ -2260,6 +2266,8 @@ To add the birthday to the model, we need to create an additional database table
         }
         
        +
        +

        registered via

        1
         2
        @@ -2280,7 +2288,9 @@ To add the birthday to the model, we need to create an additional database table
         

        As BirthdayPersonAddFormListener extends AbstractEventListener and as the name of relevant event is createForm, AbstractEventListener internally automatically calls onCreateForm() with the event object as the parameter. It is important to set <inherit>1</inherit> so that the event listener is also executed for PersonEditForm, which extends PersonAddForm.

        The language item wcf.person.birthday used in the label is the only new one for this package:

        -
        1
        +
        +
        language/de.xml
        +
        1
         2
         3
         4
        @@ -2293,7 +2303,11 @@ It is important to set <inherit>1</inherit> so that the
         </language>
         
        -
        1
        +
        + +
        +
        language/en.xml
        +
        1
         2
         3
         4
        @@ -2306,6 +2320,8 @@ It is important to set <inherit>1</inherit> so that the
         </language>
         
        +
        +

        Adding Birthday Table Column in ACP#

        To add a birthday column to the person list page in the ACP, we need three parts:

          @@ -2314,7 +2330,9 @@ It is important to set <inherit>1</inherit> so that the
        1. a template listener that adds the birthday column to the table’s rows.

        The first part is a very simple class:

        -
         1
        +
        +
        files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php
        +
         1
          2
          3
          4
        @@ -2363,6 +2381,8 @@ It is important to set <inherit>1</inherit> so that the
         }
         
        +
        +

        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.

        @@ -2379,15 +2399,21 @@ The code for the table head is similar to the other th elements:

        In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person’s “statistics”.

        To add the birthday as a valid sort field, we use BirthdaySortFieldPersonListPageListener just as in the ACP. In the front end, we will now use a template (__personListBirthdaySortField.tpl) instead of a directly putting the template code in the templateListener.xml file:

        -
        1
        <option value="birthday"{if $sortField == 'birthday'} selected{/if}>{lang}wcf.person.birthday{/lang}</option>
        +
        +
        templates/__personListBirthdaySortField.tpl
        +
        1
        <option value="birthday"{if $sortField == 'birthday'} selected{/if}>{lang}wcf.person.birthday{/lang}</option>
         
        +
        +

        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.

        Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable.

        To show the birthday, we use the following template code for the personStatistics template event, which again makes sure that the birthday is only shown if it is actually set:

        -
        1
        +
        +
        templates/__personListBirthday.tpl
        +
        1
         2
         3
         4
        {if $person->birthday}
        @@ -2396,9 +2422,13 @@ In the front end, we will now use a template (__personListBirthdaySortFiel
         {/if}
         
        +
        +

        templateListener.xml#

        The following code shows the templateListener.xml file used to install all mentioned template listeners:

        -
         1
        +
        +
        templateListener.xml
        +
         1
          2
          3
          4
        @@ -2467,10 +2497,14 @@ In the front end, we will now use a template (__personListBirthdaySortFiel
         </data>
         
        +
        +

        In cases where a template is used, we simply use the include syntax to load the template.

        eventListener.xml#

        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.

        -
         1
        +
        +
        eventListener.xml
        +
         1
          2
          3
          4
        @@ -2529,9 +2563,13 @@ In the front end, we will now use a template (__personListBirthdaySortFiel
         </data>
         
        +
        +

        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>):

        -
         1
        +
        +
        package.xml
        +
         1
          2
          3
          4
        @@ -2598,6 +2636,8 @@ In the front end, we will now use a template (__personListBirthdaySortFiel
         </package>
         
        +
        +

        This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people.

        The complete source code of this part can be found on GitHub.

        @@ -2609,7 +2649,7 @@ In the front end, we will now use a template (__personListBirthdaySortFiel
        - Last update: 2021-04-20 + Last update: 2021-04-23
        diff --git a/5.4/tutorial/series/part_3/index.html b/5.4/tutorial/series/part_3/index.html index 5dd8ad16..0d7ed8ac 100644 --- a/5.4/tutorial/series/part_3/index.html +++ b/5.4/tutorial/series/part_3/index.html @@ -2218,7 +2218,9 @@ To make good use of this new page and introduce a new API of WoltLab Suite, we w

        Runtime Cache#

        To reduce the number of database queries when different APIs require person objects, we implement a runtime cache for people:

        -
         1
        +
        +
        files/lib/system/cache/runtime/PersonRuntimeCache.class.php
        +
         1
          2
          3
          4
        @@ -2271,10 +2273,14 @@ To make good use of this new page and introduce a new API of WoltLab Suite, we w
         }
         
        +
        +

        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:

        -
         1
        +
        +
        objectType.xml
        +
         1
          2
          3
          4
        @@ -2295,8 +2301,12 @@ This is done by registering a com.woltlab.wcf.comment.commentableContent</data>
         
        +
        +

        The PersonCommentManager class extended ICommentManager’s default implementation AbstractCommentManager:

        -
         1
        +
        +
        files/lib/system/comment/manager/PersonCommentManager.class.php
        +
         1
          2
          3
          4
        @@ -2477,6 +2487,8 @@ This is done by registering a com.woltlab.wcf.comment.commentableContent}
         
        +
        +
        • First, the system is told the names of the permissions via the $permission* properties. More information about comment permissions can be found here.
        • @@ -2495,7 +2507,9 @@ With this option, comments on individual people can be disabled.

        Person Page#

        PersonPage#

        -
          1
        +
        +
        files/lib/page/PersonPage.class.php
        +
          1
           2
           3
           4
        @@ -2706,12 +2720,16 @@ With this option, comments on individual people can be disabled.

        }
        +
        +

        The PersonPage class is similar to the PersonEditForm in the ACP in that it reads the id of the requested person from the request data and validates the id in readParameters(). The rest of the code only handles fetching the list of comments on the requested person. In readData(), this list is fetched using CommentHandler::getCommentList() if comments are enabled for the person. The assignVariables() method assigns some additional template variables like $commentCanAdd, which is 1 if the active person can add comments and is 0 otherwise, $lastCommentTime, which contains the UNIX timestamp of the last comment, and $likeData, which contains data related to the likes for the disabled comments.

        person.tpl#

        -
         1
        +
        +
        templates/person.tpl
        +
         1
          2
          3
          4
        @@ -2802,13 +2820,17 @@ The assignVariables() method assigns some additional template varia
         {include file='footer'}
         
        +
        +

        For now, the person template is still very empty and only shows the comments in the content area. The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container ul#personCommentList element for the comments shown by commentList template. The ul#personCommentList elements has five additional data- attributes required by the JavaScript API for comments for loading more comments or creating new ones. The commentListAddComment template adds the WYSIWYG support. The attribute wysiwygSelector should be the id of the comment list personCommentList with an additional AddComment suffix.

        page.xml#

        -
         1
        +
        +
        page.xml
        +
         1
          2
          3
          4
        @@ -2863,6 +2885,8 @@ The attribute wysiwygSelector should be the id of the comment list
         </data>
         
        +
        +

        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:

          @@ -2874,7 +2898,9 @@ Compared to the pre-existing com.woltlab.wcf.people.PersonList page In general, the details page for any type of object that is listed on a different page has the list page as its parent.

        PersonPageHandler#

        -
          1
        +
        +
        files/lib/system/page/handler/PersonPageHandler.class.php
        +
          1
           2
           3
           4
        @@ -3083,6 +3109,8 @@ Compared to the pre-existing com.woltlab.wcf.people.PersonList page
         }
         
        +
        +

        Like any page handler, the PersonPageHandler class has to implement the IMenuPageHandler interface, which should be done by extending the AbstractMenuPageHandler class. As we want administrators to link to specific people in menus, for example, we have to also implement the ILookupPageHandler interface by extending the AbstractLookupPageHandler class.

        For the ILookupPageHandler interface, we need to implement three methods:

        @@ -3115,7 +3143,7 @@ The IOnlineLocationPageHandler interface requires two methods to be
        - Last update: 2021-04-20 + Last update: 2021-04-23
        diff --git a/5.4/tutorial/series/part_4/index.html b/5.4/tutorial/series/part_4/index.html index 43b9905c..c370ddb4 100644 --- a/5.4/tutorial/series/part_4/index.html +++ b/5.4/tutorial/series/part_4/index.html @@ -2088,7 +2088,9 @@ To do so, we first have to register a new object type for this person list box c

        The com.woltlab.wcf.boxController object type definition requires the provided class to implement wcf\system\box\IBoxController:

        -
         1
        +
        +
        files/lib/system/box/PersonListBoxController.class.php
        +
         1
          2
          3
          4
        @@ -2227,6 +2229,8 @@ To do so, we first have to register a new object type for this person list box c
         }
         
        +
        +

        By extending AbstractDatabaseObjectListBoxController, we only have to provide minimal data ourself and rely mostly on the default implementation provided by AbstractDatabaseObjectListBoxController:

        1. As we will support conditions for the listed people, we have to set the relevant condition definition via $conditionDefinition.
        2. @@ -2285,7 +2289,9 @@ In our case, the box system supports conditions to filter the objects shown in a Admittedly, our current person implementation only contains minimal data so that filtering might not make the most sense here but it will still show how to use the condition system for boxes. We will support filtering the people by their first and last name so that, for example, a box can be created listing all people with a specific first name.

          The first step for condition support is to register a object type definition for the relevant conditions requiring the IObjectListCondition interface:

          -
          1
          +
          +
          objectTypeDefinition.xml
          +
          1
           2
           3
           4
          @@ -2304,6 +2310,8 @@ We will support filtering the people by their first and last name so that, for e
           </data>
           
          +
          +

          Next, we register the specific conditions for filtering by the first and last name using this object type condition:

           1
            2
          @@ -2327,7 +2335,9 @@ We will support filtering the people by their first and last name so that, for e
           

          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:

          -
           1
          +
          +
          files/lib/system/condition/person/PersonFirstNameTextPropertyCondition.class.php
          +
           1
            2
            3
            4
          @@ -2422,6 +2432,8 @@ We will support filtering the people by their first and last name so that, for e
           }
           
          +
          +
          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.
          2. 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.
          3. @@ -2437,7 +2449,7 @@ The wcf\system\condition namespace also contains several other defa
            - Last update: 2021-04-20 + Last update: 2021-04-23
            diff --git a/5.4/tutorial/series/part_5/index.html b/5.4/tutorial/series/part_5/index.html index 97407cab..f08347a0 100644 --- a/5.4/tutorial/series/part_5/index.html +++ b/5.4/tutorial/series/part_5/index.html @@ -2160,7 +2160,9 @@ To make use of those APIs, we need content generated by users in the frontend.

            Person Information Model#

            The PHP file with the database layout has been updated as follows:

            -
             1
            +
            +
            files/acp/database/install_com.woltlab.wcf.people.php
            +
             1
              2
              3
              4
            @@ -2271,13 +2273,17 @@ To make use of those APIs, we need content generated by users in the frontend.];
             
            +
            +
            • 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.
            -
             1
            +
            +
            files/lib/data/person/information/PersonInformation.class.php
            +
             1
              2
              3
              4
            @@ -2470,6 +2476,8 @@ To make use of those APIs, we need content generated by users in the frontend.}
             
            +
            +

            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.

            There also two getter methods for the person, the piece of information belongs to (getPerson()), and for the user profile of the user who created the information (getUserProfile()). @@ -2478,7 +2486,9 @@ For such a case, we also save the name of the user who created the information i The most interesting method is getFormattedInformation(), which returns the HTML code of the information text meant for output. To generate such an output, HtmlOutputProcessor::process() is used and here is where we first use the associated message object type com.woltlab.wcf.people.information mentioned before.

            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:

            -
             1
            +
            +
            files/lib/data/person/information/PersonInformationList.class.php
            +
             1
              2
              3
              4
            @@ -2553,9 +2563,13 @@ To generate such an output, HtmlOutputProcessor::process() is used
             }
             
            +
            +

            Listing and Deleting Person Information#

            The person.tpl template has been updated to include a block for listing the information at the beginning:

            -
              1
            +
            +
            templates/person.tpl
            +
              1
               2
               3
               4
            @@ -2838,6 +2852,8 @@ To generate such an output, HtmlOutputProcessor::process() is used
             {include file='footer'}
             
            +
            +

            To keep things simple here, we reuse the structure and CSS classes used for comments. Additionally, we always list all pieces of information. If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript.

            @@ -2850,7 +2866,9 @@ Additionally, we extract the information text from the information_htmlInp

            Creating and Editing Person Information#

            To create new pieces of information or editing existing ones, we do not add new form controllers but instead use dialogs generated by the form builder API so that the user does not have to leave the person page.

            When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form:

            -
             1
            +
            +
            ts/WoltLabSuite/Core/Controller/Person.ts
            +
             1
              2
              3
              4
            @@ -3049,6 +3067,8 @@ Additionally, we extract the information text from the information_htmlInp
             }
             
            +
            +

            We use the WoltLabSuite/Core/Form/Builder/Dialog module, 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.

            Explanation of the initialization arguments for WoltLabSuite/Core/Form/Builder/Dialog used here:

            @@ -3069,7 +3089,9 @@ We only have to provide some data during for initializing these objects and call

            Next, we focus on PersonInformationAction, which actually provides the contents of these dialogs and creates and edits the information:

            -
              1
            +
            +
            files/lib/data/person/information/PersonInformationAction.class.php
            +
              1
               2
               3
               4
            @@ -3676,6 +3698,8 @@ We only have to provide some data during for initializing these objects and call
             }
             
            +
            +

            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. As the forms for adding and editing pieces of information have the same structure, this form is created in buildDialog() using a DialogFormDocument object, which is intended for forms in dialogs. @@ -3881,7 +3905,9 @@ After editing a piece of information, we do not reload the page but dynamically

            Lastly, we present the updated eventListener.xml file with new entries for all of these event listeners:

            -
             1
            +
            +
            eventListener.xml
            +
             1
              2
              3
              4
            @@ -3940,6 +3966,7 @@ After editing a piece of information, we do not reload the page but dynamically
             </data>
             
            +
            @@ -3948,7 +3975,7 @@ After editing a piece of information, we do not reload the page but dynamically
            - Last update: 2021-04-22 + Last update: 2021-04-23
            -- 2.20.1