Support replacing existing media files
authorMatthias Schmidt <gravatronics@live.com>
Sat, 27 Jun 2020 06:44:56 +0000 (08:44 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sat, 27 Jun 2020 06:44:56 +0000 (08:44 +0200)
See #2922

16 files changed:
com.woltlab.wcf/templates/mediaEditor.tpl
com.woltlab.wcf/templates/mediaJavaScript.tpl
wcfsetup/install/files/acp/templates/mediaEditor.tpl
wcfsetup/install/files/acp/templates/mediaJavaScript.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js [new file with mode: 0644]
wcfsetup/install/files/lib/data/media/Media.class.php
wcfsetup/install/files/lib/data/media/MediaAction.class.php
wcfsetup/install/files/lib/page/MediaPage.class.php
wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php
wcfsetup/install/files/lib/system/upload/MediaReplaceUploadFileValidationStrategy.class.php [new file with mode: 0644]
wcfsetup/install/files/style/ui/media.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 9fecb7586c9e7c573a8322ca40b54ddfba122e90..e25e443cbd132694152dcd4b92c4f2f8731de3c7 100644 (file)
@@ -1,3 +1,7 @@
+<ul class="mediaEditorButtons buttonGroup">
+       <li><div class="mediaManagerMediaReplaceButton"></div></li>
+</ul>
+
 {if $media->isImage && $media->hasThumbnail('small')}
        <div class="mediaThumbnail">
                {@$media->getThumbnailTag('small')}
index b8c5d5fed709a7c058bcf4f20459512b3b25a3b2..609991a1bfa679e13ceb0c414fb6441e227e5fed 100644 (file)
@@ -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}',
index 9fecb7586c9e7c573a8322ca40b54ddfba122e90..e25e443cbd132694152dcd4b92c4f2f8731de3c7 100644 (file)
@@ -1,3 +1,7 @@
+<ul class="mediaEditorButtons buttonGroup">
+       <li><div class="mediaManagerMediaReplaceButton"></div></li>
+</ul>
+
 {if $media->isImage && $media->hasThumbnail('small')}
        <div class="mediaThumbnail">
                {@$media->getThumbnailTag('small')}
index bfa7c9750b9afc31aa4cc8e01340a4273076c564..609991a1bfa679e13ceb0c414fb6441e227e5fed 100644 (file)
@@ -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}',
index 9f5b8072dd77abaf752e852321f0a27f36f017af..c430f1b0692264b2dc16f96bcb4ab7056851a4a5 100644 (file)
@@ -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);
+                       }
                }
        };
        
index 5f811a6ff6c85c2a35ab07e175a61b8713fb7e80..f51656121b4878056dc3568eabbcf895c6c847f8 100644 (file)
@@ -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 (file)
index 0000000..c77c8c1
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * Uploads replacemnts for media files.
+ *
+ * @author      Matthias Schmidt
+ * @copyright   2001-2020 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+       }
+);
index 65a888a26070d6c4c96b83d03f1a4cca781ba11c..3a4b32ea01c7d954ccf4374b89b8550fdb659855 100644 (file)
@@ -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
index ff71759694a324cb8d472f9084c1b92fa62846ca..42d5a5f6fe9085d288a2b65cbbc6a5379984f3a5 100644 (file)
@@ -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;
+       }
 }
