Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
5967ec8
Implement basic condition types and provider
Cyperghost May 21, 2025
12055c4
Migrate user group assignment form to form builder
Cyperghost May 21, 2025
815a8dc
Implement condition form container
Cyperghost May 21, 2025
985fbd1
Load and save conditions
Cyperghost May 21, 2025
234679b
Implement condition date form field
Cyperghost May 22, 2025
abc4eb6
Use the new condition system in the user group assignment handler
Cyperghost May 23, 2025
67f0a77
Remove unnecessary ConditionHandler calls
Cyperghost May 23, 2025
3fcf09d
Add some phrases
Cyperghost May 23, 2025
a0e4c73
Reformate code
Cyperghost May 23, 2025
5f4ffd3
Handle invalid value
Cyperghost May 23, 2025
2d04ee6
Remove unnecessary conditions
Cyperghost May 23, 2025
47c505e
Check whether an invalid date value was sent
Cyperghost May 26, 2025
4757c9e
Add event for condition provider
Cyperghost May 26, 2025
9bc6dda
Add condition types for user is and is not in group
Cyperghost May 26, 2025
cb94d43
Add condition types for days after registration
Cyperghost May 26, 2025
7a5c603
Add user username condition type and associated form field
Cyperghost May 26, 2025
9276ab6
Allow any type as a filter and do not (un)serialize
Cyperghost May 27, 2025
2ed529e
Remove the type `ConditionValue` and replace it with `mixed`.
Cyperghost May 27, 2025
48efdf7
Store time/date as unix timestamp
Cyperghost May 27, 2025
135ca54
Refactor condition type match methods to enforce strict type checking
Cyperghost May 27, 2025
715a3e3
Add language variables
Cyperghost May 27, 2025
65773d2
Add missing database column for the conditions
BurntimeX May 29, 2025
61357c7
Use a global prefix container for conditions that are supposed to sto…
Cyperghost Jun 2, 2025
e3fcf76
Use a meaningful name for the template types.
Cyperghost Jun 2, 2025
b68ace6
Make the prefix form field a required field and change it to a generi…
Cyperghost Jun 3, 2025
18ac2e6
Refactor condition class names for consistency and clarity
Cyperghost Jun 3, 2025
70403b2
Refactor ConditionFormField to remove lastConditionIndex and adjust i…
Cyperghost Jun 4, 2025
02cc584
Refactor `ConditionFormContainer` for improved PHPStan compatibility
Cyperghost Jun 10, 2025
5fbc871
Sort condition types by name
Cyperghost Jun 4, 2025
ccec4f4
Add user condition types for email, language, avatar, and signature
Cyperghost Jun 4, 2025
80bfb6b
Implement an `AbstractUserBooleanConditionType` and coverPhoto condit…
Cyperghost Jun 4, 2025
f7b2a53
Add abstract user string condition type and refactor email/username c…
Cyperghost Jun 4, 2025
9f87171
Improve sorting logic
Cyperghost Jun 4, 2025
2510f26
Rename 'id' to 'identifier' in AbstractUserBooleanConditionType
Cyperghost Jun 4, 2025
cc6867b
Fix condition handling in UserRegistrationDaysConditionType
Cyperghost Jun 4, 2025
dadd545
Add user condition types for banned, email confirmation, and enabled …
Cyperghost Jun 4, 2025
455115e
Add user trophy condition types
Cyperghost Jun 4, 2025
9c7d401
Add user integer condition types for activity points, likes received,…
Cyperghost Jun 4, 2025
2b15f96
Fix SQL query syntax and add TODO for user options conditions
Cyperghost Jun 5, 2025
8d3f738
Refactor user condition types to extend AbstractUserIsNullConditionTy…
Cyperghost Jun 5, 2025
4ea3ea2
Refactor user condition types to use anonymous classes for improved f…
Cyperghost Jun 5, 2025
6104e0b
Add multifactor authentication condition to user conditions
Cyperghost Jun 5, 2025
25d86b9
Fix condition logic for user signature and null checks
Cyperghost Jun 10, 2025
6fcefb9
Apply suggestions from code review
Cyperghost Jun 17, 2025
9906fa2
Fix comparison operators
Cyperghost Jun 17, 2025
6362be9
Add missing `.` in the sql query
Cyperghost Jun 18, 2025
bd9747a
Fix comparison logic
Cyperghost Jun 18, 2025
67f0256
Fix matches logic
Cyperghost Jun 18, 2025
1dd2d2d
Change Filter Type to string when `SelectFormField` is used
Cyperghost Jun 18, 2025
8b415f1
Escape special characters
Cyperghost Jun 18, 2025
c482ad0
Compare strings case-insensitive
Cyperghost Jun 18, 2025
aef3d3a
Fix condition comparison logic
Cyperghost Jun 18, 2025
c2b7354
Add IMigrateConditionType interface and implement migration methods i…
Cyperghost Jun 5, 2025
9d1c9be
Implement migration support for user condition types
Cyperghost Jun 10, 2025
508c36f
Add migration support for user group assignment conditions
Cyperghost Jun 10, 2025
f85ba58
Use a unique structure for the object types for migration.
Cyperghost Jun 18, 2025
e91a9ee
Rename 'needMigration' to 'isLegacy'
Cyperghost Jun 29, 2025
c6c7ae8
Apply suggestions from code review
Cyperghost Jun 29, 2025
4f105b0
Refactor `ConditionMigration`
Cyperghost Jun 29, 2025
aae31ac
Refactor migration condition checks for consistency
Cyperghost Jun 29, 2025
c63a223
Refactor condition data migration logic to improve null handling
Cyperghost Jun 29, 2025
713642b
Migrate user condition types to use named parameters for improved rea…
Cyperghost Jun 29, 2025
a47ba70
Add legacy notice for automatic user group assignments and implement …
Cyperghost Jun 29, 2025
e9b67b4
Rename `UserGroupAssignmentMigrateCondition` to `MigrateLegacyCondition`
Cyperghost Jun 29, 2025
d6009bb
Fix typo in `$field`
Cyperghost Jun 29, 2025
9033e8f
Add error handling for JSON decoding in condition migration
Cyperghost Jun 29, 2025
a78416b
Fix incorrect usage of unset function in ConditionHandler
Cyperghost Jun 29, 2025
206f69e
Use `DateTimeImmutable` instead of `\DateTime`
Cyperghost Jun 29, 2025
899599a
Expand the PHPDoc for methods in `IMigrateConditionType`
Cyperghost Jun 30, 2025
88c4d14
Apply suggestions from code review
Cyperghost Jul 7, 2025
94550e4
Add a status box if content needs to be recalculated using Rebuild Data.
Cyperghost Jul 7, 2025
6d38f37
Invert check if migrations needed
Cyperghost Jul 10, 2025
926d312
Avoid inheriting from concrete implementations
dtdesign Jul 10, 2025
13c7979
Fix the class naming to be consistent
dtdesign Jul 10, 2025
d9bf06c
`\strtolower()` is not binary-safe
dtdesign Jul 10, 2025
0f8d751
Fix the handling of condition types
dtdesign Jul 10, 2025
53448d3
Mark the abstract user conditions as non-abstract
dtdesign Jul 10, 2025
d2c251d
Register the conditions sequentually
dtdesign Jul 10, 2025
a25534b
Fix the direction of the comparison
dtdesign Jul 10, 2025
2b356a4
Simplify the condition names
dtdesign Jul 11, 2025
28550ca
Rename the class to the correct name
Cyperghost Jul 11, 2025
2abe329
Adding language variable
Cyperghost Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions com.woltlab.wcf/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@
</instructions>

