Skip to content

Commit ab1f78a

Browse files
authored
Merge pull request #472 from WoltLab/6.1-migration-UploadFormField-to-FileProcessorFormField
6.1 migration upload form field to file processor form field
2 parents 610e7cb + 3b39caf commit ab1f78a

File tree

2 files changed

+261
-12
lines changed

2 files changed

+261
-12
lines changed

docs/migration/wsc60/php.md

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,252 @@ public class FooBarAction extends AbstractDatabaseObjectAction implements IMessa
198198
}
199199
}
200200
```
201+
202+
## Migration to `FileProcessorFormField`
203+
204+
Previously, the `UploadFormField` class was used to create file upload fields in forms.
205+
It is strongly recommended to use the new `FileProcessorFormField` instead which separates file validation and processing into a dedicated class, the `IFileProcessor`.
206+
207+
Only the fileID (or fileIDs) now need to be saved in the database.
208+
These should reference `wcf1_file.fileID` through a foreign key.
209+
210+
The previously required function (`getFooUploadFiles`) to retrieve `UploadFile[]` is no longer needed and can be removed.
211+
212+
### Example
213+
214+
In this example, the `Foo` object will store the `imageID` of the uploaded file.
215+
216+
#### Example using `FileProcessorFormField`
217+
218+
The form field now provides information about which `IFileProcessor` should be used for the file upload, by specifying the object type for the definition `com.woltlab.wcf.file`.
219+
220+
```PHP
221+
final class FooAddForm extends AbstractFormBuilderForm
222+
{
223+
#[\Override]
224+
protected function createForm(): void
225+
{
226+
parent::createForm();
227+
228+
$this->form->appendChildren([
229+
FormContainer::create('imageContainer')
230+
->appendChildren([
231+
FileProcessorFormField::create('imageID')
232+
->singleFileUpload()
233+
->required()
234+
->objectType('foo.bar.image')
235+
]),
236+
]);
237+
}
238+
}
239+
```
240+
241+
#### Example for implementing an `IFileProcessor`
242+
243+
The `objectID` in the `$context` comes from the form and corresponds to the objectID of the `FooAddForm::$formObject`.
244+
245+
This is a rather exhaustive example and tries to cover a lot of different use cases including but not limited to fine-grained control through user group permissions.
246+
247+
The `AbstractFileProcessor` implementation already provides a lot of sane defaults for less restricted uploads.
248+
For example, if your field allows arbitrary files to be uploaded, you can skip that check in `acceptUpload()` and also remove the overriden method `getAllowedFileExtensions()` because the base implementation already permits all types of files.
249+
250+
Please see the explanation of the new [file uploads](../../php/api/file_uploads.md) to learn more.
251+
252+
```PHP
253+
final class FooImageFileProcessor extends AbstractFileProcessor
254+
{
255+
#[\Override]
256+
public function acceptUpload(string $filename, int $fileSize, array $context): FileProcessorPreflightResult
257+
{
258+
if (isset($context['objectID'])) {
259+
$foo = $this->getFoo($context);
260+
if ($foo === null) {
261+
return FileProcessorPreflightResult::InvalidContext;
262+
}
263+
264+
if (!$foo->canEdit()) {
265+
return FileProcessorPreflightResult::InsufficientPermissions;
266+
}
267+
} elseif (!WCF::getSession()->getPermission('foo.bar.canAdd')) {
268+
return FileProcessorPreflightResult::InsufficientPermissions;
269+
}
270+
271+
if ($fileSize > $this->getMaximumSize($context)) {
272+
return FileProcessorPreflightResult::FileSizeTooLarge;
273+
}
274+
275+
if (!FileUtil::endsWithAllowedExtension($filename, $this->getAllowedFileExtensions($context))) {
276+
return FileProcessorPreflightResult::FileExtensionNotPermitted;
277+
}
278+
279+
return FileProcessorPreflightResult::Passed;
280+
}
281+
282+
#[\Override]
283+
public function getMaximumSize(array $context): ?int
284+
{
285+
return WCF::getSession()->getPermission('foo.bar.image.maxSize');
286+
}
287+
288+
#[\Override]
289+
public function getAllowedFileExtensions(array $context): array
290+
{
291+
return \explode("\n", WCF::getSession()->getPermission('foo.bar.image.allowedFileExtensions'));
292+
}
293+
294+
#[\Override]
295+
public function canAdopt(File $file, array $context): bool
296+
{
297+
$fooFromContext = $this->getFoo($context);
298+
$fooFromCoreFile = $this->getFooByFile($file);
299+
300+
if ($fooFromCoreFile === null) {
301+
return true;
302+
}
303+
304+
if ($fooFromCoreFile->fooID === $fooFromContext->fooID) {
305+
return true;
306+
}
307+
308+
return false;
309+
}
310+
311+
#[\Override]
312+
public function adopt(File $file, array $context): void
313+
{
314+
$foo = $this->getFoo($context);
315+
if ($foo === null) {
316+
return;
317+
}
318+
319+
(new FooEditor($foo))->update([
320+
'imageID' => $file->fileID,
321+
]);
322+
}
323+
324+
#[\Override]
325+
public function canDelete(File $file): bool
326+
{
327+
$foo = $this->getFooByFile($file);
328+
if ($foo === null) {
329+
return WCF::getSession()->getPermission('foo.bar.canAdd');
330+
}
331+
332+
return false;
333+
}
334+
335+
#[\Override]
336+
public function canDownload(File $file): bool
337+
{
338+
$foo = $this->getFooByFile($file);
339+
if ($foo === null) {
340+
return WCF::getSession()->getPermission('foo.bar.canAdd');
341+
}
342+
343+
return $foo->canRead();
344+
}
345+
346+
#[\Override]
347+
public function delete(array $fileIDs, array $thumbnailIDs): void
348+
{
349+
$fooList = new FooList();
350+
$fooList->getConditionBuilder()->add('imageID IN (?)', [$fileIDs]);
351+
$fooList->readObjects();
352+
353+
if ($fooList->count() === 0) {
354+
return;
355+
}
356+
357+
(new FooAction($fooList->getObjects(), 'delete'))->executeAction();
358+
}
359+
360+
#[\Override]
361+
public function getObjectTypeName(): string
362+
{
363+
return 'foo.bar.image';
364+
}
365+
366+
#[\Override]
367+
public function countExistingFiles(array $context): ?int
368+
{
369+
$foo = $this->getFoo($context);
370+
if ($foo === null) {
371+
return null;
372+
}
373+
374+
return $foo->imageID === null ? 0 : 1;
375+
}
376+
377+
private function getFoo(array $context): ?Foo
378+
{
379+
// extract foo from context
380+
}
381+
382+
private function getFooByFile(File $file): ?Foo
383+
{
384+
// search foo in database by given file
385+
}
386+
}
387+
```
388+
389+
### Migrating existing files
390+
391+
To insert existing files into the upload pipeline, a `RebuildDataWorker` should be used which calls `FileEditor::createFromExistingFile()`.
392+
393+
#### Example for a `RebuildDataWorker`
394+
395+
```PHP
396+
final class FooRebuildDataWorker extends AbstractLinearRebuildDataWorker
397+
{
398+
/**
399+
* @inheritDoc
400+
*/
401+
protected $objectListClassName = FooList::class;
402+
403+
/**
404+
* @inheritDoc
405+
*/
406+
protected $limit = 100;
407+
408+
#[\Override]
409+
public function execute()
410+
{
411+
parent::execute();
412+
413+
$fooToFileID = [];
414+
$defunctFileIDs = [];
415+
416+
foreach ($this->objectList as $foo) {
417+
if ($foo->imageID !== null) {
418+
continue;
419+
}
420+
421+
$file = FileEditor::createFromExistingFile(
422+
$foo->getLocation(),
423+
$foo->getFilename(),
424+
'foo.bar.image'
425+
);
426+
427+
if ($file === null) {
428+
$defunctFileIDs[] = $foo->fooID;
429+
continue;
430+
}
431+
432+
$fooToFileID[$foo->fooID] = $file->fileID;
433+
}
434+
435+
$this->saveFileIDs($fooToFileID);
436+
// disable or delete defunct foo objects
437+
}
438+
439+
/**
440+
* @param array<int,int> $fooToFileID
441+
*/
442+
private function saveFileIDs(array $fooToFileID): void
443+
{
444+
// store fileIDs in database
445+
}
446+
}
447+
```
448+
449+
See [WoltLab/WCF#5911](https://github.com/WoltLab/WCF/pull/5951) for more details.

docs/php/api/file_uploads.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ Attachments are implemented as an extra layer on top of the upload pipeline.
66

77
The most common use cases are the attachment system that relies on the WYSIWYG editor as well as the `FileProcessorFormField`.
88

9-
# Provide the `IFileProcessor`
9+
## Provide the `IFileProcessor`
1010

1111
At the very core of each uploadable type is an implementation of `IFileProcessor` that handles the validation and handling of new files.
1212

1313
It is strongly recommended to derive from `AbstractFileProcessor` that makes it easy to opt out of some extra features and will provide backwards compatibility with new features.
1414

15-
## Important Methods and Features
15+
### Important Methods and Features
1616

1717
Please refer to the documentation in `IFileProcessor` for an explanation of methods that are not explained here.
1818

19-
### Thumbnails
19+
#### Thumbnails
2020

2121
It is possible to automatically generate thumbnails for any uploaded file.
2222
The number of thumbnail variants is not limited but each thumbnail must have an identifier that is unique for your file processor.
@@ -50,7 +50,7 @@ The existing rebuild data worker for files will check the existing thumbnails ag
5050

5151
The identifier must be stable because it is used to verify if a specific thumbnail still matches the configured settings.
5252

53-
### Resizing Images Before Uploading
53+
#### Resizing Images Before Uploading
5454

5555
You can opt-in to the resize feature by returning a custom `ResizeConfiguration` from `getResizeConfiguration`, otherwise images of arbitrary size will be accepted.
5656

@@ -75,22 +75,22 @@ public function getResizeConfiguration(): ResizeConfiguration
7575
The `ResizeFileType` controls the output format of the resized image.
7676
The available options are `jpeg` and `webp` but it is also possible to keep the existing image format.
7777

78-
### Adopting Files and Thumbnails
78+
#### Adopting Files and Thumbnails
7979

8080
Files are associated with your object type after being uploaded but you possibly want to store the file id in your database table.
8181
This is where `adopt(File $file, array $context): void` comes into play which notifies you of the successful upload of a file while providing the context that is used to upload the file in the first place.
8282

8383
Thumbnails are generated in a separate request for performance reasons and you are being notified through `adoptThumbnail(FileThumbnail $thumbnail): void`.
8484
This is meant to allow you to track the thumbnail id in the database.
8585

86-
### Tracking Downloads
86+
#### Tracking Downloads
8787

8888
File downloads are handled through the `FileDownloadAction` which validates the requested file and permissions to download it.
8989
Every time a file is being downloaded, `trackDownload(File $file): void` is invoked to allow you to update any counters.
9090

9191
Static images are served directly by the web server for performance reasons and it is not possible to track those accesses.
9292

93-
## Registering the File Processor
93+
### Registering the File Processor
9494

9595
The file processor is registered as an object type for `com.woltlab.wcf.file` through the `objectType.xml`:
9696

@@ -109,11 +109,11 @@ The integration with the form builder enables you to focus on the file processin
109109

110110
Please see documentation for [FileProcessorFormField](form_fields.md#fileprocessorformfield) to learn more.
111111

112-
# Implementing an Unmanaged File Upload
112+
## Implementing an Unmanaged File Upload
113113

114114
If you cannot use or want to use the existing form builder implementation you can still implement the UI yourself following this guide.
115115

116-
## Creating the Context for New Files
116+
### Creating the Context for New Files
117117

118118
The HTML element for the file upload is generated through the helper method `FileProcessor::getHtmlElement()` that expects a reference to your `IFileProcessor` as well as a context for new files.
119119

@@ -137,7 +137,7 @@ final class ExampleFileProcessor extends AbstractFileProcessor {
137137
This code will generate a `<woltlab-core-file-upload>` HTML element that can be inserted anywhere on the page.
138138
You do not need to initialize any extra JavaScript to make the element work, it will be initialized dynamically.
139139

140-
## Lifecycle of Uploaded Files
140+
### Lifecycle of Uploaded Files
141141

142142
Any file that passes the pre-upload validation will be uploaded to the server.
143143
This will trigger the `uploadStart` event on the `<woltlab-core-file-upload>` element, exposing a `<woltlab-core-file>` element as the only detail.
@@ -150,11 +150,11 @@ A successful upload will resolve the promise without any value, you can then acc
150150
A failed upload will reject the `.ready` promise without any value, instead you can retrieve the error through the `.apiError` property if you want to further process it.
151151
The UI is automatically updated with the error message, you only need to handle the `.apiError` property if you need to inspect the root cause.
152152

153-
## Deleting a File
153+
### Deleting a File
154154

155155
You can delete a file from the UI by invoking `deleteFile()` from `WoltLabSuite/Core/Api/Files` which takes the value of `.fileId`.
156156

157-
## Render a Previously Uploaded File
157+
### Render a Previously Uploaded File
158158

159159
You can render the `<woltlab-core-file>` element through `File::toHtmlElement()`.
160160
This method accepts an optional list of meta data that is serialized to JSON and exposed on the `data-meta-data` property.

0 commit comments

Comments
 (0)