index 7d247b1bbd0853d196c3243b6c9fadadb1b6f111..2311fad04a6fdb720600d44a329bcbf5959495ed 100644 (file)
@@ -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) {
index 9bb1874936536863c5397b8d4cbd28b8a7b5949b..39337985732be866c38d65f49c926991d1f439e3 100644 (file)
@@ -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 (file)
index 0000000..0d0387c
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace wcf\system\upload;
+use wcf\data\media\Media;
+
+/**
+ * Upload file validation strategy implementation for media file replacements.
+ * 
+ * @author      Matthias Schmidt
+ * @copyright   2001-2020 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+       }
+}
index 2bf6caa07590cf76db8fad04f3e159a0b408c206..a915b552901a87b2c9b8140c08ee612ab334c2c1 100644 (file)
 }
 
 [id^=mediaEditor] {
+       .mediaEditorButtons {
+               margin-bottom: 20px;
+       }
+       
        .mediaThumbnail {
                text-align: center;
                margin-bottom: 20px;
index c61b385319080d616baab47397d160cb49aa18f6..796a728314df90bc09b956c44bcdd621afe6fa5d 100644 (file)
@@ -4064,6 +4064,7 @@ Dateianhänge:
        <category name="wcf.media">
                <item name="wcf.media.altText"><![CDATA[Alternativ-Text]]></item>
                <item name="wcf.media.button.insert"><![CDATA[Einfügen]]></item>
+               <item name="wcf.media.button.replaceFile"><![CDATA[Datei ersetzen]]></item>
                <item name="wcf.media.button.select"><![CDATA[Auswählen]]></item>
                <item name="wcf.media.caption"><![CDATA[Bildunterschrift]]></item>
                <item name="wcf.media.caption.enableHtml"><![CDATA[HTML in der Bildunterschrift verwenden]]></item>
@@ -4090,6 +4091,8 @@ Dateianhänge:
                <item name="wcf.media.media.pageTitle"><![CDATA[{if $__wcf->session->getPermission('admin.content.cms.canOnlyAccessOwnMedia')}Eigene {/if}Medien]]></item>
                <item name="wcf.media.search.cancel"><![CDATA[Suche abbrechen]]></item>
                <item name="wcf.media.search.placeholder"><![CDATA[Datei suchen]]></item>
+               <item name="wcf.media.upload.error.differentFileExtension"><![CDATA[Die neue Datei muss die gleiche Dateiendung haben wie die aktuelle Datei.]]></item>
+               <item name="wcf.media.upload.error.differentFileType"><![CDATA[Die neue Datei muss vom gleichen Dateityp sein wie die aktuelle Datei.]]></item>
                <item name="wcf.media.upload.error.noImage"><![CDATA[Die hochgeladene Datei ist kein Bild.]]></item>
                <item name="wcf.media.upload.error.uploadFailed"><![CDATA[Beim Hochladen der Datei ist ein unbekannter Fehler aufgetreten.]]></item>
                <item name="wcf.media.upload.success"><![CDATA[Die Datei wurde erfolgreich hochgeladen.]]></item>
index d66828622452b4bc1093e6eb609bf154fe2c996d..943c3d89562e20b2776551cb38090576b65bd159 100644 (file)
@@ -4009,6 +4009,7 @@ Attachments:
        <category name="wcf.media">
                <item name="wcf.media.altText"><![CDATA[Alternate Text]]></item>
                <item name="wcf.media.button.insert"><![CDATA[Insert]]></item>
+               <item name="wcf.media.button.replaceFile"><![CDATA[Replace File]]></item>
                <item name="wcf.media.button.select"><![CDATA[Select]]></item>
                <item name="wcf.media.caption"><![CDATA[Caption]]></item>
                <item name="wcf.media.caption.enableHtml"><![CDATA[Enable HTML code in caption]]></item>
@@ -4035,6 +4036,8 @@ Attachments:
                <item name="wcf.media.media.pageTitle"><![CDATA[{if $__wcf->session->getPermission('admin.content.cms.canOnlyAccessOwnMedia')}Own {/if}Media]]></item>
                <item name="wcf.media.search.cancel"><![CDATA[Cancel Search]]></item>
                <item name="wcf.media.search.placeholder"><![CDATA[Search Files]]></item>
+               <item name="wcf.media.upload.error.differentFileExtension"><![CDATA[The new file must have the same file extension as the current file.]]></item>
+               <item name="wcf.media.upload.error.differentFileType"><![CDATA[The new file must be of the same file type as the current file.]]></item>
                <item name="wcf.media.upload.error.noImage"><![CDATA[The uploaded file is no image.]]></item>
                <item name="wcf.media.upload.error.uploadFailed"><![CDATA[An unknown error occurred during the upload.]]></item>
                <item name="wcf.media.upload.success"><![CDATA[The file has been successfully uploaded.]]></item>
index 60e3f1c5573ebe4b0b8497e95be855a5e2932cd6..700c393a3d9ad12ce86f7255053918e6186eea88 100644 (file)
@@ -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),