<!--
Required order of the following steps for the update to 6.2:
<instruction type="script">acp/update_com.woltlab.wcf_6.2_backgroundJob.php</instruction>
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_62_step1.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.2_contactOptions.php</instruction>
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_62_step2.php</instruction>
Required order of the following steps for the update to 6.3:
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_6.3_step1.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php</instruction>
-->
</package>
36 changes: 36 additions & 0 deletions com.woltlab.wcf/templates/shared_conditionFormContainer.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<section id="{$container->getPrefixedId()}Container"{*
*}{if !$container->getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}{if !$container->checkDependencies()} style="display: none" {/if}{*
*}>
{if $container->getLabel() !== null}
{if $container->getDescription() !== null}
<header class="sectionHeader">
<h2 class="sectionTitle">{unsafe:$container->getLabel()}{if $container->markAsRequired()}<span class="formFieldRequired">*</span>{/if}</h2>
<p class="sectionDescription">{unsafe:$container->getDescription()}</p>
</header>
{else}
<h2 class="sectionTitle">{unsafe:$container->getLabel()}{if $container->markAsRequired()}<span class="formFieldRequired">*</span>{/if}</h2>
{/if}
{/if}

<div class="conditions" id="{$container->getPrefixedId()}Conditions">
{include file='shared_formContainerChildren'}
</div>

