-
.github/dependabot.yml<pre>@@ -0,0 +1,26 @@+# YoureCom Standard: managed by _dev/standard (apply.php), do not edit here.+# Major updates of the npm tools are skipped here, they are raised once for all plugins in _dev/standard+# (initial/package.json) and the shared ../node_modules. Updates of the GitHub Actions belong into+# _dev/standard/managed/.github/workflows as well, a pull request here shows that a new version exists.+version: 2+updates:+ - package-ecosystem: github-actions+ directory: /+ schedule:+ interval: monthly+ groups:+ actions:+ patterns:+ - "*"++ - package-ecosystem: npm+ directory: /+ schedule:+ interval: monthly+ ignore:+ - dependency-name: "*"+ update-types: ["version-update:semver-major"]+ groups:+ dev-tools:+ patterns:+ - "*"
-
.github/scripts/build-package.sh<pre>@@ -0,0 +1,110 @@+#!/usr/bin/env bash+# YoureCom Standard: managed by _dev/standard (apply.php), do not edit here.+# Builds the installable package (WoltLab format) into the given directory, default: var/package.+# Used by the release workflow and locally. Uploading stays a manual step that needs Marcel's approval.+#+# Usage: bash .github/scripts/build-package.sh [output dir]+# Prints the path of the created archive. Locally the archive is also copied to C:\laragon\www\archive\<package>.tar.gz+# (override the folder with ARCHIVE_DIR).+set -euo pipefail++ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"+OUT="${1:-$ROOT/var/package}"+STAGE="$OUT/stage"++PACKAGE="$(sed -n 's:.*<package name="\([^"]*\)".*:\1:p' "$ROOT/package.xml" | head -1)"+VERSION="$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' "$ROOT/package.xml" | head -1)"+[[ -n "$PACKAGE" && -n "$VERSION" ]] || { echo "package.xml has no name or version" >&2; exit 1; }++# the package ships the committed JavaScript, nothing compiles it here: a stale or missing build would load fine locally+# (deploy.sh compiles into the dev system) and fail on the live site, so stop before packing+if [[ -f "$ROOT/tests/lint/check-js-build.mjs" ]]; then+ (cd "$ROOT" && node tests/lint/check-js-build.mjs) || { echo "the committed JavaScript is out of date, run: npm run build" >&2; exit 1; }+fi++rm -rf "$STAGE"+mkdir -p "$STAGE"++# the archives of the file and template PIPs, named as in package.xml (the default names when no name is given)+pack() { # pack <source dir> <archive>+ [[ -d "$ROOT/$1" ]] || return 0+ (cd "$ROOT/$1" && tar --format=ustar -cf "$STAGE/$2" --exclude='.DS_Store' --exclude='*.orig' --exclude='*.bak' -- *)+}+pack files files.tar+pack files_wcf files_wcf.tar+pack templates templates.tar+pack acptemplates acptemplates.tar++# package.xml, the PIP files and the language files; development files stay outside+for FILE in "$ROOT"/*.xml; do+ case "$(basename "$FILE")" in+ phpunit*.xml) ;;+ *) cp "$FILE" "$STAGE/" ;;+ esac+done+# packages without phrases (e.g. branding free) have no language folder+if [[ -d "$ROOT/language" ]]; then+ cp -r "$ROOT/language" "$STAGE/language"+fi+# packages delivered with the plugin (<requiredpackage file="requirements/...">)+if [[ -d "$ROOT/requirements" ]]; then+ cp -r "$ROOT/requirements" "$STAGE/requirements"+fi++ARCHIVE="$OUT/${PACKAGE}_$(echo "$VERSION" | tr ' ' '_').tar.gz"+rm -f "$ARCHIVE"+(cd "$STAGE" && tar --format=ustar -czf "$ARCHIVE" -- *)+rm -rf "$STAGE"++# the archive contains everything package.xml refers to: the named files of the instructions and required packages,+# the default files of instructions without a name (file -> files.tar, language -> language/*.xml, <pip> -> <pip>.xml)+# and no pax headers (WoltLab reads ustar)+CONTENTS="$(tar -tzf "$ARCHIVE")"+if grep -q 'PaxHeader' <<< "$CONTENTS"; then+ echo "the archive contains pax headers" >&2+ exit 1+fi+php -r '+ $contents = array_flip(explode("\n", $argv[2]));+ $xml = new DOMDocument();+ $xml->load($argv[1]);+ $xpath = new DOMXPath($xml);+ $xpath->registerNamespace("w", "http://www.woltlab.com");+ $defaults = ["file" => "files.tar", "template" => "templates.tar", "acpTemplate" => "acptemplates.tar"];+ $missing = [];+ foreach ($xpath->query("//w:requiredpackage[@file]") as $node) {+ $missing[] = $node->getAttribute("file");+ }+ foreach ($xpath->query("//w:instruction") as $node) {+ $type = $node->getAttribute("type");+ $name = trim($node->textContent);+ if ($name === "") {+ if ($type === "language") {+ if (!preg_grep("~^language/\w+\.xml$~", array_keys($contents))) {+ $missing[] = "language/*.xml";+ }+ continue;+ }+ $name = $defaults[$type] ?? "{$type}.xml";+ } elseif (in_array($type, ["script", "database", "sql"], true)) {+ // these refer to files inside files.tar+ continue;+ }+ $missing[] = $name;+ }+ $missing = array_values(array_unique(array_filter($missing, static fn (string $file): bool => !isset($contents[$file]))));+ if ($missing !== []) {+ fwrite(STDERR, "package.xml refers to files the archive does not contain: " . implode(", ", $missing) . "\n");+ exit(1);+ }+' -- "$ROOT/package.xml" "$CONTENTS" || exit 1++# local builds: the finished package is also put into the archive folder as <package>.tar.gz, an existing file is+# overwritten (the folder does not exist on the CI runner, there the step is skipped)+ARCHIVE_DIR="${ARCHIVE_DIR:-/c/laragon/www/archive}"+if [[ -d "$ARCHIVE_DIR" ]]; then+ cp -f "$ARCHIVE" "$ARCHIVE_DIR/$PACKAGE.tar.gz"+ echo "copied to $ARCHIVE_DIR/$PACKAGE.tar.gz" >&2+fi++echo "$ARCHIVE"
-
.github/scripts/checkout-dependencies.sh<pre>@@ -0,0 +1,28 @@+#!/usr/bin/env bash+# YoureCom Standard: clones the required YoureCom packages of package.xml next to the plugin (CI only), so PHPStan+# finds their classes like in the local plugins folder. Managed by _dev/standard (apply.php), do not edit here.+#+# Usage: TOKEN=<token> bash .github/scripts/checkout-dependencies.sh <plugin dir>+set -euo pipefail++PLUGIN="${1:-.}"+BRANCH="${DEPENDENCY_BRANCH:-6.2}"+mapfile -t PACKAGES < <(sed -n 's:.*<requiredpackage[^>]*>\(de\.yourecom\.[^<]*\)</requiredpackage>.*:\1:p' "$PLUGIN/package.xml")++if [[ ${#PACKAGES[@]} -eq 0 ]]; then+ echo "No required YoureCom packages."+ exit 0+fi++if [[ -z "${TOKEN:-}" ]]; then+ echo "::warning::YOURECOM_DEPENDENCY_TOKEN is not set, PHPStan runs without: ${PACKAGES[*]}"+ exit 0+fi++for PACKAGE in "${PACKAGES[@]}"; do+ # the repositories keep the camel case of the plugin folders, GitHub resolves the name case-insensitively+ git clone --quiet --depth 1 --branch "$BRANCH" "https://x-access-token:${TOKEN}@github.com/YoureCom/${PACKAGE}.git" "$PACKAGE" \+ || git clone --quiet --depth 1 "https://x-access-token:${TOKEN}@github.com/YoureCom/${PACKAGE}.git" "$PACKAGE" \+ || { echo "::warning::$PACKAGE could not be cloned, PHPStan runs without it"; continue; }+ echo "Cloned $PACKAGE"+done
-
.github/workflows/ci.yml<pre>@@ -0,0 +1,180 @@+# YoureCom Standard: checks of every plugin. Managed by _dev/standard (apply.php), do not edit here.+# One job instead of one per check: GitHub bills every job rounded up to whole minutes, nine small jobs cost nine minutes+# even when each takes seconds. All steps run even if an earlier one fails (`!cancelled()`), the job is red if any is.+# Locally `bash _dev/standard/check.sh <plugin>` runs the same checks and more (dev system, browser).+name: CI++on:+ push:+ branches: ['6.2']+ paths-ignore: ['**.md', 'docs/**']+ pull_request:+ paths-ignore: ['**.md', 'docs/**']+ # the release workflow runs the same checks first, with PHP 8.4 in addition+ workflow_call:++permissions:+ contents: read++concurrency:+ group: ci-${{ github.ref }}+ cancel-in-progress: true++jobs:+ checks:+ name: Checks+ runs-on: ubuntu-latest+ # Dependabot pull requests only signal new versions, the update itself happens once in _dev/standard+ if: github.actor != 'dependabot[bot]'+ defaults:+ run:+ working-directory: plugin+ steps:+ # the whole history: check-deletes.php compares with the released versions+ - uses: actions/checkout@v7+ with:+ path: plugin+ fetch-depth: 0++ # the WoltLab Suite Core: the classes for PHPStan and the XSDs of the PIP files+ - uses: actions/checkout@v7+ with:+ repository: WoltLab/WCF+ ref: '6.2'+ path: wcf++ # required YoureCom packages (package.xml) next to the plugin, as in the local plugins folder; needs the+ # organization secret YOURECOM_DEPENDENCY_TOKEN (read access to the private repositories)+ - name: Required YoureCom packages+ working-directory: .+ env:+ TOKEN: ${{ secrets.YOURECOM_DEPENDENCY_TOKEN }}+ run: bash plugin/.github/scripts/checkout-dependencies.sh plugin++ - uses: shivammathur/setup-php@v2+ with:+ php-version: '8.2'+ tools: composer+ coverage: pcov+ extensions: dom, libxml++ # a plugin without PHP classes (files*/lib) has nothing for PHPStan and PHPUnit, like in check.sh+ - id: detect+ name: Detect PHP classes and TypeScript+ run: |+ echo "php=$(find files*/lib -name '*.php' -print -quit 2>/dev/null | grep -q . && echo true || echo false)" >> "$GITHUB_OUTPUT"+ echo "typescript=$([ -f tsconfig.json ] && echo true || echo false)" >> "$GITHUB_OUTPUT"++ - name: PHP syntax+ run: find . -path ./vendor -prune -o -path ./node_modules -prune -o -name '*.php' -print0 | xargs -0 -n1 -P4 php -l > /dev/null++ # the versions of PHPStan, PHPUnit and PHP-CS-Fixer are pinned in composer.json, locally and in the CI the same+ - run: composer install --no-interaction --no-progress++ - name: Code style (PHP-CS-Fixer, the rules of WoltLab Suite)+ if: ${{ !cancelled() }}+ run: vendor/bin/php-cs-fixer check --diff++ - name: PHPStan+ if: ${{ !cancelled() && steps.detect.outputs.php == 'true' }}+ run: vendor/bin/phpstan analyse -c phpstan.ci.neon --memory-limit=2G --no-progress++ - name: PHPUnit with coverage floor+ if: ${{ !cancelled() && steps.detect.outputs.php == 'true' }}+ run: |+ vendor/bin/phpunit --testsuite unit --coverage-clover=var/coverage.xml+ php tests/lint/check-coverage.php var/coverage.xml++ - name: PIP files match the XSDs of WoltLab Suite 6.2+ if: ${{ !cancelled() }}+ run: php tests/lint/check-xsd.php+ env:+ WCF_XSD: ${{ github.workspace }}/wcf/XSD++ - name: package.xml and constants.php+ if: ${{ !cancelled() }}+ run: php tests/lint/check-package.php++ - name: Removed files and language items of released versions are deleted on update+ if: ${{ !cancelled() }}+ run: php tests/lint/check-deletes.php++ - name: Language files+ if: ${{ !cancelled() }}+ run: php tests/lint/check-language.php++ - name: PHP rules (debug output, WCF_N, json_decode, trim, docblocks)+ if: ${{ !cancelled() }}+ run: php tests/lint/check-php.php++ - name: Phrases of the TypeScript modules exist and are preloaded+ if: ${{ !cancelled() }}+ run: php tests/lint/check-js-phrases.php++ - name: RPC endpoints are registered+ if: ${{ !cancelled() }}+ run: php tests/lint/check-endpoints.php++ - name: Templates and TypeScript (no jQuery, no legacy AJAX, strict comparisons, labels, ids, buttons)+ if: ${{ !cancelled() }}+ run: php tests/lint/check-templates.php++ - name: Architecture (grid/list views, interactions, RPC endpoints instead of legacy lists and AJAX actions)+ if: ${{ !cancelled() }}+ run: php tests/lint/check-architecture.php++ - uses: actions/setup-node@v7+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ with:+ node-version: 22++ # the WoltLab packages come from GitHub, the lock file names them by SSH+ - name: npm install+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ run: |+ git config --global url."https://github.com/".insteadOf ssh://git@github.com/+ if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi++ - name: TypeScript (strict)+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ run: npx tsc -p tsconfig.ci.json++ - name: ESLint+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ run: npm run lint++ - name: Prettier+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ run: npm run format:check++ - name: The committed JavaScript is the build of ts/+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ run: node tests/lint/check-js-build.mjs++ - name: Vitest with coverage+ if: ${{ !cancelled() && steps.detect.outputs.typescript == 'true' }}+ run: npm run test:coverage++ # the newest PHP only before a release (tags), not on every push+ php84:+ name: PHP 8.4 (release)+ runs-on: ubuntu-latest+ if: startsWith(github.ref, 'refs/tags/')+ steps:+ - uses: actions/checkout@v7++ - uses: shivammathur/setup-php@v2+ with:+ php-version: '8.4'+ tools: composer++ - name: PHP syntax+ run: find . -path ./vendor -prune -o -path ./node_modules -prune -o -name '*.php' -print0 | xargs -0 -n1 -P4 php -l > /dev/null++ - run: composer install --no-interaction --no-progress++ - name: PHPUnit+ run: |+ if find files*/lib -name '*.php' -print -quit 2>/dev/null | grep -q .; then+ vendor/bin/phpunit --testsuite unit+ fi
-
.github/workflows/release.yml<pre>@@ -0,0 +1,54 @@+# YoureCom Standard: managed by _dev/standard (apply.php), do not edit here.+name: Release++# Builds the package for a tag like v6.2.14 or v6.2.14-beta.2 and attaches it to a GitHub release.+# Nothing is deployed: uploading to yourecom.de stays a manual step that needs Marcel's approval+# (before that: bash ../_dev/release-check.sh <plugin>, it needs SSH access to the live server).++on:+ push:+ tags: ['v*']++permissions:+ contents: read++jobs:+ checks:+ uses: ./.github/workflows/ci.yml++ release:+ name: Build and publish+ needs: checks+ runs-on: ubuntu-latest+ permissions:+ contents: write+ steps:+ - uses: actions/checkout@v7++ - name: The tag matches the version in package.xml+ id: version+ run: |+ version="$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' package.xml | head -1)"+ # "6.2.14 Beta 2" and "v6.2.14-beta.2" both become "6.2.14.beta.2"+ normalize() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed -e 's/^v//' -e 's/[ -]/./g'; }+ if [ "$(normalize "$version")" != "$(normalize "$GITHUB_REF_NAME")" ]; then+ echo "::error::The tag $GITHUB_REF_NAME does not match the version '$version' in package.xml"+ exit 1+ fi+ echo "version=$version" >> "$GITHUB_OUTPUT"+ if echo "$version" | grep -qiE 'alpha|beta|rc'; then+ echo "prerelease=--prerelease" >> "$GITHUB_OUTPUT"+ fi++ - name: Build the package+ id: build+ run: echo "archive=$(bash .github/scripts/build-package.sh "$RUNNER_TEMP/package")" >> "$GITHUB_OUTPUT"++ - name: Publish the release+ env:+ GH_TOKEN: ${{ github.token }}+ run: |+ gh release create "$GITHUB_REF_NAME" "${{ steps.build.outputs.archive }}" \+ --title "${{ steps.version.outputs.version }}" \+ --generate-notes \+ ${{ steps.version.outputs.prerelease }}
-
.php-cs-fixer.dist.php<pre>@@ -0,0 +1,126 @@+<?php++/**+ * YoureCom Standard (managed by _dev/standard, do not edit here). Code style of the plugin: the rules of WoltLab Suite (https://github.com/WoltLab/WCF/blob/6.2/.php-cs-fixer.dist.php).+ * Check: vendor/bin/php-cs-fixer check --diff Fix: vendor/bin/php-cs-fixer fix+ */++$finder = PhpCsFixer\Finder::create()+ ->in(array_values(array_filter([__DIR__ . "/files", __DIR__ . "/files_wcf", __DIR__ . "/tests"], "is_dir")))+ ->name('*.php');++return (new PhpCsFixer\Config())+ ->setRiskyAllowed(true)+ ->setRules([+ '@PSR1' => true,+ '@PSR2' => true,++ 'array_push' => true,+ 'backtick_to_shell_exec' => true,+ 'no_alias_language_construct_call' => true,+ 'no_mixed_echo_print' => true,+ 'pow_to_exponentiation' => true,+ 'random_api_migration' => true,++ 'array_syntax' => ['syntax' => 'short'],+ 'no_multiline_whitespace_around_double_arrow' => true,+ 'no_trailing_comma_in_singleline_array' => true,+ 'no_whitespace_before_comma_in_array' => true,+ 'normalize_index_brace' => true,+ 'whitespace_after_comma_in_array' => true,++ 'non_printable_character' => ['use_escape_sequences_in_strings' => true],++ 'lowercase_static_reference' => true,+ 'magic_constant_casing' => true,+ 'magic_method_casing' => true,+ 'native_function_casing' => true,+ 'native_function_type_declaration_casing' => true,++ 'cast_spaces' => ['space' => 'none'],+ 'lowercase_cast' => true,+ 'no_unset_cast' => true,+ 'short_scalar_cast' => true,++ 'class_attributes_separation' => true,+ 'no_blank_lines_after_class_opening' => true,+ 'no_null_property_initialization' => true,+ // 'self_accessor' would turn `Class::method()` in an anonymous class into `self::method()` of the anonymous class+ 'single_class_element_per_statement' => true,+ 'single_trait_insert_per_statement' => true,++ 'no_empty_comment' => true,+ 'single_line_comment_style' => ['comment_types' => ['hash']],++ 'native_constant_invocation' => ['strict' => false],++ 'no_alternative_syntax' => true,+ 'no_trailing_comma_in_list_call' => true,+ 'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield', 'yield_from']],+ 'no_unneeded_curly_braces' => ['namespaces' => true],+ 'switch_continue_to_break' => true,+ 'trailing_comma_in_multiline' => ['elements' => ['arrays']],++ 'function_typehint_space' => true,+ 'lambda_not_used_import' => true,+ 'native_function_invocation' => ['include' => ['@internal']],+ 'no_unreachable_default_argument_value' => true,+ 'nullable_type_declaration_for_default_null_value' => true,+ 'return_type_declaration' => true,+ 'static_lambda' => true,++ 'fully_qualified_strict_types' => ['leading_backslash_in_global_namespace' => true],+ 'no_leading_import_slash' => true,+ 'no_unused_imports' => true,+ 'ordered_imports' => true,++ 'declare_equal_normalize' => true,+ 'dir_constant' => true,+ 'explicit_indirect_variable' => true,+ 'function_to_constant' => true,+ 'is_null' => true,+ 'no_unset_on_property' => true,++ 'list_syntax' => ['syntax' => 'short'],++ 'clean_namespace' => true,+ 'no_leading_namespace_whitespace' => true,+ 'single_blank_line_before_namespace' => true,++ 'no_homoglyph_names' => true,++ 'binary_operator_spaces' => true,+ 'concat_space' => ['spacing' => 'one'],+ 'increment_style' => ['style' => 'post'],+ 'logical_operators' => true,+ 'object_operator_without_whitespace' => true,+ 'operator_linebreak' => true,+ 'standardize_increment' => true,+ 'standardize_not_equals' => true,+ 'ternary_operator_spaces' => true,+ 'ternary_to_elvis_operator' => true,+ 'ternary_to_null_coalescing' => true,+ 'unary_operator_spaces' => true,++ 'no_useless_return' => true,+ // 'return_assignment' would drop the variable of a `@var T $x` annotated assignment that is returned++ 'multiline_whitespace_before_semicolons' => true,+ 'no_empty_statement' => true,+ 'no_singleline_whitespace_before_semicolons' => true,+ 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],++ 'escape_implicit_backslashes' => true,+ 'explicit_string_variable' => true,+ 'heredoc_to_nowdoc' => true,+ 'no_binary_string' => true,+ 'simple_to_complex_string_variable' => true,++ 'array_indentation' => true,+ 'blank_line_before_statement' => ['statements' => ['return', 'exit']],+ 'compact_nullable_typehint' => true,+ 'method_chaining_indentation' => true,+ 'no_extra_blank_lines' => ['tokens' => ['case', 'continue', 'curly_brace_block', 'default', 'extra', 'parenthesis_brace_block', 'square_brace_block', 'switch', 'throw', 'use']],+ 'no_spaces_around_offset' => true,+ ])+ ->setFinder($finder);
-
.prettierignore<pre>@@ -0,0 +1,7 @@+files/+files_wcf/+node_modules/+vendor/+var/+snapshots/+tests/ts/mocks/
-
.prettierrc.json<pre>@@ -0,0 +1,4 @@+{+ "printWidth": 140,+ "trailingComma": "all"+}
-
README.md<pre>@@ -0,0 +1,9 @@+# de.yourecom.wcf.api.mpdf++WoltLab Suite 6.2 plugin of YoureCom.++- Rules, standard and tests: [`../CLAUDE.md`](../CLAUDE.md) and [`../_dev/standard/README.md`](../_dev/standard/README.md)+- Deploy into the dev system: `bash ../_dev/deploy.sh de.yourecom.wcf.api.mpdf`+- All checks: `bash ../_dev/standard/check.sh de.yourecom.wcf.api.mpdf`++Describe here what the plugin does and what is special about it (options, dependencies, interfaces).
-
composer.json<pre>@@ -0,0 +1,24 @@+{+ "name": "yourecom/de.yourecom.wcf.api.mpdf",+ "description": "Development tools of de.yourecom.wcf.api.mpdf (not part of the package). YoureCom Standard: same versions as _dev/tools.",+ "type": "project",+ "require": {+ "php": ">=8.2"+ },+ "require-dev": {+ "friendsofphp/php-cs-fixer": "3.95.27",+ "phpstan/phpstan": "2.2.16",+ "phpstan/phpstan-deprecation-rules": "2.0.5",+ "phpstan/phpstan-strict-rules": "2.0.12",+ "phpunit/phpunit": "^11"+ },+ "scripts": {+ "test": "phpunit --testsuite unit",+ "test-integration": "phpunit -c phpunit.integration.xml",+ "cs": "php-cs-fixer check --diff",+ "cs-fix": "php-cs-fixer fix"+ },+ "config": {+ "sort-packages": true+ }+}
-
constants.php<pre>@@ -0,0 +1,17 @@+<?php+/**+ * Defines constants for autocompletion in IDEs. This file is not meant to be actively used anywhere!+ *+ * @author Marcel Beckers+ * @license YoureCom License - Commercial (YCLC) <https://www.yourecom.de/shop/licence-commercial/>+ * @package de.yourecom.wcf.api.mpdf+ */++// option constants++// application constants++// hardcoded framework constants+define('WCF_DIR', '');+define('LEGAL_NOTICE_EMAIL_ADDRESS', '');+define('DOMPDF_LOG_FILE', WCF_DIR . 'tmp/dompdf.log');
-
eslint.config.mjs<pre>@@ -0,0 +1,31 @@+// YoureCom Standard: managed by _dev/standard (apply.php), do not edit here.+// @ts-check+import tseslint from "typescript-eslint";+import eslintConfigPrettier from "eslint-config-prettier";++export default tseslint.config(+ {+ ignores: ["files/**", "files_wcf/**", "node_modules/**", "vendor/**", "var/**", "snapshots/**"],+ },+ ...tseslint.configs.recommended,+ {+ rules: {+ // the core modules are typed loosely in places, `any` at those boundaries is a warning only+ "@typescript-eslint/no-explicit-any": "warn",+ "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],+ // strict comparisons only+ eqeqeq: ["error", "always"],+ // no debug output in the package (tests may log)+ "no-console": "error",+ "no-debugger": "error",+ "no-alert": "error",+ },+ },+ {+ files: ["tests/**"],+ rules: {+ "no-console": "off",+ },+ },+ eslintConfigPrettier,+);
-
files/lib/system/api/mpdf/autoload.php<pre>@@ -1 +1 @@-<?php require (__DIR__ . '/vendor/autoload.php');+<?php require(__DIR__ . '/vendor/autoload.php');
-
package.xml<pre>@@ -1,13 +1,13 @@<?xml version="1.0" encoding="utf-8"?>-<package name="de.yourecom.wcf.api.mpdf" 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/package.xsd">+<package name="de.yourecom.wcf.api.mpdf" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com https://www.woltlab.com/XSD/6.0/package.xsd"><packageinformation><packagename><![CDATA[mPDF]]></packagename><packagename languagecode="de"><![CDATA[mPDF]]></packagename><packagedescription><![CDATA[Bundles the mPDF library (HTML to PDF converter) for use by other packages. Developed by Matěj Humpál and contributors: https://github.com/mpdf/mpdf]]></packagedescription><packagedescription languagecode="de"><![CDATA[Stellt die mPDF-Bibliothek (HTML-zu-PDF-Konverter) für andere Pakete bereit. Entwickelt von Matěj Humpál und Mitwirkenden: https://github.com/mpdf/mpdf]]></packagedescription><isapplication>0</isapplication>- <version>8.3.2 Beta 1</version>- <date>2026-10-12</date>+ <version>8.3.2 Beta 2</version>+ <date>2026-10-13</date><packageurl><![CDATA[https://yourecom.de]]></packageurl></packageinformation>
-
phpstan.ci.neon<pre>@@ -0,0 +1,17 @@+# YoureCom Standard: PHPStan in the GitHub workflow, the WoltLab Suite Core is checked out into ../wcf and the+# required YoureCom packages next to the plugin (.github/scripts/checkout-dependencies.sh).+includes:+ - vendor/phpstan/phpstan-strict-rules/rules.neon+ - vendor/phpstan/phpstan-deprecation-rules/rules.neon+ - phpstan.common.neon++parameters:+ tmpDir: var/phpstan++ bootstrapFiles:+ - '../wcf/constants.php'+ - 'constants.php'++ scanDirectories:+ - ../wcf/wcfsetup/install/files/lib/+
-
phpstan.common.neon<pre>@@ -0,0 +1,36 @@+# YoureCom Standard: shared PHPStan settings of the plugin (level 9 at least, strict and deprecation rules).+# Local: phpstan.neon.dist, CI: phpstan.ci.neon.+includes:+ - phpstanWoltLab.neon+ # rules of the standard: strict comparisons (no empty(), no ==), #[\Override] or a docblock on every method+ - phpstan.standard.neon++parameters:+ level: 9++ # strict rules, except the ones that ask for a different style than the WoltLab code base+ strictRules:+ booleansInConditions: false+ disallowedShortTernary: false+ # `@property-read int` on a database object names the target type, the database delivers strings+ uselessCast: false++ paths:+ - files+ - tests++ reportUnmatchedIgnoredErrors: true++ excludePaths:+ analyse:+ - vendor/*+ - "**/vendor/*"+ - node_modules/*+ - var/*+ - tests/e2e/*+ # tooling of the standard, not part of the package+ - tests/lint/*+ - .vscode (?)++ dynamicConstantNames:+ - TIME_NOW
-
phpstan.neon.dist<pre>@@ -0,0 +1,17 @@+# YoureCom Standard: local PHPStan run against C:/laragon/www/wsc6.2 with the shared tools of _dev/tools.+# php -d memory_limit=4G ../_dev/tools/vendor/bin/phpstan analyse -c phpstan.neon.dist --no-progress+includes:+ - ../_dev/tools/vendor/phpstan/phpstan-strict-rules/rules.neon+ - ../_dev/tools/vendor/phpstan/phpstan-deprecation-rules/rules.neon+ - phpstan.common.neon++parameters:+ tmpDir: ../var/phpstan/de.yourecom.wcf.api.mpdf++ bootstrapFiles:+ - 'C:/laragon/www/wsc6.2/constants.php'+ - 'constants.php'++ scanDirectories:+ - C:/laragon/www/wsc6.2/lib/+
-
phpstan.standard.neon<pre>@@ -0,0 +1,15 @@+# YoureCom Standard: managed by _dev/standard (apply.php), do not edit here.+# Rules of the standard on top of level 9 and the strict rules, included by phpstan.common.neon.+# - strict comparisons: no empty() (compare with === [], === '', === 0 or === null), no == or != (strict rules)+# - every method has #[\Override] or a docblock that explains it (tests/phpstan/OverrideOrDocblockRule.php)+includes:+ - tests/phpstan/rules.php++parameters:+ strictRules:+ disallowedEmpty: true+ disallowedLooseComparison: true++ excludePaths:+ analyse:+ - tests/phpstan/*
-
phpstanWoltLab.neon<pre>@@ -0,0 +1,235 @@+parameters:+ universalObjectCratesClasses:+ - wcf\data\category\Category+ - wcf\data\condition\Condition+ - wcf\data\modification\log\ModificationLog+ - wcf\data\object\type\ObjectType+ - wcf\data\option\Option+ - wcf\data\user\activity\event\ViewableUserActivityEvent+ - wcf\data\user\notification\UserNotification+ - wcf\data\user\User+ - wcf\system\menu\ITreeMenuItem+ - wcf\system\message\embedded\object\IMessageEmbeddedObjectHandler+ - wcf\system\user\notification\event\IUserNotificationEvent+ - wcf\system\user\notification\object\type\IUserNotificationObjectType++ dynamicConstantNames:+ - PACKAGE_ID+ - PACKAGE_NAME+ - RELATIVE_WCF_DIR+ - TMP_DIR+ - WCF_N+ - TIME_NOW+ - WCF_OPTION_INC_PHP_SUCCESS+ - LAST_UPDATE_TIME+ - WCF_UUID+ - WOLTLAB_BRANDING+ - VISITOR_USE_TINY_BUILD+ - ENABLE_DEBUG_MODE+ - ENABLE_BENCHMARK+ - LOG_IP_ADDRESS+ - MODULE_SYSTEM_RECAPTCHA+ - MODULE_SMILEY+ - MODULE_USERS_ONLINE+ - MODULE_USER_RANK+ - MODULE_USER_SIGNATURE+ - MODULE_MEMBERS_LIST+ - MODULE_TEAM_PAGE+ - MODULE_LIKE+ - MODULE_USER_PROFILE_WALL+ - MODULE_TAGGING+ - MODULE_POLL+ - MODULE_WCF_AD+ - PAGE_TITLE+ - PAGE_DESCRIPTION+ - META_DESCRIPTION+ - SHOW_VERSION_NUMBER+ - EXTERNAL_LINK_TARGET_BLANK+ - FOOTER_CODE+ - URL_OMIT_INDEX_PHP+ - URL_TITLE_COMPONENT_REPLACEMENT+ - OFFLINE+ - OFFLINE_MESSAGE+ - OFFLINE_MESSAGE_ALLOW_HTML+ - IMAGE_ADAPTER_TYPE+ - SEARCH_ENGINE+ - EXCEPTION_PRIVACY+ - COOKIE_PREFIX+ - PACKAGE_SERVER_AUTH_CODE+ - PROXY_SERVER_HTTP+ - IP_ADDRESS_SEARCH_ENGINE+ - USER_ONLINE_TIMEOUT+ - ENABLE_USER_AUTHENTICATION_FAILURE+ - USER_AUTHENTICATION_FAILURE_TIMEOUT+ - USER_AUTHENTICATION_FAILURE_IP_CAPTCHA+ - USER_AUTHENTICATION_FAILURE_IP_BLOCK+ - USER_AUTHENTICATION_FAILURE_USER_CAPTCHA+ - USER_AUTHENTICATION_FAILURE_EXPIRATION+ - SIGNATURE_SECRET+ - CAPTCHA_TYPE+ - REGISTER_USE_CAPTCHA+ - LOST_PASSWORD_USE_CAPTCHA+ - RECAPTCHA_PUBLICKEY+ - RECAPTCHA_PRIVATEKEY+ - RECAPTCHA_PRIVATEKEY_INVISIBLE+ - TIMEZONE+ - GOOGLE_MAPS_API_KEY+ - GOOGLE_MAPS_ZOOM+ - GOOGLE_MAPS_DEFAULT_LATITUDE+ - GOOGLE_MAPS_DEFAULT_LONGITUDE+ - MAIL_FROM_NAME+ - MAIL_FROM_ADDRESS+ - MAIL_ADMIN_ADDRESS+ - MAIL_SIGNATURE+ - MAIL_SIGNATURE_HTML+ - MAIL_SEND_METHOD+ - MAIL_SMTP_HOST+ - MAIL_SMTP_PORT+ - MAIL_SMTP_STARTTLS+ - MAIL_SMTP_USER+ - MAIL_SMTP_PASSWORD+ - MAIL_USE_F_PARAM+ - CACHE_SOURCE_TYPE+ - CACHE_SOURCE_REDIS_HOST+ - AVAILABLE_PAYMENT_METHODS+ - PAYPAL_EMAIL_ADDRESS+ - MODULE_PAID_SUBSCRIPTION+ - PAID_SUBSCRIPTION_ENABLE_TOS_CONFIRMATION+ - PAID_SUBSCRIPTION_TOS_URL+ - ATTACHMENT_RETAIN_DIMENSIONS+ - ATTACHMENT_THUMBNAIL_HEIGHT+ - ATTACHMENT_THUMBNAIL_WIDTH+ - MODULE_EDIT_HISTORY+ - EDIT_HISTORY_EXPIRATION+ - ENABLE_SHARE_BUTTONS+ - SHARE_BUTTONS_PROVIDERS+ - MESSAGE_FORCE_SECURE_IMAGES+ - MODULE_IMAGE_PROXY+ - IMAGE_PROXY_INSECURE_ONLY+ - IMAGE_PROXY_EXPIRATION+ - IMAGE_PROXY_HOST_WHITELIST+ - CENSORED_WORDS+ - PASSWORD_MIN_SCORE+ - REGISTER_FORBIDDEN_USERNAMES+ - REGISTER_FORBIDDEN_EMAILS+ - REGISTER_ALLOWED_EMAILS+ - REGISTER_DISABLED+ - REGISTER_ENABLE_DISCLAIMER+ - REGISTER_ACTIVATION_METHOD+ - REGISTER_USERNAME_MIN_LENGTH+ - REGISTER_USERNAME_MAX_LENGTH+ - REGISTER_USERNAME_FORCE_ASCII+ - REGISTER_MIN_USER_AGE+ - REGISTER_ANTISPAM_ACTION+ - GITHUB_PUBLIC_KEY+ - GITHUB_PRIVATE_KEY+ - TWITTER_PUBLIC_KEY+ - TWITTER_PRIVATE_KEY+ - FACEBOOK_PUBLIC_KEY+ - FACEBOOK_PRIVATE_KEY+ - GOOGLE_PUBLIC_KEY+ - GOOGLE_PRIVATE_KEY+ - SIGNATURE_MAX_IMAGE_HEIGHT+ - USER_TITLE_MAX_LENGTH+ - USER_FORBIDDEN_TITLES+ - PROFILE_SHOW_OLD_USERNAME+ - PROFILE_ENABLE_VISITORS+ - MEMBERS_LIST_USERS_PER_PAGE+ - MEMBERS_LIST_DEFAULT_SORT_FIELD+ - MEMBERS_LIST_DEFAULT_SORT_ORDER+ - USERS_ONLINE_SHOW_GUESTS+ - USERS_ONLINE_SHOW_ROBOTS+ - USERS_ONLINE_DEFAULT_SORT_FIELD+ - USERS_ONLINE_DEFAULT_SORT_ORDER+ - USERS_ONLINE_PAGE_REFRESH+ - USERS_ONLINE_ENABLE_LEGEND+ - USERS_ONLINE_RECORD+ - USERS_ONLINE_RECORD_TIME+ - USER_CLEANUP_NOTIFICATION_LIFETIME+ - USER_CLEANUP_ACTIVITY_EVENT_LIFETIME+ - USER_CLEANUP_PROFILE_VISITOR_LIFETIME+ - LIKE_SHOW_SUMMARY+ - MESSAGE_SIDEBAR_ENABLE_ONLINE_STATUS+ - MESSAGE_SIDEBAR_ENABLE_LIKES_RECEIVED+ - MESSAGE_SIDEBAR_ENABLE_ACTIVITY_POINTS+ - MESSAGE_SIDEBAR_USER_OPTIONS+ - TAGGING_MAX_TAG_LENGTH+ - MESSAGE_MAX_QUOTE_DEPTH+ - SEARCH_RESULTS_PER_PAGE+ - SEARCH_DEFAULT_SORT_FIELD+ - SEARCH_DEFAULT_SORT_ORDER+ - POLL_MAX_OPTIONS+ - POLL_FULL_WIDTH+ - MEDIA_SMALL_THUMBNAIL_WIDTH+ - MEDIA_SMALL_THUMBNAIL_HEIGHT+ - MEDIA_SMALL_THUMBNAIL_RETAIN_DIMENSIONS+ - MEDIA_MEDIUM_THUMBNAIL_WIDTH+ - MEDIA_MEDIUM_THUMBNAIL_HEIGHT+ - MEDIA_MEDIUM_THUMBNAIL_RETAIN_DIMENSIONS+ - MEDIA_LARGE_THUMBNAIL_WIDTH+ - MEDIA_LARGE_THUMBNAIL_HEIGHT+ - MEDIA_LARGE_THUMBNAIL_RETAIN_DIMENSIONS+ - MODULE_ARTICLE+ - ARTICLE_SHOW_ABOUT_AUTHOR+ - ARTICLE_ENABLE_COMMENTS_DEFAULT_VALUE+ - ARTICLE_ENABLE_LIKE+ - ARTICLES_PER_PAGE+ - ARTICLE_RELATED_ARTICLES+ - ARTICLE_RELATED_ARTICLES_MATCH_THRESHOLD+ - SHOW_UPDATE_NOTICE_FRONTEND+ - LANGUAGE_USE_INFORMAL_VARIANT+ - SHOW_STYLE_CHANGER+ - ARTICLE_SORT_ORDER+ - USE_PAGE_TITLE_ON_LANDING_PAGE+ - OG_IMAGE+ - HEAD_CODE+ - AVATAR_DEFAULT_TYPE+ - ARTICLE_ENABLE_VISIT_TRACKING+ - ENABLE_AD_ROTATION+ - ENABLE_POLLING+ - FB_SHARE_APP_ID+ - MODULE_CONTACT_FORM+ - SITEMAP_INDEX_TIME_FRAME+ - MODULE_TROPHY+ - ENABLE_DEVELOPER_TOOLS+ - FORCE_LOGIN+ - PAGE_LOGO_LINK_TO_APP_DEFAULT+ - IMAGE_ALLOW_EXTERNAL_SOURCE+ - MESSAGE_ENABLE_TOC+ - MESSAGE_PUBLIC_HIGHLIGHTERS+ - MESSAGE_SIDEBAR_ENABLE_ARTICLES+ - IMAGE_PROXY_ENABLE_PRUNE+ - SEARCH_ENABLE_PAGES+ - SEARCH_ENABLE_ARTICLES+ - ATTACHMENT_IMAGE_AUTOSCALE+ - ATTACHMENT_IMAGE_AUTOSCALE_MAX_WIDTH+ - ATTACHMENT_IMAGE_AUTOSCALE_MAX_HEIGHT+ - ATTACHMENT_IMAGE_AUTOSCALE_FILE_TYPE+ - ATTACHMENT_IMAGE_AUTOSCALE_QUALITY+ - LOG_MISSING_LANGUAGE_ITEMS+ - PRUNE_IP_ADDRESS+ - BREADCRUMBS_HOME_USE_PAGE_TITLE+ - CONTACT_FORM_ENABLE_ATTACHMENTS+ - CONTACT_FORM_PRUNE_ATTACHMENTS+ - IMAGE_EXTERNAL_SOURCE_WHITELIST+ - SEARCH_MAX_COMBINED_TAGS+ - ENABLE_PRODUCTION_DEBUG_MODE+ - BLACKLIST_SFS_ENABLE+ - BLACKLIST_SFS_USERNAME+ - BLACKLIST_SFS_EMAIL_ADDRESS+ - BLACKLIST_SFS_IP_ADDRESS+ - ENABLE_ENTERPRISE_MODE+ - MESSAGE_ENABLE_USER_CONSENT+ - MODIFICATION_LOG_EXPIRATION+ - INTERNAL_HOSTNAMES+ - MODULE_URL_UNFURLING+ - FIRST_TIME_SETUP_STATE+ - FORMATTING_REMOVE_COLOR+ - FORMATTING_REMOVE_FONT+ - FORMATTING_REMOVE_SIZE+ - URL_UNFURLING_SAVE_IMAGES+ - SERVICE_WORKER_PRIVATE_KEY+ - SERVICE_WORKER_PUBLIC_KEY+ - RECAPTCHA_PRIVATEKEY_V3+ - IMAGE_CONVERT_FORMAT+ - IMAGE_STRIP_EXIF
-
phpunit.integration.xml<pre>@@ -0,0 +1,17 @@+<?xml version="1.0" encoding="UTF-8"?>+<!--+ Integration tests against the local WoltLab Suite installation with the plugin deployed (deploy first).+ The installation is taken from the environment variable WCF_DIR (default: C:/laragon/www/wsc6.2).+ Every test runs in a transaction that is rolled back.+-->+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"+ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"+ bootstrap="tests/Integration/bootstrap.php"+ colors="true"+ cacheDirectory=".phpunit.cache">+ <testsuites>+ <testsuite name="integration">+ <directory>tests/Integration</directory>+ </testsuite>+ </testsuites>+</phpunit>