Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cc1c05a
Add `DatabaseObjectBuilder` with `TagBuilder` implementation
BurntimeX Jun 15, 2026
31525aa
Migrate tag forms to `DatabaseObjectBuilder` with command and events
BurntimeX Jun 25, 2026
2587ea6
Simplify checks in `AbstractDatabaseObjectBuilderForm`
BurntimeX Jun 25, 2026
9ecf8f8
Add `beforeDeleteAll()` hook and seal `DatabaseObjectBuilder` API
BurntimeX Jun 26, 2026
4ac2dbf
Use fully qualified `\array_merge()` calls
BurntimeX Jun 26, 2026
57dea39
Add `setID()` for explicit ID assignment on create
BurntimeX Jun 26, 2026
a3791a5
Add `loadValueCallback()` to `IFormField` for custom value loading
BurntimeX Jun 28, 2026
9bebdae
Return void from save value callbacks
BurntimeX Jun 28, 2026
3ed7ca2
Add `updateCounters()` to `DatabaseObjectBuilder
BurntimeX Jun 30, 2026
4156ec4
Add `getHtmlInputProcessor()` to `WysiwygFormField`
BurntimeX Jun 30, 2026
4728f85
Add `afterSave()` hook and use `DatabaseObject` for the form object
BurntimeX Jun 30, 2026
c3b4e41
Drop `DatabaseObjectBuilder::fastSave()`
BurntimeX Jun 30, 2026
427f1a3
Add `getObject()` to `DatabaseObjectBuilder`
BurntimeX Jun 30, 2026
05eed3e
Allow accessing wysiwyg and attachment fields before the form is built
BurntimeX Jun 30, 2026
612e44a
Use a dedicated template for the form field in save/load callback types
BurntimeX Jun 30, 2026
fc5cd40
Honor `loadValueCallback` in `TI18nFormField::updatedObject()`
BurntimeX Jun 30, 2026
3c6235c
Validate required properties in `DatabaseObjectBuilder::create()`
BurntimeX Jul 1, 2026
8254741
Apply suggestions from code review
BurntimeX Jul 5, 2026
458e09d
Add `afterValidateCreate()`, restricted updates and counter increments
dtdesign Jul 6, 2026
95c2d02
Call `create()` and `update()` directly to improve safety
dtdesign Jul 6, 2026
bec93c5
Infer the type of the form field
dtdesign Jul 8, 2026
d23f6dd
Add typings for the save value, move callbacks into a separate interface
dtdesign Jul 11, 2026
b16a372
Improve the type inference
dtdesign Jul 11, 2026
c0ffbd8
Explicitly provide the date format
dtdesign Jul 11, 2026
af64cee
Fix the handling of polls
dtdesign Jul 12, 2026
af8abc8
Fix the type inference
dtdesign Jul 12, 2026
8f9f293
Improve documentation of `DatabaseObjectBuilder:: afterValidateCreate()`
BurntimeX Jul 13, 2026
a898462
Use asymmetric visibility for builder properties
BurntimeX Jul 15, 2026
9f97f23
Pass builder to file events
BurntimeX Jul 15, 2026
bec8e8a
Prevent reuse of a consumed builder
BurntimeX Jul 15, 2026
1a8f791
Associate WYSIWYG embedded content with the edited object
BurntimeX Jul 19, 2026
2cb24e5
Refactor `ArticleAddForm` to builder pattern
BurntimeX Jul 19, 2026
3c84f9e
Migrate article content editor / action to `ArticleContentBuilder`
BurntimeX Jul 19, 2026
591a55b
Migrate article content editor / action to `ArticleContentBuilder`
BurntimeX Jul 19, 2026
c7bfb4b
Fix type error if `getParsedBody` returns null
BurntimeX Jul 20, 2026
19d59f8
Add support for dialog forms using the builder pattern
BurntimeX Jul 20, 2026
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
529 changes: 292 additions & 237 deletions wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php

Large diffs are not rendered by default.

107 changes: 1 addition & 106 deletions wcfsetup/install/files/lib/acp/form/ArticleEditForm.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,11 @@

use wcf\acp\page\ArticleListPage;
use wcf\data\article\Article;
use wcf\data\IStorableObject;
use wcf\form\AbstractFormBuilderForm;
use wcf\http\Helper;
use wcf\system\exception\PermissionDeniedException;
use wcf\system\form\builder\data\processor\CustomFormDataProcessor;
use wcf\system\form\builder\IFormDocument;
use wcf\system\interaction\admin\ArticleInteractions;
use wcf\system\interaction\StandaloneInteractionContextMenuComponent;
use wcf\system\label\object\ArticleLabelObjectHandler;
use wcf\system\language\LanguageFactory;
use wcf\system\request\LinkHandler;
use wcf\system\tagging\TagEngine;
use wcf\system\version\VersionTracker;
use wcf\system\WCF;

Expand All @@ -36,7 +29,7 @@ class ArticleEditForm extends ArticleAddForm
/**
* @inheritDoc
*/
public $formAction = 'edit';
public string $formAction = 'edit';

#[\Override]
public function readParameters(): void
Expand All @@ -60,104 +53,6 @@ protected function readMultilingualSetting(): void
// not required for editing
}

#[\Override]
public function save(): void
{
if (
!WCF::getSession()->hasPermission('admin.content.article.canManageArticle')
&& !WCF::getSession()->hasPermission('admin.content.article.canManageOwnArticles')
) {
$this->additionalFields['publicationStatus'] = $this->formObject->publicationStatus;
$this->additionalFields['publicationDate'] = $this->formObject->publicationDate;
}

AbstractFormBuilderForm::save();

// save labels
$labelIDs = $this->objectAction->getParameters()['labelIDs'] ?? [];
ArticleLabelObjectHandler::getInstance()->setLabels($labelIDs, $this->formObject->articleID);
}

#[\Override]
public function finalizeForm(): void
{
parent::finalizeForm();

$this->form->getDataHandler()
->addProcessor(
new CustomFormDataProcessor(
'editArticleProcessor',
// Save callback: preserve images when user can't use media
function (IFormDocument $document, array $parameters) {
if (!WCF::getSession()->hasPermission('admin.content.cms.canUseMedia')) {
foreach ($this->formObject->getArticleContents() as $languageID => $content) {
$key = $this->isMultilingual ? $languageID : 0;
if (isset($parameters['content'][$key])) {
$parameters['content'][$key]['imageID'] = $content->imageID;
$parameters['content'][$key]['teaserImageID'] = $content->teaserImageID;
}
}
}

return $parameters;
},
// Object callback: load article data for editing
function (IFormDocument $document, array $data, IStorableObject $object) {
\assert($object instanceof Article);

if ($object->publicationDate === 0) {
unset($data['publicationDate']);
}

foreach ($object->getArticleContents() as $languageID => $content) {
if ($this->isMultilingual) {
$language = LanguageFactory::getInstance()->getLanguage($languageID);
if ($language === null) {
continue;
}
$lc = $language->languageCode;

$data["title_{$lc}"] = $content->title;
$data["slug_{$lc}"] = $content->slug;
$data["teaser_{$lc}"] = $content->teaser;
$data["content_{$lc}"] = $content->content;
$data["imageID_{$lc}"] = $content->imageID;
$data["teaserImageID_{$lc}"] = $content->teaserImageID;
$data["metaTitle_{$lc}"] = $content->metaTitle;
$data["metaDescription_{$lc}"] = $content->metaDescription;

if (\MODULE_TAGGING) {
$data["tags_{$lc}"] = TagEngine::getInstance()->getObjectTags(
'com.woltlab.wcf.article',
$content->articleContentID,
[$languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()]
);
}
} else {
$data['title'] = $content->title;
$data['slug'] = $content->slug;
$data['teaser'] = $content->teaser;
$data['content'] = $content->content;
$data['imageID'] = $content->imageID;
$data['teaserImageID'] = $content->teaserImageID;
$data['metaTitle'] = $content->metaTitle;
$data['metaDescription'] = $content->metaDescription;

if (\MODULE_TAGGING) {
$data['tags'] = TagEngine::getInstance()->getObjectTags(
'com.woltlab.wcf.article',
$content->articleContentID,
);
}
}
}

return $data;
}
)
);
}

#[\Override]
public function assignVariables(): void
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace wcf\acp\form;

use wcf\data\AbstractDatabaseObjectAction;
use wcf\data\devtools\project\DevtoolsProject;
use wcf\data\devtools\project\DevtoolsProjectAction;
use wcf\data\devtools\project\DevtoolsProjectList;
Expand Down
121 changes: 61 additions & 60 deletions wcfsetup/install/files/lib/acp/form/TagAddForm.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

namespace wcf\acp\form;

use wcf\data\IStorableObject;
use wcf\command\tag\CreateTag;
use wcf\command\tag\UpdateTag;
use wcf\data\DatabaseObjectBuilder;
use wcf\data\tag\Tag;
use wcf\data\tag\TagAction;
use wcf\data\tag\TagBuilder;
use wcf\data\tag\TagList;
use wcf\form\AbstractFormBuilderForm;
use wcf\form\AbstractDatabaseObjectBuilderForm;
use wcf\system\form\builder\container\FormContainer;
use wcf\system\form\builder\data\processor\CustomFormDataProcessor;
use wcf\system\form\builder\field\IFormField;
use wcf\system\form\builder\field\SingleSelectionFormField;
use wcf\system\form\builder\field\tag\TagFormField;
use wcf\system\form\builder\field\TextFormField;
use wcf\system\form\builder\field\validation\FormFieldValidationError;
use wcf\system\form\builder\field\validation\FormFieldValidator;
use wcf\system\form\builder\IFormDocument;
use wcf\system\form\builder\TemplateFormNode;
use wcf\system\language\LanguageFactory;
use wcf\system\WCF;
Expand All @@ -23,13 +24,13 @@
/**
* Shows the tag add form.
*
* @author Olaf Braun, Tim Duesterhus
* @copyright 2001-2024 WoltLab GmbH
* @author Olaf Braun, Tim Duesterhus, Marcel Werk
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*
* @extends AbstractFormBuilderForm<Tag>
* @extends AbstractDatabaseObjectBuilderForm<Tag, TagBuilder>
*/
class TagAddForm extends AbstractFormBuilderForm
class TagAddForm extends AbstractDatabaseObjectBuilderForm
{
/**
* @inheritDoc
Expand All @@ -49,18 +50,31 @@ class TagAddForm extends AbstractFormBuilderForm
/**
* @inheritDoc
*/
public $objectActionClass = TagAction::class;
public string $objectEditLinkController = TagEditForm::class;

/**
* @inheritDoc
*/
public $objectEditLinkController = TagEditForm::class;
#[\Override]
protected function getDatabaseObjectBuilder(): TagBuilder
{
if ($this->formObject !== null) {
return TagBuilder::forUpdate($this->formObject);
}

return TagBuilder::forCreate();
}

#[\Override]
protected function createForm()
protected function getCommand(DatabaseObjectBuilder $builder): callable
{
parent::createForm();
if ($this->formObject !== null) {
return new UpdateTag($builder);
}

return new CreateTag($builder);
}

#[\Override]
protected function createForm(): void
{
$contentLanguages = LanguageFactory::getInstance()->getContentLanguages();

$this->form->appendChildren([
Expand All @@ -70,12 +84,20 @@ protected function createForm()
->label('wcf.global.name')
->required()
->maximumLength(\TAGGING_MAX_TAG_LENGTH)
->saveValueCallback(static function (TagBuilder $builder, IFormField $field) {
$builder->setName(
\str_replace(',', '', StringUtil::trim($field->getSaveValue()))
);
})
->loadValueCallback(static function (Tag $object, IFormField $field) {
$field->value($object->name);
})
->addValidator(
new FormFieldValidator('duplicateTagValidator', function (TextFormField $field) {
$languageIDFormField = $field->getDocument()->getFormField('languageID');
$languageID = $languageIDFormField->getValue();

$tag = Tag::getTag($field->getValue(), $languageID);
$tag = Tag::getTag($field->getValue(), $languageID ?? 0);
if ($tag !== null && $tag->tagID !== $this->formObject?->tagID) {
$field->addValidationError(
new FormFieldValidationError(
Expand All @@ -92,10 +114,30 @@ protected function createForm()
->options($contentLanguages)
->value(isset($contentLanguages[WCF::getLanguage()->languageID]) ? WCF::getLanguage()->languageID : null)
->immutable($this->formAction !== 'create')
->required(),
->required()
->saveValueCallback(static function (TagBuilder $builder, IFormField $field) {
$builder->setLanguageID(
(int)$field->getSaveValue()
);
})->loadValueCallback(static function (Tag $object, IFormField $field) {
$field->value($object->languageID);
}),
TagFormField::create('synonyms')
->available($this->formObject?->synonymFor === null)
->label('wcf.acp.tag.synonyms'),
->label('wcf.acp.tag.synonyms')
->saveValueCallback(static function (TagBuilder $builder, IFormField $field) {
$builder->setSynonyms(
$field->getSaveValue() ?? []
);
})->loadValueCallback(static function (Tag $object, IFormField $field) {
$synonymList = new TagList();
$synonymList->getConditionBuilder()->add('synonymFor = ?', [$object->getObjectID()]);
$synonymList->readObjects();
$field->value(\array_map(
static fn($synonym) => $synonym->name,
$synonymList->getObjects()
));
}),
TemplateFormNode::create('tagSynonymFor')
->available($this->formObject?->synonymFor !== null)
->variables([
Expand All @@ -105,45 +147,4 @@ protected function createForm()
])
]);
}

#[\Override]
protected function finalizeForm()
{
parent::finalizeForm();

$this->form->getDataHandler()
->addProcessor(
new CustomFormDataProcessor(
'tagNameProcessor',
static function (IFormDocument $document, array $parameters) {
$parameters['data']['name'] = \str_replace(
',',
'',
StringUtil::trim($parameters['data']['name'])
);

return $parameters;
}
)
)
->addProcessor(
new CustomFormDataProcessor(
'synonymsProcessor',
null,
static function (IFormDocument $document, array $data, IStorableObject $tag) {
\assert($tag instanceof Tag);

$synonymList = new TagList();
$synonymList->getConditionBuilder()->add('synonymFor = ?', [$tag->tagID]);
$synonymList->readObjects();
$data['synonyms'] = [];
foreach ($synonymList as $synonym) {
$data['synonyms'][] = $synonym->name;
}

return $data;
}
)
);
}
}
2 changes: 1 addition & 1 deletion wcfsetup/install/files/lib/acp/form/TagEditForm.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class TagEditForm extends TagAddForm
/**
* @inheritDoc
*/
public $formAction = 'edit';
public string $formAction = 'edit';

#[\Override]
public function readParameters()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ public function handle(ServerRequestInterface $request): ResponseInterface
if ($lifetimeInSeconds !== null) {
$expiresAt = (new \DateTimeImmutable('@' . \TIME_NOW))
->modify("+{$lifetimeInSeconds} seconds")
->format(\DateTimeImmutable::RFC7231);
->setTimezone(new \DateTimeZone('UTC'))
->format('D, d M Y H:i:s \\G\\M\\T');
$maxAge = \sprintf(
'max-age=%d, private',
$lifetimeInSeconds ?: 0,
Expand Down
Loading
Loading