<button type="button" class="button" id="{$container->getPrefixedId()}AddCondition">
{lang}wcf.condition.add{/lang}
</button>
</section>

{include file='shared_formContainerDependencies'}

<script data-relocate="true">
require([
'WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/Default',
'WoltLabSuite/Core/Form/Builder/Container/ConditionFormField',
], (DefaultContainerDependency, { ConditionFormField }) => {
new DefaultContainerDependency('{unsafe:$container->getPrefixedId()|encodeJS}Container');
new ConditionFormField('{unsafe:$container->getPrefixedId()|encodeJS}', '{link controller="ConditionAdd" isACP=false provider=$container->getConditionProviderClass()}{/link}');
});
</script>
60 changes: 60 additions & 0 deletions com.woltlab.wcf/templates/shared_prefixFormFieldContainer.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{assign var='field' value=$container->getField()}
{assign var='prefixField' value=$container->getPrefixField()}

<dl id="{$field->getPrefixedId()}Container"{*
*}{if !$field->getClasses()|empty} class="{implode from=$field->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{foreach from=$field->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}{if !$field->checkDependencies()} style="display: none
;"{/if}{*
*}>
<dt>{if $container->getLabel() !== null}<label for="{$field->getPrefixedId()}">{unsafe:$container->getLabel()}</label>{if $field->isRequired() && $form->marksRequiredFields()} <span class="formFieldRequired">*</span>{/if}{/if}</dt>
<dd>
<div class="inputAddon">
{if $prefixField->isAvailable()}
{if !$container->prefixHasSelectableOptions()}
<span class="inputPrefix">{unsafe:$prefixField->getFieldHtml()}</span>
{else}
<span class="inputPrefix dropdown" id="{$prefixField->getPrefixedId()}_dropdown">
<span class="dropdownToggle">{unsafe:$container->getSelectedPrefixOption()[label]} {icon name='caret-down' type='solid'}</span>

<ul class="dropdownMenu">
{foreach from=$prefixField->getNestedOptions() item=__fieldNestedOption}
<li{if ($prefixField->getValue() == $__fieldNestedOption[value] && $__fieldNestedOption[isSelectable]) || !$__fieldNestedOption[isSelectable]} class="{if $prefixField->getValue() == $__fieldNestedOption[value] && $__fieldNestedOption[isSelectable]}active{if !$__fieldNestedOption[isSelectable]} disabled{/if}{else}disabled{/if}"{/if} data-value="{$__fieldNestedOption[value]}" data-label="{$__fieldNestedOption[label]}"><span>{unsafe:'&nbsp;'|str_repeat:$__fieldNestedOption[depth] * 4}{unsafe:$__fieldNestedOption[label]}</span></li>
{/foreach}
</ul>
<input type="hidden" id="{$prefixField->getPrefixedId()}" name="{$prefixField->getPrefixedId()}" value="{if $prefixField->getValue() === null}{$container->getSelectedPrefixOption()[value]}{else}{$prefixField->getValue()}{/if}" />
</span>
{/if}
{include file='shared_formFieldDependencies' field=$prefixField sandbox=true}
{include file='shared_formFieldDataHandler' field=$prefixField sandbox=true}
{/if}
{unsafe:$field->getFieldHtml()}
</div>

{if $container->getDescription() !== null}
<small>{unsafe:$container->getDescription()}</small>
{/if}

{include file='shared_formFieldErrors' field=$field sandbox=true}

{if $prefixField !== null && $prefixField->isAvailable()}
{foreach from=$prefixField->getValidationErrors() item='validationError'}
{unsafe:$validationError->getHtml()}
{/foreach}
{/if}

{include file='shared_formFieldDependencies' field=$field sandbox=true}
{include file='shared_formFieldDataHandler' field=$field sandbox=true}
</dd>
</dl>

