1. YoureCom
    1. Über mich
  2. Lösungen
    1. Tipprunde
    2. Aufgaben & Support
    3. Mitglieder-Netzwerk
    4. Online-Shop
    5. Alle Lösungen & Preise
  3. Für Admins
    1. Plugins
    2. ByteMon
    3. ByteMon: Community verwalten
    4. ByteMon: Portalidentität
    5. Individuelle Entwicklung
    6. WSC 6.3 Alpha
    7. GitHub
  4. Neuigkeiten
    1. Kurzmeldungen
  5. Support
  6. Mein Konto
  7. Lösungen testen
  • Anmelden
  • Registrieren
  • Warenkorb 0
  • Suche
Alles
  • Alles
  • Artikel
  • Seiten
  • Aufgaben
  • Produkte
  • Pulse-Beiträge
  • Erweiterte Suche
  1. YoureCom – Software für Vereine, Teams & Communities
  2. Repository-Liste
  3. de.yourecom.wcf.api.mpdf

YoureCom Standard automatisch aktualisiert (verwaltete Dateien aus _dev/standard)

  • Marcel Beckers
  • -15 Löschungen
  • +440 Hinzufügungen
  • 455 Gesamt
  • 6 Dateien
  • 20. Oktober 2026 um 17:47
  • tests/lint/check-architecture.php
    <pre>@@ -29,14 +29,17 @@
    * - core-table-write: no INSERT/UPDATE/DELETE/REPLACE on the tables of a base plugin (`coreTables` in
    * tests/lint/config.php), write through the commands of the base plugin; reading and the update
    * scripts in files/acp stay allowed
    - * - permission-strict: no `getPermission(...) === true/false`, the session returns the raw group value ('1', 1, false)
    * - open-redirect: a RedirectResponse to a request value (`$_GET/$_POST/$_REQUEST`, `returnURL`) needs an
    * ApplicationHandler::isInternalURL() check in the same file
    * - grid-view-access: a grid view with an ID constructor parameter (comes from the client when rows are reloaded) whose
    * isAccessible() only checks `userID > 0`, it has to check access to the parent object
    * - link-application: `{link controller='X'}` in templates/ of an application without `application=` for a controller
    * (Page/Form/Action) of the plugin itself, it resolves against wcf
    *
    + * Rules that need the types of PHPStan are PHPStan rules (tests/phpstan): getPermission() compared with true/false
    + * (yourecom.permissionStrict), legacy actions that change data without the token (yourecom.secureAction), endpoints that
    + * bypass $requireACP (yourecom.endpointRequireAcp).
    + *
    * Known findings that cannot be changed yet are listed in tests/lint/config.php (`architectureKnown`: relative file
    * => list of rules); entries that no longer match make the check fail, so the list can only shrink.
    *
    @@ -95,10 +98,6 @@
    $report($relative, 'listener-instanceof', 'event listener without an instanceof check of $eventObj');
    }
    - if (\preg_match('~getPermission\([^)]*\)\s*[!=]==?\s*(?:true|false)\b~', $source)) {
    - $report($relative, 'permission-strict', 'getPermission() compared with true/false, it returns the raw group value, cast with (bool)');
    - }
    -
    if (
    \preg_match('~new\s+RedirectResponse\(\s*(?:\$_(?:GET|POST|REQUEST)\b|\$this->(?:returnURL|redirectURL|url)\b)~', $source)
    && !\str_contains($source, 'isInternalURL')
  • tests/lint/check-coverage.php
    <pre>@@ -2,16 +2,19 @@
    /**
    * YoureCom Standard: line coverage of the PHPUnit unit suite (Clover report) against the floor in
    - * tests/coverage-minimum.txt. The floor only goes up. Managed by _dev/standard (apply.php), do not edit here.
    + * tests/coverage-minimum.txt. The floor only goes up: with --raise (check.sh does that, the CI does not) it is lifted to
    + * the measured coverage minus one point, rounded down to a whole number (the margin absorbs the small difference between
    + * the local run and the CI). Managed by _dev/standard (apply.php), do not edit here.
    *
    - * Usage: php tests/lint/check-coverage.php var/coverage.xml
    + * Usage: php tests/lint/check-coverage.php var/coverage.xml [--raise]
    */
    namespace yourecom\standard;
    require __DIR__ . '/lib.php';
    $report = $argv[1] ?? 'var/coverage.xml';
    +$raise = \in_array('--raise', $argv, true);
    if (!\is_file($report)) {
    fail(["The coverage report {$report} does not exist (run PHPUnit with --coverage-clover first)"]);
    }
    @@ -24,9 +27,17 @@
    $statements = (int)$xml->project->metrics['statements'];
    $covered = (int)$xml->project->metrics['coveredstatements'];
    $percent = $statements > 0 ? 100 * $covered / $statements : 100.0;
    -$minimum = (float)\trim((string)@\file_get_contents(ROOT . '/tests/coverage-minimum.txt'));
    +$floorFile = ROOT . '/tests/coverage-minimum.txt';
    +$minimum = (float)\trim((string)@\file_get_contents($floorFile));
    \printf("Coverage of the unit suite: %.2f%% (%d of %d statements), floor %.2f%%\n", $percent, $covered, $statements, $minimum);
    if ($percent + 0.005 < $minimum) {
    fail(['The coverage is below the floor.']);
    }
    +
    +// the floor follows the coverage up (never down): check.sh lifts it, the change is committed with the code
    +$lifted = \floor($percent - 1);
    +if ($raise && $lifted > $minimum) {
    + \file_put_contents($floorFile, \sprintf("%d\n", $lifted));
    + \printf("Floor raised from %s to %d%% (tests/coverage-minimum.txt), commit it with the change.\n", $minimum, $lifted);
    +}
  • tests/phpstan/EndpointRequireAcpRule.php
    <pre>@@ -0,0 +1,153 @@
    +<?php
    +
    +namespace yourecom\standard\phpstan;
    +
    +use PhpParser\Node;
    +use PhpParser\Node\Expr\MethodCall;
    +use PHPStan\Analyser\Scope;
    +use PHPStan\Reflection\ClassReflection;
    +use PHPStan\Rules\Rule;
    +use PHPStan\Rules\RuleErrorBuilder;
    +
    +/**
    + * YoureCom Standard: an RPC endpoint that calls `validateX()` of a DatabaseObjectAction whose `x` is in `$requireACP`
    + * checks the ACP access itself. `$requireACP` only blocks AJAX requests (`validateAction()`), the direct call bypasses it, so
    + * a method that was meant for the ACP only is open to everyone who passes `validateX()` (e.g. moderators without ACP access
    + * created shipping labels through `/api/rpc/`, WoltLab review, October 2026).
    + *
    + * Only own method names count (not create/update/delete: the standard puts those into `$requireACP` as a second lock for
    + * frontend actions, too). Fine if the endpoint or `validateX()` mentions `canUseAcp` or an `admin.` permission; a method
    + * that is meant for the frontend leaves `$requireACP` (or gets an ignore with the reason).
    + * Managed by _dev/standard (apply.php), do not edit here.
    + *
    + * @author Marcel Beckers
    + * @license YoureCom License - Commercial (YCLC) <https://www.yourecom.de/shop/licence-commercial/>
    + * @implements Rule<MethodCall>
    + */
    +final class EndpointRequireAcpRule implements Rule
    +{
    + private const CRUD = ['create', 'update', 'delete', 'action'];
    +
    + #[\Override]
    + public function getNodeType(): string
    + {
    + return MethodCall::class;
    + }
    +
    + #[\Override]
    + public function processNode(Node $node, Scope $scope): array
    + {
    + $endpoint = $scope->getClassReflection();
    + if (
    + $endpoint === null
    + || !$endpoint->implementsInterface('wcf\system\endpoint\IController')
    + || !$node->name instanceof Node\Identifier
    + || \preg_match('~^validate(\w+)$~', $node->name->toString(), $match) !== 1
    + ) {
    + return [];
    + }
    +
    + $name = \lcfirst($match[1]);
    + if (\in_array(\strtolower($name), self::CRUD, true)) {
    + return [];
    + }
    +
    + foreach ($scope->getType($node->var)->getObjectClassReflections() as $action) {
    + if (!$action->isSubclassOf('wcf\data\AbstractDatabaseObjectAction') || !\in_array($name, $this->requireAcp($action), true)) {
    + continue;
    + }
    + if ($this->checksAcp($this->source($endpoint)) || $this->methodChecksAcp($action, $node->name->toString())) {
    + continue;
    + }
    +
    + return [
    + RuleErrorBuilder::message(\sprintf(
    + 'The endpoint calls %s::%s() directly, which bypasses $requireACP (\'%s\' is in it): check admin.general.canUseAcp or an administrator permission in the endpoint or in %s(). If the method is meant for the frontend, remove it from $requireACP.',
    + $action->getDisplayName(),
    + $node->name->toString(),
    + $name,
    + $node->name->toString(),
    + ))
    + ->identifier('yourecom.endpointRequireAcp')
    + ->build(),
    + ];
    + }
    +
    + return [];
    + }
    +
    + /**
    + * The default value of `$requireACP` of the action class.
    + *
    + * @return list<string>
    + */
    + private function requireAcp(ClassReflection $action): array
    + {
    + $value = $action->getNativeReflection()->getDefaultProperties()['requireACP'] ?? [];
    +
    + return \is_array($value) ? \array_values(\array_filter($value, 'is_string')) : [];
    + }
    +
    + /**
    + * Whether the source checks the ACP access or an administrator permission.
    + */
    + private function checksAcp(string $source): bool
    + {
    + return \str_contains($source, 'canUseAcp') || \preg_match('~[\'"]admin\.~', $source) === 1;
    + }
    +
    + /**
    + * Whether the validation method checks the ACP access, itself or in a `$this->validate…()` it calls (e.g.
    + * validateCreateReturnLabel() calling validateCreateLabel()), up to three calls deep.
    + */
    + private function methodChecksAcp(ClassReflection $action, string $method, int $depth = 0): bool
    + {
    + $source = $this->methodSource($action, $method);
    + if ($this->checksAcp($source)) {
    + return true;
    + }
    + if ($depth >= 3 || \preg_match_all('~\$this->(validate\w+)\s*\(~', $source, $calls) === 0) {
    + return false;
    + }
    + foreach (\array_unique($calls[1]) as $called) {
    + if (\strcasecmp($called, $method) !== 0 && $this->methodChecksAcp($action, $called, $depth + 1)) {
    + return true;
    + }
    + }
    +
    + return false;
    + }
    +
    + /**
    + * The source of the class file, empty if unknown.
    + */
    + private function source(ClassReflection $class): string
    + {
    + $file = $class->getFileName();
    +
    + return $file !== null && \is_file($file) ? (string)\file_get_contents($file) : '';
    + }
    +
    + /**
    + * The source of a method (its lines in the declaring class file), empty if unknown.
    + */
    + private function methodSource(ClassReflection $class, string $method): string
    + {
    + $native = $class->getNativeReflection();
    + if (!$native->hasMethod($method)) {
    + return '';
    + }
    +
    + $reflection = $native->getMethod($method);
    + $file = $reflection->getFileName();
    + $start = $reflection->getStartLine();
    + $end = $reflection->getEndLine();
    + if ($file === false || $start === false || $end === false || !\is_file($file)) {
    + return '';
    + }
    +
    + $lines = \file($file) ?: [];
    +
    + return \implode('', \array_slice($lines, $start - 1, $end - $start + 1));
    + }
    +}
  • tests/phpstan/PermissionStrictRule.php
    <pre>@@ -0,0 +1,69 @@
    +<?php
    +
    +namespace yourecom\standard\phpstan;
    +
    +use PhpParser\Node;
    +use PhpParser\Node\Expr;
    +use PHPStan\Analyser\Scope;
    +use PHPStan\Rules\Rule;
    +use PHPStan\Rules\RuleErrorBuilder;
    +
    +/**
    + * YoureCom Standard: `getPermission()` is never compared with `true` or `false`. The session returns the raw value of the
    + * group option (`'1'`, `1`, `false` …), so `=== true` denies everyone and `=== false` lets everyone through; cast with
    + * `(bool)` instead. Replaces the regular expression of check-architecture.php, which missed calls over several lines and
    + * arguments with parentheses.
    + * Managed by _dev/standard (apply.php), do not edit here.
    + *
    + * @author Marcel Beckers
    + * @license YoureCom License - Commercial (YCLC) <https://www.yourecom.de/shop/licence-commercial/>
    + * @implements Rule<Expr\BinaryOp>
    + */
    +final class PermissionStrictRule implements Rule
    +{
    + #[\Override]
    + public function getNodeType(): string
    + {
    + return Expr\BinaryOp::class;
    + }
    +
    + #[\Override]
    + public function processNode(Node $node, Scope $scope): array
    + {
    + if (!$node instanceof Expr\BinaryOp\Identical && !$node instanceof Expr\BinaryOp\NotIdentical) {
    + return [];
    + }
    +
    + foreach ([[$node->left, $node->right], [$node->right, $node->left]] as [$call, $value]) {
    + if ($this->isGetPermission($call) && $this->isBoolean($value)) {
    + return [
    + RuleErrorBuilder::message('getPermission() compared with true/false: it returns the raw group value (\'1\', 1, false), cast with (bool).')
    + ->identifier('yourecom.permissionStrict')
    + ->build(),
    + ];
    + }
    + }
    +
    + return [];
    + }
    +
    + /**
    + * Whether the expression calls a method named getPermission (session, user profile, any object or class).
    + */
    + private function isGetPermission(Expr $expression): bool
    + {
    + if (!$expression instanceof Expr\MethodCall && !$expression instanceof Expr\NullsafeMethodCall && !$expression instanceof Expr\StaticCall) {
    + return false;
    + }
    +
    + return $expression->name instanceof Node\Identifier && \strcasecmp($expression->name->toString(), 'getPermission') === 0;
    + }
    +
    + /**
    + * Whether the expression is the constant true or false.
    + */
    + private function isBoolean(Expr $expression): bool
    + {
    + return $expression instanceof Expr\ConstFetch && \in_array(\strtolower($expression->name->toString()), ['true', 'false'], true);
    + }
    +}
  • tests/phpstan/SecureActionRule.php
    <pre>@@ -0,0 +1,186 @@
    +<?php
    +
    +namespace yourecom\standard\phpstan;
    +
    +use PhpParser\Node;
    +use PhpParser\Node\Expr;
    +use PhpParser\NodeFinder;
    +use PHPStan\Analyser\Scope;
    +use PHPStan\Node\InClassNode;
    +use PHPStan\Reflection\ClassReflection;
    +use PHPStan\Rules\Rule;
    +use PHPStan\Rules\RuleErrorBuilder;
    +
    +/**
    + * YoureCom Standard: a legacy action (`wcf\action\AbstractAction`) that changes something extends
    + * `AbstractSecureAction`, which checks the security token (`t={csrfToken type=url}` in links, `{csrfToken}` in forms).
    + * The core checks the XSRF header only for PSR-15 controllers (`RequestHandlerInterface`, middleware `Xsrf`), a legacy
    + * action without the token can be triggered by any page the user visits (found by an external review in the bank transfer
    + * plugin, October 2026).
    + *
    + * "Changes something": it calls `executeAction()`, creates a command (`…\command\…`), creates or calls a
    + * `…Editor`, or prepares an INSERT/UPDATE/DELETE/REPLACE. An action that checks the token itself (`checkSecurityToken()`)
    + * is fine, and so is an action that does not act for the signed-in user (no WCF::getUser()/getSession(), no login or
    + * permissions required: a server-to-server API with a signature, a webhook). A download whose write is harmless (e.g. a download counter) gets an ignore with the reason.
    + * Managed by _dev/standard (apply.php), do not edit here.
    + *
    + * @author Marcel Beckers
    + * @license YoureCom License - Commercial (YCLC) <https://www.yourecom.de/shop/licence-commercial/>
    + * @implements Rule<InClassNode>
    + */
    +final class SecureActionRule implements Rule
    +{
    + #[\Override]
    + public function getNodeType(): string
    + {
    + return InClassNode::class;
    + }
    +
    + #[\Override]
    + public function processNode(Node $node, Scope $scope): array
    + {
    + $class = $node->getClassReflection();
    + if (
    + !$class->isSubclassOf('wcf\action\AbstractAction')
    + || $class->isSubclassOf('wcf\action\AbstractSecureAction')
    + || $class->implementsInterface('Psr\Http\Server\RequestHandlerInterface')
    + ) {
    + return [];
    + }
    +
    + $finder = new NodeFinder();
    + $statements = $node->getOriginalNode()->stmts;
    + $checksToken = $finder->findFirst($statements, static fn (Node $found): bool => ($found instanceof Expr\MethodCall || $found instanceof Expr\StaticCall)
    + && $found->name instanceof Node\Identifier
    + && \strcasecmp($found->name->toString(), 'checkSecurityToken') === 0);
    + if ($checksToken !== null) {
    + return [];
    + }
    +
    + // without the session (e.g. a server-to-server API with a signature, a webhook) a forged request gains nothing
    + if (!$this->actsForTheUser($class, $statements, $finder)) {
    + return [];
    + }
    +
    + $writes = [];
    + foreach ($finder->find($statements, static fn (Node $found): bool => $found instanceof Expr) as $expression) {
    + $write = $this->describeWrite($expression);
    + if ($write !== null) {
    + $writes[$write] = true;
    + }
    + }
    + if ($writes === []) {
    + return [];
    + }
    +
    + return [
    + RuleErrorBuilder::message(\sprintf(
    + '%s changes data (%s) but does not extend AbstractSecureAction: links need t={csrfToken type=url}, forms {csrfToken}. A harmless write (e.g. a download counter, a signed webhook) gets an ignore with the reason.',
    + $class->getDisplayName(),
    + \implode(', ', \array_slice(\array_keys($writes), 0, 3)),
    + ))
    + ->identifier('yourecom.secureAction')
    + ->line($node->getOriginalNode()->getStartLine())
    + ->build(),
    + ];
    + }
    +
    + /**
    + * Whether the action acts for the signed-in user: it reads the user or the session (WCF::getUser(),
    + * WCF::getSession(), also in a parent class of the plugin), requires a login or permissions.
    + *
    + * @param array<Node\Stmt> $statements
    + */
    + private function actsForTheUser(ClassReflection $class, array $statements, NodeFinder $finder): bool
    + {
    + $usesSession = static fn (Node $found): bool => $found instanceof Expr\StaticCall
    + && $found->class instanceof Node\Name
    + && \in_array(\ltrim($found->class->toString(), '\\'), ['wcf\system\WCF', 'WCF'], true)
    + && $found->name instanceof Node\Identifier
    + && \in_array(\strtolower($found->name->toString()), ['getuser', 'getsession', 'getuserprofilehandler'], true);
    + if ($finder->findFirst($statements, $usesSession) !== null) {
    + return true;
    + }
    +
    + $defaults = $class->getNativeReflection()->getDefaultProperties();
    + if (($defaults['loginRequired'] ?? false) === true || (\is_array($defaults['neededPermissions'] ?? null) && $defaults['neededPermissions'] !== [])) {
    + return true;
    + }
    +
    + // parent classes of the plugin (not the core's AbstractAction, which reads the session for its permission check)
    + foreach ($class->getParents() as $parent) {
    + if (\str_starts_with($parent->getName(), 'wcf\action\\')) {
    + break;
    + }
    + $file = $parent->getFileName();
    + if ($file !== null && \preg_match('~WCF::get(?:User|Session|UserProfileHandler)\(~', (string)\file_get_contents($file)) === 1) {
    + return true;
    + }
    + }
    +
    + return false;
    + }
    +
    + /**
    + * Describes the expression if it changes data, null otherwise.
    + */
    + private function describeWrite(Expr $expression): ?string
    + {
    + if ($expression instanceof Expr\MethodCall && $expression->name instanceof Node\Identifier) {
    + $method = $expression->name->toString();
    + if (\strcasecmp($method, 'executeAction') === 0) {
    + return 'executeAction()';
    + }
    + if (\preg_match('~^prepare(?:Statement|Unmanaged)?$~i', $method) === 1 && isset($expression->args[0]) && $expression->args[0] instanceof Node\Arg) {
    + $sql = $this->leadingString($expression->args[0]->value);
    + if ($sql !== null && \preg_match('~^\s*(INSERT|UPDATE|DELETE|REPLACE)\b~i', $sql, $match) === 1) {
    + return \strtoupper($match[1]) . ' statement';
    + }
    + }
    + }
    +
    + if ($expression instanceof Expr\New_ && $expression->class instanceof Node\Name) {
    + $name = $expression->class->toString();
    + if (\str_contains(\strtolower($name), '\command\\')) {
    + return 'command ' . $this->shortName($name);
    + }
    + if (\str_ends_with($name, 'Editor')) {
    + return $this->shortName($name);
    + }
    + }
    +
    + if ($expression instanceof Expr\StaticCall && $expression->class instanceof Node\Name && \str_ends_with($expression->class->toString(), 'Editor')) {
    + return $this->shortName($expression->class->toString()) . '::' . ($expression->name instanceof Node\Identifier ? $expression->name->toString() : '…') . '()';
    + }
    +
    + return null;
    + }
    +
    + /**
    + * The literal start of a string expression (a literal, or the left end of a concatenation or interpolation).
    + */
    + private function leadingString(Expr $expression): ?string
    + {
    + while ($expression instanceof Expr\BinaryOp\Concat) {
    + $expression = $expression->left;
    + }
    + if ($expression instanceof Node\Scalar\String_) {
    + return $expression->value;
    + }
    + if ($expression instanceof Node\Scalar\InterpolatedString && ($expression->parts[0] ?? null) instanceof Node\InterpolatedStringPart) {
    + return $expression->parts[0]->value;
    + }
    +
    + return null;
    + }
    +
    + /**
    + * The class name without its namespace.
    + */
    + private function shortName(string $name): string
    + {
    + $position = \strrpos($name, '\\');
    +
    + return $position === false ? $name : \substr($name, $position + 1);
    + }
    +}
  • tests/phpstan/rules.php
    <pre>@@ -6,13 +6,20 @@
    * classes are loaded here, while the configuration is read.
    */
    -require_once __DIR__ . '/OverrideOrDocblockRule.php';
    +$rules = [
    + 'OverrideOrDocblockRule',
    + 'PermissionStrictRule',
    + 'SecureActionRule',
    + 'EndpointRequireAcpRule',
    +];
    +
    +foreach ($rules as $rule) {
    + require_once __DIR__ . '/' . $rule . '.php';
    +}
    return [
    - 'services' => [
    - [
    - 'class' => \yourecom\standard\phpstan\OverrideOrDocblockRule::class,
    - 'tags' => ['phpstan.rules.rule'],
    - ],
    - ],
    + 'services' => \array_map(
    + static fn (string $rule): array => ['class' => 'yourecom\standard\phpstan\\' . $rule, 'tags' => ['phpstan.rules.rule']],
    + $rules,
    + ),
    ];

Bereit, deine Community smarter zu machen?

Ob fertiges Produkt oder individuelle Softwarelösung – überzeuge dich selbst.

Jetzt kaufen Demo testen

Direkter Support vom Entwickler

Zuletzt erledigte Aufgaben

  1. Overlay beim Verschieben eines Themas Clan Verwaltung Clan Verwaltung: Foren

    Marcel Beckers Erledigt am 23. Oktober 2026 um 22:10
  2. Feature-Wunsch: Einführung von Tippgemeinschaften / Team-Wertung (Team-Tippen) Wettbewerbe Basissystem Wettbewerbe: Tippspiel

    villain Erledigt am 22. Oktober 2026 um 08:15
  3. Verschiebung zwischen Clanforen/WSB Clan Verwaltung Clan Verwaltung: Foren

    Marcel Beckers Erledigt am 22. Oktober 2026 um 00:12
  4. Massenbearbeitung von Clans Clan Verwaltung

    Marcel Beckers Erledigt am 22. Oktober 2026 um 00:10
  5. Synchronisation zwischen Clan und Benutzergruppe erweitern Clan Verwaltung

    Marcel Beckers Erledigt am 21. Oktober 2026 um 23:59
  6. Überarbeitung der eigenen Events Clan Verwaltung

    Marcel Beckers Erledigt am 21. Oktober 2026 um 23:40
Arbeitsplatz
YoureCom – Software für Vereine, Teams & Communities

YoureCom – Software für Vereine, Teams & Communities. Seit 2004.

Fertig eingerichtete Lösungen für Verein, Team und Community, dazu Erweiterungen für die WoltLab Suite. Aus Wuppertal, für Kunden weltweit.

Lösungen

  • Tipprunde
  • Aufgaben & Support
  • Mitglieder-Netzwerk
  • Online-Shop
  • Alle Lösungen & Preise

Für Administratoren

  • Plugins
  • Clan Verwaltung
  • Wettbewerbe
  • MultiHunter
  • ByteMon
  • Individuelle Entwicklung

YoureCom

  • Über mich
  • Shop
  • Kontakt
  • Neuigkeiten
  • GitHub

Cloud-Verträge

  • AGB Cloud
  • AVV Cloud
  • Rechtswidrige Inhalte melden
  • Datenwechsel

Rechtliches

  • Datenschutzerklärung
  • Impressum
  • AGB
  • Vertrag widerrufen
  • Abonnements kündigen
  • Widerrufsbelehrung
© 2026 YoureCom · Marcel Beckers