From: Matthias Schmidt Date: Sat, 27 Jun 2020 06:44:56 +0000 (+0200) Subject: Support replacing existing media files X-Git-Tag: 5.3.0_Alpha_1~151^2~3 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=c2e9de94d281045f885a4d1f8873eff7c452bee8;p=GitHub%2FWoltLab%2FWCF.git Support replacing existing media files See #2922 --- diff --git a/com.woltlab.wcf/templates/mediaEditor.tpl b/com.woltlab.wcf/templates/mediaEditor.tpl index 9fecb7586c..e25e443cbd 100644 --- a/com.woltlab.wcf/templates/mediaEditor.tpl +++ b/com.woltlab.wcf/templates/mediaEditor.tpl @@ -1,3 +1,7 @@ + + {if $media->isImage && $media->hasThumbnail('small')}
{@$media->getThumbnailTag('small')} diff --git a/com.woltlab.wcf/templates/mediaJavaScript.tpl b/com.woltlab.wcf/templates/mediaJavaScript.tpl index b8c5d5fed7..609991a1bf 100644 --- a/com.woltlab.wcf/templates/mediaJavaScript.tpl +++ b/com.woltlab.wcf/templates/mediaJavaScript.tpl @@ -2,8 +2,10 @@ require(['Language', 'Permission'], function(Language, Permission) { Language.addObject({ 'wcf.global.button.insert': '{lang}wcf.global.button.insert{/lang}', - 'wcf.media.button.select': '{lang}wcf.media.button.select{/lang}', + 'wcf.media.button.replaceFile': '{lang}wcf.media.button.replaceFile{/lang}', + 'wcf.media.button.select': '{lang}wcf.media.button.select{/lang}', 'wcf.media.delete.confirmMessage': '{lang __encode=true __literal=true}wcf.media.delete.confirmMessage{/lang}', + 'wcf.media.imageDimensions.value': '{lang __literal=true}wcf.media.imageDimensions.value{/lang}', 'wcf.media.insert': '{lang}wcf.media.insert{/lang}', 'wcf.media.insert.imageSize': '{lang}wcf.media.insert.imageSize{/lang}', 'wcf.media.insert.imageSize.small': '{lang}wcf.media.insert.imageSize.small{/lang}', @@ -15,6 +17,8 @@ 'wcf.media.button.insert': '{lang}wcf.media.button.insert{/lang}', 'wcf.media.search.info.searchStringThreshold': '{lang __literal=true}wcf.media.search.info.searchStringThreshold{/lang}', 'wcf.media.search.noResults': '{lang}wcf.media.search.noResults{/lang}', + 'wcf.media.upload.error.differentFileExtension': '{lang}wcf.media.upload.error.differentFileExtension{/lang}', + 'wcf.media.upload.error.differentFileType': '{lang}wcf.media.upload.error.differentFileType{/lang}', 'wcf.media.upload.error.noImage': '{lang}wcf.media.upload.error.noImage{/lang}', 'wcf.media.upload.error.uploadFailed': '{lang}wcf.media.upload.error.uploadFailed{/lang}', 'wcf.media.upload.success': '{lang}wcf.media.upload.success{/lang}', diff --git a/wcfsetup/install/files/acp/templates/mediaEditor.tpl b/wcfsetup/install/files/acp/templates/mediaEditor.tpl index 9fecb7586c..e25e443cbd 100644 --- a/wcfsetup/install/files/acp/templates/mediaEditor.tpl +++ b/wcfsetup/install/files/acp/templates/mediaEditor.tpl @@ -1,3 +1,7 @@ + + {if $media->isImage && $media->hasThumbnail('small')}
{@$media->getThumbnailTag('small')} diff --git a/wcfsetup/install/files/acp/templates/mediaJavaScript.tpl b/wcfsetup/install/files/acp/templates/mediaJavaScript.tpl index bfa7c9750b..609991a1bf 100644 --- a/wcfsetup/install/files/acp/templates/mediaJavaScript.tpl +++ b/wcfsetup/install/files/acp/templates/mediaJavaScript.tpl @@ -2,8 +2,10 @@ require(['Language', 'Permission'], function(Language, Permission) { Language.addObject({ 'wcf.global.button.insert': '{lang}wcf.global.button.insert{/lang}', + 'wcf.media.button.replaceFile': '{lang}wcf.media.button.replaceFile{/lang}', 'wcf.media.button.select': '{lang}wcf.media.button.select{/lang}', 'wcf.media.delete.confirmMessage': '{lang __encode=true __literal=true}wcf.media.delete.confirmMessage{/lang}', + 'wcf.media.imageDimensions.value': '{lang __literal=true}wcf.media.imageDimensions.value{/lang}', 'wcf.media.insert': '{lang}wcf.media.insert{/lang}', 'wcf.media.insert.imageSize': '{lang}wcf.media.insert.imageSize{/lang}', 'wcf.media.insert.imageSize.small': '{lang}wcf.media.insert.imageSize.small{/lang}', @@ -15,6 +17,8 @@ 'wcf.media.button.insert': '{lang}wcf.media.button.insert{/lang}', 'wcf.media.search.info.searchStringThreshold': '{lang __literal=true}wcf.media.search.info.searchStringThreshold{/lang}', 'wcf.media.search.noResults': '{lang}wcf.media.search.noResults{/lang}', + 'wcf.media.upload.error.differentFileExtension': '{lang}wcf.media.upload.error.differentFileExtension{/lang}', + 'wcf.media.upload.error.differentFileType': '{lang}wcf.media.upload.error.differentFileType{/lang}', 'wcf.media.upload.error.noImage': '{lang}wcf.media.upload.error.noImage{/lang}', 'wcf.media.upload.error.uploadFailed': '{lang}wcf.media.upload.error.uploadFailed{/lang}', 'wcf.media.upload.success': '{lang}wcf.media.upload.success{/lang}', diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js index 9f5b8072dd..c430f1b069 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js @@ -8,14 +8,34 @@ */ define( [ - 'Ajax', 'Core', 'Dictionary', 'Dom/ChangeListener', - 'Dom/Traverse', 'Language', 'Ui/Dialog', 'Ui/Notification', - 'WoltLabSuite/Core/Language/Chooser', 'WoltLabSuite/Core/Language/Input', 'EventKey' + 'Ajax', + 'Core', + 'Dictionary', + 'Dom/ChangeListener', + 'Dom/Traverse', + 'Dom/Util', + 'Language', + 'Ui/Dialog', + 'Ui/Notification', + 'WoltLabSuite/Core/Language/Chooser', + 'WoltLabSuite/Core/Language/Input', + 'EventKey', + 'WoltLabSuite/Core/Media/Replace' ], function( - Ajax, Core, Dictionary, DomChangeListener, - DomTraverse, Language, UiDialog, UiNotification, - LanguageChooser, LanguageInput, EventKey + Ajax, + Core, + Dictionary, + DomChangeListener, + DomTraverse, + DomUtil, + Language, + UiDialog, + UiNotification, + LanguageChooser, + LanguageInput, + EventKey, + MediaReplace ) { "use strict"; @@ -98,6 +118,90 @@ define( } }, + /** + * Initializes the editor dialog. + * + * @param {HTMLElement} content + * @param {object} data + * @since 5.3 + */ + _initEditor: function(content, data) { + this._availableLanguageCount = ~~data.returnValues.availableLanguageCount; + this._categoryIds = data.returnValues.categoryIDs.map(function(number) { + return ~~number; + }); + + var didLoadMediaData = false; + if (data.returnValues.mediaData) { + this._media = data.returnValues.mediaData; + + didLoadMediaData = true; + } + + // make sure that the language chooser is initialized first + setTimeout(function() { + if (this._availableLanguageCount > 1) { + LanguageChooser.setLanguageId('mediaEditor_' + this._media.mediaID + '_languageID', this._media.languageID || LANGUAGE_ID); + } + + if (this._categoryIds.length) { + elBySel('select[name=categoryID]', content).value = ~~this._media.categoryID; + } + + var title = elBySel('input[name=title]', content); + var altText = elBySel('input[name=altText]', content); + var caption = elBySel('textarea[name=caption]', content); + + if (this._availableLanguageCount > 1 && this._media.isMultilingual) { + if (elById('altText_' + this._media.mediaID)) LanguageInput.setValues('altText_' + this._media.mediaID, Dictionary.fromObject(this._media.altText || { })); + if (elById('caption_' + this._media.mediaID)) LanguageInput.setValues('caption_' + this._media.mediaID, Dictionary.fromObject(this._media.caption || { })); + LanguageInput.setValues('title_' + this._media.mediaID, Dictionary.fromObject(this._media.title || { })); + } + else { + title.value = this._media.title ? this._media.title[this._media.languageID || LANGUAGE_ID] : ''; + if (altText) altText.value = this._media.altText ? this._media.altText[this._media.languageID || LANGUAGE_ID] : ''; + if (caption) caption.value = this._media.caption ? this._media.caption[this._media.languageID || LANGUAGE_ID] : ''; + } + + if (this._availableLanguageCount > 1) { + var isMultilingual = elBySel('input[name=isMultilingual]', content); + isMultilingual.addEventListener('change', this._updateLanguageFields.bind(this)); + + this._updateLanguageFields(null, isMultilingual); + } + + var keyPress = this._keyPress.bind(this); + if (altText) altText.addEventListener('keypress', keyPress); + title.addEventListener('keypress', keyPress); + + elBySel('button[data-type=submit]', content).addEventListener(WCF_CLICK_EVENT, this._saveData.bind(this)); + + // remove focus from input elements and scroll dialog to top + document.activeElement.blur(); + elById('mediaEditor_' + this._media.mediaID).parentNode.scrollTop = 0; + + // Initialize button to replace media file. + var uploadButton = elByClass('mediaManagerMediaReplaceButton', content)[0]; + var target = elByClass('mediaThumbnail', content)[0]; + if (!target) { + target = elCreate('div'); + content.appendChild(target); + } + new MediaReplace( + this._media.mediaID, + DomUtil.identify(uploadButton), + // Pass an anonymous element for non-images which is required internally + // but not needed in this case. + DomUtil.identify(target), + { + mediaEditor: this + } + ); + + DomChangeListener.trigger(); + }.bind(this), 200); + }, + /** * Handles the `[ENTER]` key to submit the form. * @@ -288,64 +392,7 @@ define( title: Language.get('wcf.media.edit') }, source: { - after: (function(content, data) { - this._availableLanguageCount = ~~data.returnValues.availableLanguageCount; - this._categoryIds = data.returnValues.categoryIDs.map(function(number) { - return ~~number; - }); - - var didLoadMediaData = false; - if (data.returnValues.mediaData) { - this._media = data.returnValues.mediaData; - - didLoadMediaData = true; - } - - // make sure that the language chooser is initialized first - setTimeout(function() { - if (this._availableLanguageCount > 1) { - LanguageChooser.setLanguageId('mediaEditor_' + this._media.mediaID + '_languageID', this._media.languageID || LANGUAGE_ID); - } - - if (this._categoryIds.length) { - elBySel('select[name=categoryID]', content).value = ~~this._media.categoryID; - } - - var title = elBySel('input[name=title]', content); - var altText = elBySel('input[name=altText]', content); - var caption = elBySel('textarea[name=caption]', content); - - if (this._availableLanguageCount > 1 && this._media.isMultilingual) { - if (elById('altText_' + this._media.mediaID)) LanguageInput.setValues('altText_' + this._media.mediaID, Dictionary.fromObject(this._media.altText || { })); - if (elById('caption_' + this._media.mediaID)) LanguageInput.setValues('caption_' + this._media.mediaID, Dictionary.fromObject(this._media.caption || { })); - LanguageInput.setValues('title_' + this._media.mediaID, Dictionary.fromObject(this._media.title || { })); - } - else { - title.value = this._media.title ? this._media.title[this._media.languageID || LANGUAGE_ID] : ''; - if (altText) altText.value = this._media.altText ? this._media.altText[this._media.languageID || LANGUAGE_ID] : ''; - if (caption) caption.value = this._media.caption ? this._media.caption[this._media.languageID || LANGUAGE_ID] : ''; - } - - if (this._availableLanguageCount > 1) { - var isMultilingual = elBySel('input[name=isMultilingual]', content); - isMultilingual.addEventListener('change', this._updateLanguageFields.bind(this)); - - this._updateLanguageFields(null, isMultilingual); - } - - var keyPress = this._keyPress.bind(this); - if (altText) altText.addEventListener('keypress', keyPress); - title.addEventListener('keypress', keyPress); - - elBySel('button[data-type=submit]', content).addEventListener(WCF_CLICK_EVENT, this._saveData.bind(this)); - - // remove focus from input elements and scroll dialog to top - document.activeElement.blur(); - elById('mediaEditor_' + this._media.mediaID).parentNode.scrollTop = 0; - - DomChangeListener.trigger(); - }.bind(this), 200); - }).bind(this), + after: this._initEditor.bind(this), data: { actionName: 'getEditorDialog', className: 'wcf\\data\\media\\MediaAction', @@ -358,6 +405,18 @@ define( } UiDialog.open(this._dialogs.get('mediaEditor_' + media.mediaID)); + }, + + /** + * Updates the data of the currently edited media file. + * + * @param {object} data + * @since 5.3 + */ + updateData: function(data) { + if (this._callbackObject._editorSuccess) { + this._callbackObject._editorSuccess(data); + } } }; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js index 5f811a6ff6..f51656121b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js @@ -288,10 +288,28 @@ define( var listItem = this._listItems.get(~~media.mediaID); var p = elByClass('mediaTitle', listItem)[0]; if (media.isMultilingual) { - p.textContent = media.title[LANGUAGE_ID] || media.filename; + if (media.title && media.title[LANGUAGE_ID]) { + p.textContent = media.title[LANGUAGE_ID]; + } + else { + p.textContent = media.filename; + } } else { - p.textContent = media.title[media.languageID] || media.filename; + if (media.title && media.title[media.languageID]) { + p.textContent = media.title[media.languageID]; + } + else { + p.textContent = media.filename; + } + } + + var thumbnail = elByClass('mediaThumbnail', listItem)[0]; + thumbnail.innerHTML = media.elementTag; + // Bust browser cache by adding additional parameter. + var imgs = elByTag('img', thumbnail); + if (imgs.length) { + imgs[0].src += '&refresh=1'; } }, diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js new file mode 100644 index 0000000000..c77c8c152d --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js @@ -0,0 +1,138 @@ +/** + * Uploads replacemnts for media files. + * + * @author Matthias Schmidt + * @copyright 2001-2020 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Media/Upload + * @since 5.3 + */ +define( + [ + 'Core', + 'Dom/ChangeListener', + 'Dom/Util', + 'Language', + 'Ui/Notification', + './Upload' + ], + function( + Core, + DomChangeListener, + DomUtil, + Language, + UiNotification, + MediaUpload + ) + { + "use strict"; + + if (!COMPILER_TARGET_DEFAULT) { + var Fake = function() {}; + Fake.prototype = { + _createButton: function() {}, + _success: function() {}, + _upload: function() {}, + _createFileElement: function() {}, + _getParameters: function() {}, + _uploadFiles: function() {}, + _createFileElements: function() {}, + _failure: function() {}, + _insertButton: function() {}, + _progress: function() {}, + _removeButton: function() {} + }; + return Fake; + } + + /** + * @constructor + */ + function MediaReplace(mediaID, buttonContainerId, targetId, options) { + this._mediaID = mediaID; + + MediaUpload.call(this, buttonContainerId, targetId, Core.extend(options, { + action: 'replaceFile' + })); + } + Core.inherit(MediaReplace, MediaUpload, { + /** + * @see WoltLabSuite/Core/Upload#_createButton + */ + _createButton: function() { + MediaUpload.prototype._createButton.call(this); + + this._button.classList.add('small'); + + var span = elBySel('span', this._button); + span.textContent = Language.get('wcf.media.button.replaceFile'); + }, + + /** + * @see WoltLabSuite/Core/Upload#_createFileElement + */ + _createFileElement: function() { + return this._target; + }, + + /** + * @see WoltLabSuite/Core/Upload#_getFormData + */ + _getFormData: function() { + return { + objectIDs: [this._mediaID] + }; + }, + + /** + * @see WoltLabSuite/Core/Upload#_success + */ + _success: function(uploadId, data) { + var files = this._fileElements[uploadId]; + + for (var i = 0, length = files.length; i < length; i++) { + var file = files[i]; + var internalFileId = elData(file, 'internal-file-id'); + var media = data.returnValues.media[internalFileId]; + + if (media) { + if (media.isImage) { + this._target.innerHTML = media.smallThumbnailTag; + } + + elById('mediaFilename').textContent = media.filename; + elById('mediaFilesize').textContent = media.formattedFilesize; + if (media.isImage) { + elById('mediaImageDimensions').textContent = media.imageDimensions; + } + elById('mediaUploader').innerHTML = media.userLinkElement; + + this._options.mediaEditor.updateData(media); + + // Remove existing error messages. + elInnerError(this._buttonContainer, ''); + + UiNotification.show(); + } + else { + var error = data.returnValues.errors[internalFileId]; + if (!error) { + error = { + errorType: 'uploadFailed', + filename: elData(file, 'filename') + }; + } + + elInnerError(this._buttonContainer, Language.get('wcf.media.upload.error.' + error.errorType, { + filename: error.filename + })); + } + + DomChangeListener.trigger(); + } + }, + }); + + return MediaReplace; + } +); diff --git a/wcfsetup/install/files/lib/data/media/Media.class.php b/wcfsetup/install/files/lib/data/media/Media.class.php index 65a888a260..3a4b32ea01 100644 --- a/wcfsetup/install/files/lib/data/media/Media.class.php +++ b/wcfsetup/install/files/lib/data/media/Media.class.php @@ -24,6 +24,7 @@ use wcf\system\WCF; * @property-read string $fileType type of the physical media file * @property-read string $fileHash hash of the physical media file * @property-read integer $uploadTime timestamp at which the media file has been uploaded + * @property-read int $fileUpdateTime timestamp at which the media file was updated the last or `0` if it has not been updated * @property-read integer|null $userID id of the user who uploaded the media file or null if the user does not exist anymore * @property-read string $username name of the user who uploaded the media file * @property-read integer|null $languageID id of the language associated with the media file or null if the media file is multilingual or if the language has been deleted diff --git a/wcfsetup/install/files/lib/data/media/MediaAction.class.php b/wcfsetup/install/files/lib/data/media/MediaAction.class.php index ff71759694..42d5a5f6fe 100644 --- a/wcfsetup/install/files/lib/data/media/MediaAction.class.php +++ b/wcfsetup/install/files/lib/data/media/MediaAction.class.php @@ -15,6 +15,7 @@ use wcf\system\language\I18nHandler; use wcf\system\language\LanguageFactory; use wcf\system\request\LinkHandler; use wcf\system\upload\DefaultUploadFileSaveStrategy; +use wcf\system\upload\MediaReplaceUploadFileValidationStrategy; use wcf\system\upload\MediaUploadFileValidationStrategy; use wcf\system\upload\UploadFile; use wcf\system\WCF; @@ -148,6 +149,7 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, 'captionEnableHtml' => $media->captionEnableHtml, 'categoryID' => $media->categoryID, 'elementTag' => $media instanceof ViewableMedia ? $media->getElementTag($this->parameters['elementTagSize'] ?? 144) : '', + 'elementTag48' => $media instanceof ViewableMedia ? $media->getElementTag(48) : '', 'fileHash' => $media->fileHash, 'filename' => $media->filename, 'filesize' => $media->filesize, @@ -155,6 +157,9 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, 'fileType' => $media->fileType, 'height' => $media->height, 'languageID' => $media->languageID, + 'imageDimensions' => $media->isImage ? WCF::getLanguage()->getDynamicVariable('wcf.media.imageDimensions.value', [ + 'media' => $media, + ]) : '', 'isImage' => $media->isImage, 'isMultilingual' => $media->isMultilingual, 'largeThumbnailHeight' => $media->largeThumbnailHeight, @@ -169,6 +174,7 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, 'mediumThumbnailWidth' => $media->mediumThumbnailWidth, 'smallThumbnailHeight' => $media->smallThumbnailHeight, 'smallThumbnailLink' => $media->smallThumbnailType ? $media->getThumbnailLink('small') : '', + 'smallThumbnailTag' => $media->smallThumbnailType ? $media->getThumbnailTag('small') : '', 'smallThumbnailType' => $media->smallThumbnailType, 'smallThumbnailWidth' => $media->smallThumbnailWidth, 'tinyThumbnailHeight' => $media->tinyThumbnailHeight, @@ -182,6 +188,11 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, 'id' => $media->userID, 'title' => $media->username ]) : '', + // TODO: find better solution + 'userLinkElement' => $media instanceof ViewableMedia ? WCF::getTPL()->fetchString( + WCF::getTPL()->getCompiler()->compileString('userLink','{user object=$userProfile}')['template'], + ['userProfile' => $media->getUserProfile()] + ) : '', 'username' => $media->username, 'width' => $media->width ]; @@ -675,4 +686,87 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, $this->unmarkItems(); } + + /** + * Validates the `replaceFile` action. + * + * @since 5.3 + */ + public function validateReplaceFile() { + WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']); + + $this->getSingleObject(); + + /** @noinspection PhpUndefinedMethodInspection */ + $this->parameters['__files']->validateFiles( + new MediaReplaceUploadFileValidationStrategy($this->getSingleObject()->getDecoratedObject()) + ); + } + + /** + * Replaces the actual file of a media file. + * + * @return array + * @since 5.3 + */ + public function replaceFile() { + $saveStrategy = new DefaultUploadFileSaveStrategy(self::class, [ + 'action' => 'update', + 'generateThumbnails' => true, + 'object' => $this->getSingleObject()->getDecoratedObject(), + 'rotateImages' => true, + ], [ + 'fileUpdateTime' => TIME_NOW, + 'userID' => $this->getSingleObject()->userID, + 'username' => $this->getSingleObject()->username, + ]); + + /** @noinspection PhpUndefinedMethodInspection */ + $this->parameters['__files']->saveFiles($saveStrategy); + + /** @var Media[] $mediaFiles */ + $mediaFiles = $saveStrategy->getObjects(); + + $result = [ + 'errors' => [], + 'media' => [] + ]; + + if (!empty($mediaFiles)) { + $mediaIDs = $mediaToFileID = []; + foreach ($mediaFiles as $internalFileID => $media) { + $mediaIDs[] = $media->mediaID; + $mediaToFileID[$media->mediaID] = $internalFileID; + } + + // fetch media objects from database + $mediaList = new ViewableMediaList(); + $mediaList->setObjectIDs($mediaIDs); + $mediaList->readObjects(); + + foreach ($mediaList as $media) { + $result['media'][$mediaToFileID[$media->mediaID]] = $this->getMediaData($media); + } + } + + /** @var UploadFile[] $files */ + /** @noinspection PhpUndefinedMethodInspection */ + $files = $this->parameters['__files']->getFiles(); + foreach ($files as $file) { + if ($file->getValidationErrorType()) { + $result['errors'][$file->getInternalFileID()] = [ + 'filename' => $file->getFilename(), + 'filesize' => $file->getFilesize(), + 'errorType' => $file->getValidationErrorType() + ]; + } + } + + // Delete *old* files using the non-updated local media editor object. + if (empty($result['errors'])) { + $this->getSingleObject()->deleteFiles(); + } + + return $result; + } } diff --git a/wcfsetup/install/files/lib/page/MediaPage.class.php b/wcfsetup/install/files/lib/page/MediaPage.class.php index 7d247b1bbd..2311fad04a 100644 --- a/wcfsetup/install/files/lib/page/MediaPage.class.php +++ b/wcfsetup/install/files/lib/page/MediaPage.class.php @@ -92,9 +92,9 @@ class MediaPage extends AbstractPage { 'filesize' => $filesize, 'showInline' => in_array($mimeType, self::$inlineMimeTypes), 'enableRangeSupport' => $this->thumbnail ? true : false, - 'lastModificationTime' => $this->media->uploadTime, + 'lastModificationTime' => $this->media->fileUpdateTime ?? $this->media->uploadTime, 'expirationDate' => TIME_NOW + 31536000, - 'maxAge' => 31536000 + 'maxAge' => 31536000, ]); if ($this->eTag !== null) { diff --git a/wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php b/wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php index 9bb1874936..3933798573 100644 --- a/wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php +++ b/wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php @@ -85,6 +85,8 @@ class DefaultUploadFileSaveStrategy implements IUploadFileSaveStrategy { if (is_subclass_of($baseClass, IThumbnailFile::class)) { $this->options['thumbnailSizes'] = call_user_func([$baseClass, 'getThumbnailSizes']); } + + $this->options['action'] = $this->options['action'] ?? 'create'; } /** @@ -105,10 +107,13 @@ class DefaultUploadFileSaveStrategy implements IUploadFileSaveStrategy { 'filesize' => $uploadFile->getFilesize(), 'fileType' => $uploadFile->getMimeType(), 'fileHash' => sha1_file($uploadFile->getLocation()), - 'uploadTime' => TIME_NOW, - 'userID' => WCF::getUser()->userID ?: null + 'userID' => WCF::getUser()->userID ?: null, ], $this->data); + if ($this->options['action'] === 'create') { + $data['uploadTime'] = TIME_NOW; + } + // get image data if (($imageData = $uploadFile->getImageData()) !== null) { $data['width'] = $imageData['width']; @@ -121,12 +126,20 @@ class DefaultUploadFileSaveStrategy implements IUploadFileSaveStrategy { } /** @var IDatabaseObjectAction $action */ - $action = new $this->actionClassName([], 'create', [ - 'data' => $data + $objects = []; + if (isset($this->options['object'])) { + $objects = [$this->options['object']]; + } + $action = new $this->actionClassName($objects, $this->options['action'], [ + 'data' => $data, ]); /** @var IThumbnailFile $object */ $object = $action->executeAction()['returnValues']; + if (isset($this->options['object'])) { + $className = get_class($this->options['object']); + $object = new $className($this->options['object']->getObjectID()); + } $dir = dirname($object->getLocation()); if (!@file_exists($dir)) { diff --git a/wcfsetup/install/files/lib/system/upload/MediaReplaceUploadFileValidationStrategy.class.php b/wcfsetup/install/files/lib/system/upload/MediaReplaceUploadFileValidationStrategy.class.php new file mode 100644 index 0000000000..0d0387cd6d --- /dev/null +++ b/wcfsetup/install/files/lib/system/upload/MediaReplaceUploadFileValidationStrategy.class.php @@ -0,0 +1,50 @@ + + * @package WoltLabSuite\Core\System\Upload + * @since 5.3 + */ +class MediaReplaceUploadFileValidationStrategy extends MediaUploadFileValidationStrategy { + /** + * media whose file will be replaced + * @var Media + */ + protected $media; + + /** + * Creates a new instance of MediaReplaceUploadFileValidationStrategy. + * + * @param Media $media + */ + public function __construct(Media $media) { + $this->media = $media; + } + + /** + * @inheritDoc + */ + public function validate(UploadFile $uploadFile) { + if (!parent::validate($uploadFile)) { + return false; + } + + if ($this->media->fileType !== $uploadFile->getMimeType()) { + $uploadFile->setValidationErrorType('differentFileType'); + return false; + } + + if (strtolower(pathinfo($this->media->filename, PATHINFO_EXTENSION)) !== strtolower($uploadFile->getFileExtension())) { + $uploadFile->setValidationErrorType('differentFileExtension'); + return false; + } + + return true; + } +} diff --git a/wcfsetup/install/files/style/ui/media.scss b/wcfsetup/install/files/style/ui/media.scss index 2bf6caa075..a915b55290 100644 --- a/wcfsetup/install/files/style/ui/media.scss +++ b/wcfsetup/install/files/style/ui/media.scss @@ -178,6 +178,10 @@ } [id^=mediaEditor] { + .mediaEditorButtons { + margin-bottom: 20px; + } + .mediaThumbnail { text-align: center; margin-bottom: 20px; diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index c61b385319..796a728314 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4064,6 +4064,7 @@ Dateianhänge: + @@ -4090,6 +4091,8 @@ Dateianhänge: session->getPermission('admin.content.cms.canOnlyAccessOwnMedia')}Eigene {/if}Medien]]> + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index d668286224..943c3d8956 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4009,6 +4009,7 @@ Attachments: + @@ -4035,6 +4036,8 @@ Attachments: session->getPermission('admin.content.cms.canOnlyAccessOwnMedia')}Own {/if}Media]]> + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 60e3f1c557..700c393a3d 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -706,6 +706,7 @@ CREATE TABLE wcf1_media ( fileType VARCHAR(255) NOT NULL DEFAULT '', fileHash VARCHAR(255) NOT NULL DEFAULT '', uploadTime INT(10) NOT NULL DEFAULT 0, + fileUpdateTime INT(10) NOT NULL DEFAULT 0, userID INT(10), username VARCHAR(255) NOT NULL, languageID INT(10),