{if $prefixField->isAvailable() && !$prefixField->isImmutable() && $container->prefixHasSelectableOptions()}
<script data-relocate="true">
require(['WoltLabSuite/Core/Form/Builder/Container/SuffixFormField'], function(FormBuilderPrefixFormFieldContainer) {
new FormBuilderPrefixFormFieldContainer(
'{unsafe:$container->getDocument()->getId()|encodeJS}',
'{unsafe:$prefixField->getPrefixedId()|encodeJS}',
);
});
</script>
{/if}
72 changes: 72 additions & 0 deletions ts/WoltLabSuite/Core/Form/Builder/Container/ConditionFormField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/

import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
import { insertHtml } from "WoltLabSuite/Core/Dom/Util";
import { unescapeHTML } from "WoltLabSuite/Core/StringUtil";
import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
import { getPhrase } from "WoltLabSuite/Core/Language";

interface ConditionAddResponse {
field: string;
conditionType: string;
}

export class ConditionFormField {
readonly #containerId: string;
readonly #container: HTMLElement;
readonly #button: HTMLButtonElement;
#index: number = 0;

constructor(containerId: string, endpoint: string) {
this.#containerId = containerId;
this.#container = document.getElementById(`${containerId}Conditions`) as HTMLElement;

this.#button = document.getElementById(`${containerId}AddCondition`) as HTMLButtonElement;
this.#button?.addEventListener(
"click",
promiseMutex(async () => {
await this.#showConditionAddDialog(endpoint);
}),
);

wheneverFirstSeen(`#${containerId}Container .condition__container`, (container: HTMLElement) => {
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.classList.add("button", "small", "jsTooltip", "condition__remove");
deleteButton.title = getPhrase("wcf.global.button.delete");
const icon = document.createElement("fa-icon");
icon.setIcon("times");
deleteButton.appendChild(icon);
container.prepend(deleteButton);
deleteButton.addEventListener("click", () => {
container.remove();
});

const index = parseInt(container.dataset.conditionIndex!);
this.#index = Math.max(this.#index, index);
const hidden = document.createElement("input");
hidden.type = "hidden";
hidden.name = `${containerId}[${index}]`;
hidden.value = container.dataset.conditionType!;
container.appendChild(hidden);
});
}

