<button type="button">
<woltlab-core-file-upload
data-endpoint="{link controller='FileUploadPreflight'}{/link}"
+ data-type-name="com.woltlab.wcf.attachment"
+ data-context-object-type=""
+ data-context-object-id=""
></woltlab-core-file-upload>
</button>
<dl class="wide">
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { ucfirst } from "WoltLabSuite/Core/StringUtil";
type PreflightResponse = {
endpoints: string[];
};
async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<void> {
+ const typeName = element.dataset.typeName!;
+ const context = getContextFromDataAttributes(element);
+
const fileHash = await getSha256Hash(await file.arrayBuffer());
const response = (await prepareRequest(element.dataset.endpoint!)
filename: file.name,
fileSize: file.size,
fileHash,
+ typeName,
+ context,
})
.fetchAsJson()) as PreflightResponse;
const { endpoints } = response;
}
}
+function getContextFromDataAttributes(element: WoltlabCoreFileUploadElement): Record<string, string> {
+ const context = {};
+ const prefixContext = "data-context-";
+
+ for (const attribute of element.attributes) {
+ if (!attribute.name.startsWith(prefixContext)) {
+ continue;
+ }
+
+ const key = attribute.name
+ .substring(prefixContext.length)
+ .split("-")
+ .map((part, index) => {
+ if (index === 0) {
+ return part;
+ }
+
+ return ucfirst(part);
+ })
+ .join("");
+
+ context[key] = attribute.value;
+ }
+
+ return context;
+}
+
async function getSha256Hash(data: BufferSource): Promise<string> {
const buffer = await window.crypto.subtle.digest("SHA-256", data);
-define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Helper/Selector"], function (require, exports, Backend_1, Selector_1) {
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/StringUtil"], function (require, exports, Backend_1, Selector_1, StringUtil_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setup = void 0;
async function upload(element, file) {
+ const typeName = element.dataset.typeName;
+ const context = getContextFromDataAttributes(element);
const fileHash = await getSha256Hash(await file.arrayBuffer());
const response = (await (0, Backend_1.prepareRequest)(element.dataset.endpoint)
.post({
filename: file.name,
fileSize: file.size,
fileHash,
+ typeName,
+ context,
})
.fetchAsJson());
const { endpoints } = response;
}
}
}
+ function getContextFromDataAttributes(element) {
+ const context = {};
+ const prefixContext = "data-context-";
+ for (const attribute of element.attributes) {
+ if (!attribute.name.startsWith(prefixContext)) {
+ continue;
+ }
+ const key = attribute.name
+ .substring(prefixContext.length)
+ .split("-")
+ .map((part, index) => {
+ if (index === 0) {
+ return part;
+ }
+ return (0, StringUtil_1.ucfirst)(part);
+ })
+ .join("");
+ context[key] = attribute.value;
+ }
+ return context;
+ }
async function getSha256Hash(data) {
const buffer = await window.crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(buffer))
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
+use Masterminds\HTML5\Parser\EventHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use wcf\data\file\temporary\FileTemporary;
use wcf\data\file\temporary\FileTemporaryAction;
use wcf\http\Helper;
+use wcf\system\event\EventHandler as EventEventHandler;
+use wcf\system\file\processor\event\FileProcessorCollecting;
+use wcf\system\file\processor\FileProcessor;
use wcf\system\request\LinkHandler;
+use wcf\util\JSON;
final class FileUploadPreflightAction implements RequestHandlerInterface
{
filename: non-empty-string,
fileSize: positive-int,
fileHash: non-empty-string,
+ typeName: non-empty-string,
+ context: array<non-empty-string, string>,
}
EOT,
);
+ $fileProcessor = FileProcessor::getInstance()->forTypeName($parameters['typeName']);
+ if ($fileProcessor === null) {
+ // 400 Bad Request
+ return new JsonResponse([
+ 'typeName' => 'unknown',
+ ], 400);
+ }
+
+ if (!$fileProcessor->acceptUpload($parameters['filename'], $parameters['fileSize'], $parameters['context'])) {
+ // 403 Permission Denied
+ return new EmptyResponse(403);
+ }
+
$numberOfChunks = FileTemporary::getNumberOfChunks($parameters['fileSize']);
if ($numberOfChunks > FileTemporary::MAX_CHUNK_COUNT) {
// 413 Content Too Large
'filename' => $parameters['filename'],
'fileSize' => $parameters['fileSize'],
'fileHash' => $parameters['fileHash'],
+ 'typeName' => $parameters['typeName'],
+ 'context' => JSON::encode($parameters['context']),
'chunks' => \str_repeat('0', $numberOfChunks),
],
]);
use wcf\system\event\listener\PreloadPhrasesCollectingListener;
use wcf\system\event\listener\UserLoginCancelLostPasswordListener;
use wcf\system\event\listener\UsernameValidatingCheckCharactersListener;
+use wcf\system\file\processor\event\FileProcessorCollecting;
use wcf\system\language\event\LanguageImported;
use wcf\system\language\event\PhraseChanged;
use wcf\system\language\LanguageFactory;
$event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession);
});
+ $eventHandler->register(FileProcessorCollecting::class, static function (FileProcessorCollecting $event) {
+ $event->register(new \wcf\system\file\processor\AttachmentFileProcessor());
+ });
+
try {
$licenseApi = new LicenseApi();
$licenseData = $licenseApi->readFromFile();
* @property-read string $filename
* @property-read int $fileSize
* @property-read string $fileHash
+ * @property-read string $typeName
*/
class File extends DatabaseObject
{
'filename' => $fileTemporary->filename,
'fileSize' => $fileTemporary->fileSize,
'fileHash' => $fileTemporary->fileHash,
+ 'typeName' => $fileTemporary->typeName,
]]);
$file = $fileAction->executeAction()['returnValues'];
\assert($file instanceof File);
namespace wcf\data\file\temporary;
use wcf\data\DatabaseObject;
+use wcf\util\JSON;
/**
* @author Alexander Ebert
* @property-read string $filename
* @property-read int $fileSize
* @property-read string $fileHash
+ * @property-read string $typeName
+ * @property-read string $context
* @property-read string $chunks
*/
class FileTemporary extends DatabaseObject
);
}
+ public function getContext(): array
+ {
+ return JSON::decode($this->context);
+ }
+
public static function getNumberOfChunks(int $fileSize): int
{
return \ceil($fileSize / self::getOptimalChunkSize());
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+final class AttachmentFileProcessor implements IFileProcessor
+{
+ public function getTypeName(): string
+ {
+ return 'com.woltlab.wcf.attachment';
+ }
+
+ public function acceptUpload(string $filename, int $fileSize, array $context): bool
+ {
+ return true;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor;
+
+use wcf\system\event\EventHandler;
+use wcf\system\file\processor\event\FileProcessorCollecting;
+use wcf\system\SingletonFactory;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+final class FileProcessor extends SingletonFactory
+{
+ /**
+ * @var array<string, IFileProcessor>
+ */
+ private array $processors;
+
+ #[\Override]
+ public function init(): void
+ {
+ $event = new FileProcessorCollecting();
+ EventHandler::getInstance()->fire($event);
+ $this->processors = $event->getProcessors();
+ }
+
+ public function forTypeName(string $typeName): ?IFileProcessor
+ {
+ return $this->processors[$typeName] ?? null;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+interface IFileProcessor
+{
+ public function getTypeName(): string;
+
+ public function acceptUpload(string $filename, int $fileSize, array $context): bool;
+}
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor\event;
+
+use wcf\system\event\IEvent;
+use wcf\system\file\processor\exception\DuplicateFileProcessor;
+use wcf\system\file\processor\IFileProcessor;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+final class FileProcessorCollecting implements IEvent
+{
+ /**
+ * @var array<string, IFileProcessor>
+ */
+ private array $data = [];
+
+ public function register(IFileProcessor $fileUploadProcessor): void
+ {
+ $typeName = $fileUploadProcessor->getTypeName();
+ if (isset($this->data[$typeName])) {
+ throw new DuplicateFileProcessor($typeName);
+ }
+
+ $this->data[$typeName] = $fileUploadProcessor;
+ }
+
+ /**
+ * @return array<string, IFileProcessor>
+ */
+ public function getProcessors(): array
+ {
+ return $this->data;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\system\file\processor\exception;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ */
+final class DuplicateFileProcessor extends \Exception
+{
+ public function __construct(string $typeName)
+ {
+ parent::__construct(
+ \sprintf("The file processor '%s' has already been registered", $typeName),
+ );
+ }
+}
fileID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
fileSize BIGINT NOT NULL,
- fileHash CHAR(64) NOT NULL
+ fileHash CHAR(64) NOT NULL,
+ typeName VARCHAR(255) NOT NULL
);
DROP TABLE IF EXISTS wcf1_file_temporary;
filename VARCHAR(255) NOT NULL,
fileSize BIGINT NOT NULL,
fileHash CHAR(64) NOT NULL,
+ typeName VARCHAR(255) NOT NULL,
+ context TEXT,
chunks VARBINARY(255) NOT NULL
);