Skip to content

Commit

Permalink
Merge pull request #5905 from WoltLab/upload-pipeline-v2
Browse files Browse the repository at this point in the history
Upload Pipeline v2
  • Loading branch information
dtdesign authored Jun 8, 2024
2 parents fbefc10 + 82fe21f commit e7c871d
Show file tree
Hide file tree
Showing 97 changed files with 5,266 additions and 1,321 deletions.
6 changes: 6 additions & 0 deletions com.woltlab.wcf/cronjob.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
<description language="de">Löscht verwaiste Dateianhänge</description>
<expression>0 2 * * *</expression>
</cronjob>
<cronjob name="com.woltlab.wcf.fileCleanUp">
<classname>wcf\system\cronjob\FileCleanUpCronjob</classname>
<description>Deletes orphaned files</description>
<description language="de">Löscht verwaiste Dateien</description>
<expression>0 3 * * *</expression>
</cronjob>
<cronjob name="com.woltlab.wcf.backgroundQueueCleanUp">
<classname>wcf\system\cronjob\BackgroundQueueCleanUpCronjob</classname>
<description>Requeues stuck queue items</description>
Expand Down
5 changes: 5 additions & 0 deletions com.woltlab.wcf/objectType.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,11 @@
<name>com.woltlab.wcf.rescueMode</name>
<definitionname>com.woltlab.wcf.floodControl</definitionname>
</type>
<type>
<name>com.woltlab.wcf.attachment</name>
<definitionname>com.woltlab.wcf.file</definitionname>
<classname>wcf\system\file\processor\AttachmentFileProcessor</classname>
</type>
<!-- deprecated -->
<type>
<name>com.woltlab.wcf.page.controller</name>
Expand Down
4 changes: 4 additions & 0 deletions com.woltlab.wcf/objectTypeDefinition.xml
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,9 @@
<name>com.woltlab.wcf.multifactor</name>
<interfacename>wcf\system\user\multifactor\IMultifactorMethod</interfacename>
</definition>
<definition>
<name>com.woltlab.wcf.file</name>
<interfacename>wcf\system\file\processor\IFileProcessor</interfacename>
</definition>
</import>
</data>
4 changes: 0 additions & 4 deletions com.woltlab.wcf/templates/attachments.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@
{icon name='up-right-and-down-left-from-center'}
{#$attachment->width} × {#$attachment->height}
</li>
<li>
{icon name='eye'}
{#$attachment->downloads}
</li>
</ul>
</a>
</li>
Expand Down
1 change: 0 additions & 1 deletion com.woltlab.wcf/templates/headIncludeJavaScript.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ window.addEventListener('pageshow', function(event) {
);
</script>

{js application='wcf' file='WCF.Attachment' bundle='WCF.Combined' hasTiny=true}
{js application='wcf' file='WCF.ColorPicker' bundle='WCF.Combined' hasTiny=true}
{js application='wcf' file='WCF.ImageViewer' bundle='WCF.Combined' hasTiny=true}
{js application='wcf' file='WCF.Label' bundle='WCF.Combined' hasTiny=true}
Expand Down
90 changes: 19 additions & 71 deletions com.woltlab.wcf/templates/shared_messageFormAttachments.tpl
Original file line number Diff line number Diff line change
@@ -1,81 +1,29 @@
<div class="jsOnly formAttachmentContent messageTabMenuContent" id="attachments_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}">
<ul class="formAttachmentList clearfix jsObjectActionContainer" data-object-action-class-name="wcf\data\attachment\AttachmentAction"{if !$attachmentHandler->getAttachmentList()|count} style="display: none"{/if}>
{foreach from=$attachmentHandler->getAttachmentList() item=$attachment}
<li class="box64 formAttachmentListItem jsObjectActionObject" data-object-id="{@$attachment->getObjectID()}" data-height="{@$attachment->height}" data-width="{@$attachment->width}" data-is-image="{@$attachment->isImage}">
{if $attachment->tinyThumbnailType}
<img src="{$attachment->getThumbnailLink('tiny')}" alt="" class="attachmentTinyThumbnail">
{else}
{icon size=64 name=$attachment->getIconName()}
{/if}

<div>
<div>
<p><a href="{$attachment->getLink()}" target="_blank"{if $attachment->isImage} title="{$attachment->filename}" class="jsImageViewer"{/if}>{$attachment->filename}</a></p>
<small>{@$attachment->filesize|filesize}</small>
</div>

<ul class="buttonGroup">
<li><button type="button" class="button small jsObjectAction" data-object-action="delete" data-confirm-message="{lang}wcf.attachment.delete.sure{/lang}">{lang}wcf.global.button.delete{/lang}</button></li>
{if $attachment->isImage}
{if $attachment->thumbnailType}<li><button type="button" class="button small jsButtonAttachmentInsertThumbnail" data-object-id="{@$attachment->attachmentID}" data-url="{$attachment->getThumbnailLink('thumbnail')}">{lang}wcf.attachment.insertThumbnail{/lang}</button></li>{/if}
<li><button type="button" class="button small jsButtonAttachmentInsertFull" data-object-id="{@$attachment->attachmentID}" data-url="{$attachment->getLink()}">{lang}wcf.attachment.insertFull{/lang}</button></li>
{else}
<li><button type="button" class="button small jsButtonInsertAttachment" data-object-id="{@$attachment->attachmentID}">{lang}wcf.attachment.insert{/lang}</button></li>
{/if}
</ul>
</div>
</li>
<div class="messageTabMenuContent" id="attachments_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}">
{unsafe:$attachmentHandler->getHtmlElement()}

<div class="attachment__list__existingFiles">
{foreach from=$attachmentHandler->getAttachmentList() item=attachment}
{unsafe:$attachment->toHtmlElement()}
{/foreach}
</ul>
</div>

<dl class="wide">
<dt></dt>
<dd>
<div data-max-size="{@$attachmentHandler->getMaxSize()}"></div>
<div data-max-size="{$attachmentHandler->getMaxSize()}"></div>
<small>{lang}wcf.attachment.upload.limits{/lang}</small>
</dd>
</dl>

<script data-relocate="true">
{jsphrase name='wcf.attachment.insert'}
{jsphrase name='wcf.attachment.insertFull'}
{jsphrase name='wcf.attachment.moreOptions'}
require(["WoltLabSuite/Core/Component/Attachment/List"], ({ setup }) => {
setup("{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}");
});
</script>

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

<script data-relocate="true">
$(function() {
WCF.Language.addObject({
'wcf.attachment.upload.error.invalidExtension': '{jslang}wcf.attachment.upload.error.invalidExtension{/jslang}',
'wcf.attachment.upload.error.tooLarge': '{jslang}wcf.attachment.upload.error.tooLarge{/jslang}',
'wcf.attachment.upload.error.reachedLimit': '{jslang}wcf.attachment.upload.error.reachedLimit{/jslang}',
'wcf.attachment.upload.error.reachedRemainingLimit': '{jslang}wcf.attachment.upload.error.reachedRemainingLimit{/jslang}',
'wcf.attachment.upload.error.uploadFailed': '{jslang}wcf.attachment.upload.error.uploadFailed{/jslang}',
'wcf.attachment.upload.error.http413': '{jslang}wcf.attachment.upload.error.http413{/jslang}',
'wcf.attachment.upload.error.uploadPhpLimit': '{jslang}wcf.attachment.upload.error.uploadPhpLimit{/jslang}',
'wcf.attachment.insert': '{jslang}wcf.attachment.insert{/jslang}',
'wcf.attachment.insertAll': '{jslang}wcf.attachment.insertAll{/jslang}',
'wcf.attachment.insertFull': '{jslang}wcf.attachment.insertFull{/jslang}',
'wcf.attachment.insertThumbnail': '{jslang}wcf.attachment.insertThumbnail{/jslang}',
'wcf.attachment.delete.sure': '{jslang}wcf.attachment.delete.sure{/jslang}'
});
new WCF.Attachment.Upload(
$('#attachments_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if} > dl > dd > div'),
$('#attachments_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if} > ul'),
'{@$attachmentObjectType}',
'{@$attachmentObjectID}',
'{$tmpHash|encodeJS}',
'{@$attachmentParentObjectID}',
{@$attachmentHandler->getMaxCount()},
'{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}',
{
autoScale: {
enable: {if ATTACHMENT_IMAGE_AUTOSCALE}true{else}false{/if},
maxWidth: {ATTACHMENT_IMAGE_AUTOSCALE_MAX_WIDTH},
maxHeight: {ATTACHMENT_IMAGE_AUTOSCALE_MAX_HEIGHT},
fileType: '{ATTACHMENT_IMAGE_AUTOSCALE_FILE_TYPE}',
quality: {ATTACHMENT_IMAGE_AUTOSCALE_QUALITY / 100}
}
}
);
});
</script>

<input type="hidden" name="tmpHash" value="{$tmpHash}">
95 changes: 25 additions & 70 deletions com.woltlab.wcf/templates/shared_wysiwygAttachmentFormField.tpl
Original file line number Diff line number Diff line change
@@ -1,72 +1,27 @@
<ul id="{$field->getPrefixedID()}_attachmentList" {*
*}class="formAttachmentList jsObjectActionContainer" {*
*}data-object-action-class-name="wcf\data\attachment\AttachmentAction"{*
*}{if !$field->getAttachmentHandler()->getAttachmentList()|count} style="display: none"{/if}{*
*}>
{foreach from=$field->getAttachmentHandler()->getAttachmentList() item=$attachment}
<li class="box64 jsObjectActionObject" {*
*}data-object-id="{@$attachment->getObjectID()}" {*
*}data-height="{@$attachment->height}" {*
*}data-width="{@$attachment->width}" {*
*}data-is-image="{@$attachment->isImage}"{*
*}>
{if $attachment->tinyThumbnailType}
<img src="{$attachment->getThumbnailLink('tiny')}" alt="" class="attachmentTinyThumbnail">
{else}
{icon size=64 name=$attachment->getIconName()}
{/if}

<div>
<div>
<p><a href="{$attachment->getLink()}" target="_blank"{if $attachment->isImage} title="{$attachment->filename}" class="jsImageViewer"{/if}>{$attachment->filename}</a></p>
<small>{@$attachment->filesize|filesize}</small>
</div>

<ul class="buttonGroup">
<li><button type="button" class="button small jsObjectAction" data-object-action="delete" data-confirm-message="{lang}wcf.attachment.delete.sure{/lang}">{lang}wcf.global.button.delete{/lang}</button></li>
{if $attachment->isImage}
{if $attachment->thumbnailType}
<li><button type="button" class="button small jsButtonAttachmentInsertThumbnail" data-object-id="{@$attachment->attachmentID}" data-url="{$attachment->getThumbnailLink('thumbnail')}">{lang}wcf.attachment.insertThumbnail{/lang}</button></li>
{/if}
<li><button type="button" class="button small jsButtonAttachmentInsertFull" data-object-id="{@$attachment->attachmentID}" data-url="{$attachment->getLink()}">{lang}wcf.attachment.insertFull{/lang}</button></li>
{else}
<li><button type="button" class="button small jsButtonInsertAttachment" data-object-id="{@$attachment->attachmentID}">{lang}wcf.attachment.insert{/lang}</button></li>
{/if}
</ul>
</div>
</li>
{/foreach}
</ul>
<div id="{$field->getPrefixedID()}_uploadButton" class="formAttachmentButtons" data-max-size="{@$field->getAttachmentHandler()->getMaxSize()}"></div>
<div class="messageTabMenuContent" id="attachments_{$field->getPrefixedWysiwygId()}">
{unsafe:$field->getAttachmentHandler()->getHtmlElement()}

<script data-relocate="true">
$(function() {
WCF.Language.addObject({
'wcf.attachment.upload.error.invalidExtension': '{jslang}wcf.attachment.upload.error.invalidExtension{/jslang}',
'wcf.attachment.upload.error.tooLarge': '{jslang}wcf.attachment.upload.error.tooLarge{/jslang}',
'wcf.attachment.upload.error.reachedLimit': '{jslang}wcf.attachment.upload.error.reachedLimit{/jslang}',
'wcf.attachment.upload.error.reachedRemainingLimit': '{jslang}wcf.attachment.upload.error.reachedRemainingLimit{/jslang}',
'wcf.attachment.upload.error.uploadFailed': '{jslang}wcf.attachment.upload.error.uploadFailed{/jslang}',
'wcf.attachment.upload.error.http413': '{jslang}wcf.attachment.upload.error.http413{/jslang}',
'wcf.attachment.upload.error.uploadPhpLimit': '{jslang}wcf.attachment.upload.error.uploadPhpLimit{/jslang}',
'wcf.attachment.insert': '{jslang}wcf.attachment.insert{/jslang}',
'wcf.attachment.insertAll': '{jslang}wcf.attachment.insertAll{/jslang}',
'wcf.attachment.insertFull': '{jslang}wcf.attachment.insertFull{/jslang}',
'wcf.attachment.insertThumbnail': '{jslang}wcf.attachment.insertThumbnail{/jslang}',
'wcf.attachment.delete.sure': '{jslang}wcf.attachment.delete.sure{/jslang}'
});
new WCF.Attachment.Upload(
$('#{@$field->getPrefixedID()|encodeJS}_uploadButton'),
$('#{@$field->getPrefixedID()|encodeJS}_attachmentList'),
'{@$field->getAttachmentHandler()->getObjectType()->objectType}',
'{@$field->getAttachmentHandler()->getObjectID()}',
'{$field->getAttachmentHandler()->getTmpHashes()[0]|encodeJS}',
'{@$field->getAttachmentHandler()->getParentObjectID()}',
{@$field->getAttachmentHandler()->getMaxCount()},
'{@$field->getPrefixedWysiwygId()}'
);
});
</script>
<div class="attachment__list__existingFiles">
{foreach from=$field->getAttachmentHandler()->getAttachmentList() item=attachment}
{unsafe:$attachment->toHtmlElement()}
{/foreach}
</div>

<dl class="wide">
<dt></dt>
<dd>
<div data-max-size="{$field->getAttachmentHandler()->getMaxSize()}"></div>
<small>{lang}wcf.attachment.upload.limits{/lang}</small>
</dd>
</dl>

<input type="hidden" id="{$field->getPrefixedID()}_tmpHash" name="{$field->getPrefixedID()}_tmpHash" value="{$field->getAttachmentHandler()->getTmpHashes()[0]}">
<script data-relocate="true">
{jsphrase name='wcf.attachment.insert'}
{jsphrase name='wcf.attachment.insertFull'}
{jsphrase name='wcf.attachment.moreOptions'}
require(["WoltLabSuite/Core/Component/Attachment/List"], ({ setup }) => {
setup("{$field->getPrefixedWysiwygId()}");
});
</script>
</div>
2 changes: 1 addition & 1 deletion com.woltlab.wcf/userGroupOption.xml
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@
<!-- /admin.management -->
<option name="user.attachment.maxSize">
<categoryname>user.message.attachment</categoryname>
<optiontype>fileSize</optiontype>
<optiontype>uploadLimit</optiontype>
<defaultvalue>2000000</defaultvalue>
<minvalue>10000</minvalue>
</option>
Expand Down
25 changes: 19 additions & 6 deletions ts/WoltLabSuite/Core/Ajax/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const enum RequestType {
POST,
}

type Payload = FormData | Record<string, unknown>;
type Payload = Blob | FormData | Record<string, unknown>;

class SetupRequest {
private readonly url: string;
Expand All @@ -50,6 +50,7 @@ let ignoreConnectionErrors = false;
window.addEventListener("beforeunload", () => (ignoreConnectionErrors = true));

class BackendRequest {
readonly #headers = new Map<string, string>();
readonly #url: string;
readonly #type: RequestType;
readonly #payload?: Payload;
Expand Down Expand Up @@ -77,6 +78,12 @@ class BackendRequest {
return this;
}

withHeader(key: string, value: string): this {
this.#headers.set(key, value);

return this;
}

protected allowCaching(): this {
this.#allowCaching = true;

Expand Down Expand Up @@ -117,12 +124,13 @@ class BackendRequest {
async #fetch(requestOptions: RequestInit = {}): Promise<Response | undefined> {
registerGlobalRejectionHandler();

this.#headers.set("X-Requested-With", "XMLHttpRequest");
this.#headers.set("X-XSRF-TOKEN", getXsrfToken());
const headers = Object.fromEntries(this.#headers);

const init: RequestInit = extend(
{
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-XSRF-TOKEN": getXsrfToken(),
},
headers,
mode: "same-origin",
credentials: "same-origin",
cache: this.#allowCaching ? "default" : "no-store",
Expand All @@ -135,14 +143,19 @@ class BackendRequest {
init.method = "POST";

if (this.#payload) {
if (this.#payload instanceof FormData) {
if (this.#payload instanceof Blob) {
init.headers!["Content-Type"] = "application/octet-stream";
init.body = this.#payload;
} else if (this.#payload instanceof FormData) {
init.headers!["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
init.body = this.#payload;
} else {
init.headers!["Content-Type"] = "application/json; charset=UTF-8";
init.body = JSON.stringify(this.#payload);
}
}
} else if (this.#type === RequestType.DELETE) {
init.method = "DELETE";
} else {
init.method = "GET";
}
Expand Down
2 changes: 1 addition & 1 deletion ts/WoltLabSuite/Core/Api/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class ApiError {
}
}

class ValidationError {
export class ValidationError {
constructor(
public readonly code: string,
public readonly message: string,
Expand Down
38 changes: 38 additions & 0 deletions ts/WoltLabSuite/Core/Api/Files/Chunk/Chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "../../Result";

export type ResponseIncomplete = {
completed: false;
};
export type ResponseCompleted = {
completed: true;
generateThumbnails: boolean;
fileID: number;
objectTypeID: number | null;
mimeType: string;
link: string;
data: Record<string, unknown>;
};

export type Response = ResponseIncomplete | ResponseCompleted;

export async function uploadChunk(
identifier: string,
sequenceNo: number,
checksum: string,
payload: Blob,
): Promise<ApiResult<Response>> {
const url = new URL(`${window.WSC_API_URL}index.php?api/rpc/core/files/upload/${identifier}/chunk/${sequenceNo}`);

let response: Response;
try {
response = (await prepareRequest(url)
.post(payload)
.withHeader("chunk-checksum-sha256", checksum)
.fetchAsJson()) as Response;
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue(response);
}
12 changes: 12 additions & 0 deletions ts/WoltLabSuite/Core/Api/Files/DeleteFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";

export async function deleteFile(fileId: number): Promise<ApiResult<[]>> {
try {
await prepareRequest(`${window.WSC_API_URL}index.php?api/rpc/core/files/${fileId}`).delete().fetchAsJson();
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue([]);
}
Loading

0 comments on commit e7c871d

Please sign in to comment.