async #showConditionAddDialog(endpoint: string) {
const url = new URL(unescapeHTML(endpoint));
url.searchParams.set("containerId", this.#containerId);
url.searchParams.set("index", (this.#index + 1).toString());

const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint<ConditionAddResponse>(url.toString());

if (ok) {
insertHtml(result.field, this.#container, "append");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* Updates the database layout during the update from 6.2 to 6.3.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/

use wcf\system\database\table\column\DefaultFalseBooleanDatabaseTableColumn;
use wcf\system\database\table\column\MediumtextDatabaseTableColumn;
use wcf\system\database\table\PartialDatabaseTable;

return [
PartialDatabaseTable::create('wcf1_user_group_assignment')
->columns([
MediumtextDatabaseTableColumn::create('conditions'),
DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'),
]),
];
66 changes: 1 addition & 65 deletions wcfsetup/install/files/acp/templates/userGroupAssignmentAdd.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,70 +17,6 @@
</nav>
</header>

{include file='shared_formNotice'}

<form method="post" action="{if $action == 'add'}{link controller='UserGroupAssignmentAdd'}{/link}{else}{link controller='UserGroupAssignmentEdit' object=$assignment}{/link}{/if}">
<div class="section">
<dl{if $errorField == 'title'} class="formError"{/if}>
<dt><label for="title">{lang}wcf.global.name{/lang}</label></dt>
<dd>
<input type="text" id="title" name="title" value="{$title}" class="long">
{if $errorField == 'title'}
<small class="innerError">
{if $errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{lang}wcf.acp.group.assignment.title.error.{$errorType}{/lang}
{/if}
</small>
{/if}
</dd>
</dl>

<dl{if $errorField == 'groupID'} class="formError"{/if}>
<dt><label for="groupID">{lang}wcf.user.group{/lang}</label></dt>
<dd>
{htmlOptions name='groupID' id='groupID' options=$userGroups selected=$groupID}
{if $errorField == 'groupID'}
{if $errorType == 'noValidSelection'}
<small class="innerError">{lang}wcf.global.form.error.noValidSelection{/lang}</small>
{else}
<small class="innerError">{lang}wcf.acp.group.assignment.groupID.error.{$errorType}{/lang}</small>
{/if}
{/if}
</dd>
</dl>

<dl>
<dt></dt>
<dd>
<label><input type="checkbox" id="isDisabled" name="isDisabled"{if $isDisabled} checked{/if}> {lang}wcf.acp.group.assignment.isDisabled{/lang}</label>
</dd>
</dl>

{event name='dataFields'}
</div>

{event name='sections'}

<section class="section">
<header class="sectionHeader">
<h2 class="sectionTitle">{lang}wcf.acp.group.assignment.conditions{/lang}</h2>
<p class="sectionDescription">{lang}wcf.acp.group.assignment.conditions.description{/lang}</p>
</header>

{if $errorField == 'conditions'}
<woltlab-core-notice type="error">{lang}wcf.acp.group.assignment.error.noConditions{/lang}</woltlab-core-notice>
{/if}

{include file='shared_userConditions'}
</section>

<div class="formSubmit">
<input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
<input type="hidden" name="action" value="{$action}">
{csrfToken}
</div>
</form>
{unsafe:$form->getHtml()}

{include file='footer'}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
</nav>
</header>

{if $hasLegacyObjects}
<woltlab-core-notice type="warning">
{lang}wcf.acp.group.assignment.legacyNotice{/lang}
</woltlab-core-notice>
{/if}

<div class="section">
{unsafe:$gridView->render()}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

use wcf\system\condition\ConditionHandler;
use wcf\system\WCF;
use wcf\util\JSON;

$exportedConditions = ConditionHandler::getInstance()->exportConditions("com.woltlab.wcf.condition.userGroupAssignment");
if ($exportedConditions === []) {
return;
}

$sql = "UPDATE wcf1_user_group_assignment
SET conditions = ?,
isLegacy = ?
WHERE assignmentID = ?";
$statement = WCF::getDB()->prepare($sql);
foreach ($exportedConditions as $assignmentID => $conditionData) {
renameObjectTypes($conditionData);

$statement->execute([
JSON::encode($conditionData),
1,
$assignmentID,
]);
}

/**
* Rename the object types so that the migration functions can handle them.
* @see \wcf\system\condition\provider\UserConditionProvider
*
* @param array<string, mixed> $conditionData
*/
function renameObjectTypes(array &$conditionData): void
{
$objectTypeMap = [
'com.woltlab.wcf.username' => 'com.woltlab.wcf.user.username',
'com.woltlab.wcf.email' => 'com.woltlab.wcf.user.email',
'com.woltlab.wcf.userGroup' => 'com.woltlab.wcf.user.userGroup',
'com.woltlab.wcf.languages' => 'com.woltlab.wcf.user.languages',
'com.woltlab.wcf.registrationDate' => 'com.woltlab.wcf.user.registrationDate',
'com.woltlab.wcf.registrationDateInterval' => 'com.woltlab.wcf.user.registrationDateInterval',
'com.woltlab.wcf.avatar' => 'com.woltlab.wcf.user.avatar',
'com.woltlab.wcf.signature' => 'com.woltlab.wcf.user.signature',
'com.woltlab.wcf.coverPhoto' => 'com.woltlab.wcf.user.coverPhoto',
'com.woltlab.wcf.state' => 'com.woltlab.wcf.user.state',
'com.woltlab.wcf.activityPoints' => 'com.woltlab.wcf.user.activityPoints',
'com.woltlab.wcf.likesReceived' => 'com.woltlab.wcf.user.likesReceived',
// TODO 'com.woltlab.wcf.userOptions'
'com.woltlab.wcf.userTrophyCondition' => 'com.woltlab.wcf.user.trophyCondition',
'com.woltlab.wcf.trophyPoints' => 'com.woltlab.wcf.user.trophyPoints',
];

foreach ($objectTypeMap as $currentName => $newName) {
if (isset($conditionData[$currentName])) {
$conditionData[$newName] = $conditionData[$currentName];
unset($conditionData[$currentName]);
}
}
}
Loading