From ec8cc95115559c34151e08494fc67aa9db72b4fc Mon Sep 17 00:00:00 2001 From: Fedik Date: Sun, 9 Feb 2025 17:12:06 +0200 Subject: [PATCH 01/19] Joomla.request promise --- .../resources/scripts/app/Api.es6.js | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/administrator/components/com_media/resources/scripts/app/Api.es6.js b/administrator/components/com_media/resources/scripts/app/Api.es6.js index cc536ba37388e..06739b62b49a1 100644 --- a/administrator/components/com_media/resources/scripts/app/Api.es6.js +++ b/administrator/components/com_media/resources/scripts/app/Api.es6.js @@ -189,33 +189,27 @@ class Api { * @return {Promise.} */ upload(name, parent, content, override) { - // Wrap the ajax call into a real promise - return new Promise((resolve, reject) => { - const url = new URL(`${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`); - const data = { - [this.csrfToken]: '1', - name, - content, - }; + const url = new URL(`${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`); + const data = { + name, + content, + }; - // Append override - if (override === true) { - data.override = true; - } + // Append override + if (override === true) { + data.override = true; + } - Joomla.request({ - url: url.toString(), - method: 'POST', - data: JSON.stringify(data), - headers: { 'Content-Type': 'application/json' }, - onSuccess: (response) => { - notifications.success('COM_MEDIA_UPLOAD_SUCCESS'); - resolve(normalizeItem(JSON.parse(response).data)); - }, - onError: (xhr) => { - reject(xhr); - }, - }); + return Joomla.request({ + url: url.toString(), + method: 'POST', + data: JSON.stringify(data), + headers: { 'Content-Type': 'application/json' }, + promise: true, + }).then((xhr) => { + const response = xhr.responseText; + notifications.success('COM_MEDIA_UPLOAD_SUCCESS'); + return normalizeItem(JSON.parse(response).data); }).catch(handleError); } From 59b1eb556ed7a4190d9df23673775651059a57c3 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sun, 9 Feb 2025 17:55:28 +0200 Subject: [PATCH 02/19] Binary file upload --- .../resources/scripts/app/Api.es6.js | 22 ++++++------- .../scripts/components/browser/browser.vue | 23 +++----------- .../scripts/components/upload/upload.vue | 23 +++----------- .../src/Controller/ApiController.php | 31 ++++++++++++++----- 4 files changed, 45 insertions(+), 54 deletions(-) diff --git a/administrator/components/com_media/resources/scripts/app/Api.es6.js b/administrator/components/com_media/resources/scripts/app/Api.es6.js index 06739b62b49a1..82943dfafa810 100644 --- a/administrator/components/com_media/resources/scripts/app/Api.es6.js +++ b/administrator/components/com_media/resources/scripts/app/Api.es6.js @@ -181,30 +181,30 @@ class Api { } /** - * Upload a file + * Upload a file, + * In opposite to other API calls the Upload call uses Content-type: application/x-www-form-urlencoded + * * @param name * @param parent - * @param content base64 encoded string + * @param content File instance * @param override boolean whether or not we should override existing files * @return {Promise.} */ upload(name, parent, content, override) { - const url = new URL(`${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`); - const data = { - name, - content, - }; + const url = `${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`; + const data = new FormData; + data.append('name', name); + data.append('content', content) // Append override if (override === true) { - data.override = true; + data.append('override', 1); } return Joomla.request({ - url: url.toString(), + url, method: 'POST', - data: JSON.stringify(data), - headers: { 'Content-Type': 'application/json' }, + data, promise: true, }).then((xhr) => { const response = xhr.responseText; diff --git a/administrator/components/com_media/resources/scripts/components/browser/browser.vue b/administrator/components/com_media/resources/scripts/components/browser/browser.vue index 1d67f04797f7b..d8800196b24a1 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/browser.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/browser.vue @@ -247,24 +247,11 @@ export default { /* Upload files */ upload(file) { - // Create a new file reader instance - const reader = new FileReader(); - - // Add the on load callback - reader.onload = (progressEvent) => { - const { result } = progressEvent.target; - const splitIndex = result.indexOf('base64') + 7; - const content = result.slice(splitIndex, result.length); - - // Upload the file - this.$store.dispatch('uploadFile', { - name: file.name, - parent: this.$store.state.selectedDirectory, - content, - }); - }; - - reader.readAsDataURL(file); + this.$store.dispatch('uploadFile', { + name: file.name, + parent: this.$store.state.selectedDirectory, + content: file, + }); }, // Logic for the dropped file diff --git a/administrator/components/com_media/resources/scripts/components/upload/upload.vue b/administrator/components/com_media/resources/scripts/components/upload/upload.vue index 012b82a1b6367..a2c9fa737bed6 100644 --- a/administrator/components/com_media/resources/scripts/components/upload/upload.vue +++ b/administrator/components/com_media/resources/scripts/components/upload/upload.vue @@ -47,24 +47,11 @@ export default { // Loop through array of files and upload each file Array.from(files).forEach((file) => { - // Create a new file reader instance - const reader = new FileReader(); - - // Add the on load callback - reader.onload = (progressEvent) => { - const { result } = progressEvent.target; - const splitIndex = result.indexOf('base64') + 7; - const content = result.slice(splitIndex, result.length); - - // Upload the file - this.$store.dispatch('uploadFile', { - name: file.name, - parent: this.$store.state.selectedDirectory, - content, - }); - }; - - reader.readAsDataURL(file); + this.$store.dispatch('uploadFile', { + name: file.name, + parent: this.$store.state.selectedDirectory, + content: file, + }); }); }, }, diff --git a/administrator/components/com_media/src/Controller/ApiController.php b/administrator/components/com_media/src/Controller/ApiController.php index d5b54c2e47245..31a64b2bed03d 100644 --- a/administrator/components/com_media/src/Controller/ApiController.php +++ b/administrator/components/com_media/src/Controller/ApiController.php @@ -190,15 +190,32 @@ public function postFiles() throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); } - $adapter = $this->getAdapter(); - $path = $this->getPath(); - $content = $this->input->json; - $name = $content->getString('name'); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - $override = $content->get('override', false); + $adapter = $this->getAdapter(); + $path = $this->getPath(); + + // Get the data depending on the request type + if ($this->input->json->count()) { + $content = $this->input->json; + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $mediaLength = \strlen($mediaContent); + } else { + $content = $this->input->post; + $mediaContent = ''; + $mediaLength = 0; + $file = $this->input->files->get('content', []); + + if ($file && empty($file['error'])) { + // Open the uploaded file as a stream, because whole media API are expecting already loaded data. + $mediaContent = fopen($file['tmp_name'], 'r'); + $mediaLength = $file['size']; + } + } + + $name = $content->getString('name'); + $override = $content->getBool('override', false); if ($mediaContent) { - $this->checkFileSize(\strlen($mediaContent)); + $this->checkFileSize($mediaLength); // A file needs to be created $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); From 2d3cfee2a5ce4ffff2d5f2e499a14e8d95f0420c Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Feb 2025 12:47:01 +0200 Subject: [PATCH 03/19] Binary file upload --- .../src/Controller/ApiController.php | 8 ++- .../local/src/Adapter/LocalAdapter.php | 53 ++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/administrator/components/com_media/src/Controller/ApiController.php b/administrator/components/com_media/src/Controller/ApiController.php index 31a64b2bed03d..8488130acbeaa 100644 --- a/administrator/components/com_media/src/Controller/ApiController.php +++ b/administrator/components/com_media/src/Controller/ApiController.php @@ -205,9 +205,11 @@ public function postFiles() $file = $this->input->files->get('content', []); if ($file && empty($file['error'])) { - // Open the uploaded file as a stream, because whole media API are expecting already loaded data. + // Open the uploaded file as a stream, because whole media API are expecting already loaded data, but we do not want to. $mediaContent = fopen($file['tmp_name'], 'r'); $mediaLength = $file['size']; + } elseif (!empty($file['error'])) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); } } @@ -227,6 +229,10 @@ public function postFiles() $options = []; $options['url'] = $this->input->getBool('url', false); + if (\is_resource($mediaContent)) { + fclose($mediaContent); + } + return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); } diff --git a/plugins/filesystem/local/src/Adapter/LocalAdapter.php b/plugins/filesystem/local/src/Adapter/LocalAdapter.php index 1e2f58d7b4dbf..835688fe41d11 100644 --- a/plugins/filesystem/local/src/Adapter/LocalAdapter.php +++ b/plugins/filesystem/local/src/Adapter/LocalAdapter.php @@ -261,6 +261,11 @@ public function createFile(string $name, string $path, $data): string $this->checkContent($localPath, $data); try { + // Ensure the file pointer at beginning, before save. + if (\is_resource($data)) { + rewind($data); + } + File::write($localPath, $data); } catch (FilesystemException $exception) { } @@ -833,34 +838,58 @@ private function getSafeName(string $name): string * Performs various check if it is allowed to save the content with the given name. * * @param string $localPath The local path - * @param string $mediaContent The media content + * @param mixed $mediaContent The media content as resource or string * * @return void * * @since 4.0.0 * @throws \Exception */ - private function checkContent(string $localPath, string $mediaContent) + private function checkContent(string $localPath, $mediaContent) { $name = $this->getFileName($localPath); // The helper $helper = new MediaHelper(); + // Reconstruct the file array, expected by MediaHelper::canUpload() + $file = [ + 'name' => $name, + 'size' => 0, + 'tmp_name' => '', + ]; + $isResource = \is_resource($mediaContent); + + if ($isResource) { + $fstat = fstat($mediaContent); + $fmeta = stream_get_meta_data($mediaContent); + + if (!$fstat) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500); + } - // @todo find a better way to check the input, by not writing the file to the disk - $tmpFile = Path::clean(\dirname($localPath) . '/' . uniqid() . '.' . strtolower(File::getExt($name))); + $file['size'] = $fstat['size']; + $file['tmp_name'] = $fmeta['uri']; + } else { + // @todo find a better way to check the input, by not writing the file to the disk + $tmpFile = Path::clean(\dirname($localPath) . '/' . uniqid() . '.' . strtolower(File::getExt($name))); - try { - File::write($tmpFile, $mediaContent); - } catch (FilesystemException $exception) { - throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500); + try { + File::write($tmpFile, $mediaContent); + } catch (FilesystemException $exception) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500, $exception); + } + + $file['size'] = \strlen($mediaContent); + $file['tmp_name'] = $tmpFile; } - $can = $helper->canUpload(['name' => $name, 'size' => \strlen($mediaContent), 'tmp_name' => $tmpFile], 'com_media'); + $can = $helper->canUpload($file, 'com_media'); - try { - File::delete($tmpFile); - } catch (FilesystemException $exception) { + if (!$isResource) { + try { + File::delete($tmpFile); + } catch (FilesystemException $exception) { + } } if (!$can) { From 53c39ec5a9b31a19ea23a381c67d316feaf37e7c Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Feb 2025 12:58:25 +0200 Subject: [PATCH 04/19] Binary file upload --- .../resources/scripts/store/actions.es6.js | 4 +-- .../src/Controller/ApiController.php | 34 +++++++++++++++---- .../local/src/Adapter/LocalAdapter.php | 5 +++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/administrator/components/com_media/resources/scripts/store/actions.es6.js b/administrator/components/com_media/resources/scripts/store/actions.es6.js index 804ce5e13d617..99da65f0efa14 100644 --- a/administrator/components/com_media/resources/scripts/store/actions.es6.js +++ b/administrator/components/com_media/resources/scripts/store/actions.es6.js @@ -125,9 +125,9 @@ export const createDirectory = (context, payload) => { }; /** - * Create a new folder + * Upload a file * @param context - * @param payload object with the new folder name and its parent directory + * @param payload object with the new file data */ export const uploadFile = (context, payload) => { if (!api.canCreate) { diff --git a/administrator/components/com_media/src/Controller/ApiController.php b/administrator/components/com_media/src/Controller/ApiController.php index 8488130acbeaa..e52799c96f080 100644 --- a/administrator/components/com_media/src/Controller/ApiController.php +++ b/administrator/components/com_media/src/Controller/ApiController.php @@ -197,10 +197,10 @@ public function postFiles() if ($this->input->json->count()) { $content = $this->input->json; $mediaContent = base64_decode($content->get('content', '', 'raw')); - $mediaLength = \strlen($mediaContent); + $mediaLength = $mediaContent ? \strlen($mediaContent) : 0; } else { $content = $this->input->post; - $mediaContent = ''; + $mediaContent = null; $mediaLength = 0; $file = $this->input->files->get('content', []); @@ -286,14 +286,32 @@ public function putFiles() $adapter = $this->getAdapter(); $path = $this->getPath(); - $content = $this->input->json; + // Get the data depending on the request type + if ($this->input->json->count()) { + $content = $this->input->json; + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $mediaLength = $mediaContent ? \strlen($mediaContent) : 0; + } else { + $content = $this->input->post; + $mediaContent = null; + $mediaLength = 0; + $file = $this->input->files->get('content', []); + + if ($file && empty($file['error'])) { + // Open the uploaded file as a stream, because whole media API are expecting already loaded data, but we do not want to. + $mediaContent = fopen($file['tmp_name'], 'r'); + $mediaLength = $file['size']; + } elseif (!empty($file['error'])) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + } + } + $name = basename($path); - $mediaContent = base64_decode($content->get('content', '', 'raw')); $newPath = $content->getString('newPath', null); $move = $content->get('move', true); - if ($mediaContent != null) { - $this->checkFileSize(\strlen($mediaContent)); + if ($mediaContent) { + $this->checkFileSize($mediaLength); $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); } @@ -310,6 +328,10 @@ public function putFiles() $path = $destinationPath; } + if (\is_resource($mediaContent)) { + fclose($mediaContent); + } + return $this->getModel()->getFile($adapter, $path); } diff --git a/plugins/filesystem/local/src/Adapter/LocalAdapter.php b/plugins/filesystem/local/src/Adapter/LocalAdapter.php index 835688fe41d11..49c3d9fa6a589 100644 --- a/plugins/filesystem/local/src/Adapter/LocalAdapter.php +++ b/plugins/filesystem/local/src/Adapter/LocalAdapter.php @@ -307,6 +307,11 @@ public function updateFile(string $name, string $path, $data) $this->checkContent($localPath, $data); try { + // Ensure the file pointer at beginning, before save. + if (\is_resource($data)) { + rewind($data); + } + File::write($localPath, $data); } catch (FilesystemException $exception) { } From 90308376a3770ea90e2969ddcff6d35fc938c674 Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Feb 2025 17:02:19 +0200 Subject: [PATCH 05/19] Upload progress --- .../resources/scripts/app/Api.es6.js | 32 ++++++++++----- .../scripts/components/toolbar/toolbar.vue | 16 ++++++++ .../resources/scripts/store/actions.es6.js | 15 +++++-- .../scripts/store/mutation-types.es6.js | 3 ++ .../resources/scripts/store/mutations.es6.js | 39 +++++++++++++++++++ .../resources/scripts/store/state.es6.js | 4 ++ .../scss/components/_media-toolbar.scss | 14 +++++++ 7 files changed, 109 insertions(+), 14 deletions(-) diff --git a/administrator/components/com_media/resources/scripts/app/Api.es6.js b/administrator/components/com_media/resources/scripts/app/Api.es6.js index 82943dfafa810..745f487645c80 100644 --- a/administrator/components/com_media/resources/scripts/app/Api.es6.js +++ b/administrator/components/com_media/resources/scripts/app/Api.es6.js @@ -181,16 +181,17 @@ class Api { } /** - * Upload a file, - * In opposite to other API calls the Upload call uses Content-type: application/x-www-form-urlencoded - * - * @param name - * @param parent - * @param content File instance - * @param override boolean whether or not we should override existing files - * @return {Promise.} - */ - upload(name, parent, content, override) { + * Upload a file, + * In opposite to other API calls the Upload call uses Content-type: application/x-www-form-urlencoded + * + * @param {string} name File name + * @param {string} parent Parent folder path + * @param {File} content File instance + * @param {boolean} override whether we should override existing files or not + * @param {Function} progressCalback Progress callback + * @return {Promise.} + */ + upload(name, parent, content, override, progressCalback) { const url = `${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`; const data = new FormData; data.append('name', name); @@ -206,6 +207,17 @@ class Api { method: 'POST', data, promise: true, + onBefore: (xhr) => { + if (progressCalback) { + xhr.upload.addEventListener('progress', (event) => { + let progress = 100; + if (event.lengthComputable) { + progress = Math.round((event.loaded / event.total) * 100); + } + progressCalback(progress); + }); + } + }, }).then((xhr) => { const response = xhr.responseText; notifications.success('COM_MEDIA_UPLOAD_SUCCESS'); diff --git a/administrator/components/com_media/resources/scripts/components/toolbar/toolbar.vue b/administrator/components/com_media/resources/scripts/components/toolbar/toolbar.vue index 963d8f67081f9..a95adb8ffacec 100644 --- a/administrator/components/com_media/resources/scripts/components/toolbar/toolbar.vue +++ b/administrator/components/com_media/resources/scripts/components/toolbar/toolbar.vue @@ -8,6 +8,15 @@ v-if="isLoading" class="media-loader" /> +
+
+
0; + }, + uploadProgress() { + // Use extra 2 for initial visibility + return Math.max(this.$store.state.uploadProgress, 2); + }, atLeastOneItemSelected() { return this.$store.state.selectedItems.length > 0; }, diff --git a/administrator/components/com_media/resources/scripts/store/actions.es6.js b/administrator/components/com_media/resources/scripts/store/actions.es6.js index 99da65f0efa14..265cd1aed7773 100644 --- a/administrator/components/com_media/resources/scripts/store/actions.es6.js +++ b/administrator/components/com_media/resources/scripts/store/actions.es6.js @@ -133,14 +133,21 @@ export const uploadFile = (context, payload) => { if (!api.canCreate) { return; } - context.commit(types.SET_IS_LOADING, true); - api.upload(payload.name, payload.parent, payload.content, payload.override || false) + + // Commit the progress + context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, progress: 0}); + const uploadProgress = (progress) => { + context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, progress}); + }; + + // Do file upload + api.upload(payload.name, payload.parent, payload.content, payload.override || false, uploadProgress) .then((file) => { context.commit(types.UPLOAD_SUCCESS, file); - context.commit(types.SET_IS_LOADING, false); + context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, completed: true}); }) .catch((error) => { - context.commit(types.SET_IS_LOADING, false); + context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, completed: true}); // Handle file exists if (error.status === 409) { diff --git a/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js b/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js index 622658040e77f..c89f8da87a766 100644 --- a/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js +++ b/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js @@ -60,3 +60,6 @@ export const UPDATE_SORT_BY = 'UPDATE_SORT_BY'; // Update sorting direction export const UPDATE_SORT_DIRECTION = 'UPDATE_SORT_DIRECTION'; + +// Update list of files currently is uploading and their progress +export const UPDATE_ACTIVE_UPLOADS = 'UPDATE_ACTIVE_UPLOADS'; diff --git a/administrator/components/com_media/resources/scripts/store/mutations.es6.js b/administrator/components/com_media/resources/scripts/store/mutations.es6.js index e81deedf1d753..6b46ca7ea84b9 100644 --- a/administrator/components/com_media/resources/scripts/store/mutations.es6.js +++ b/administrator/components/com_media/resources/scripts/store/mutations.es6.js @@ -493,4 +493,43 @@ export default { [types.UPDATE_SORT_DIRECTION]: (state, payload) => { state.sortDirection = payload === 'asc' ? 'asc' : 'desc'; }, + + /** + * Update list of active uploads + * @param state + * @param payload As following {name: fileName, progress: 0} or {name: fileName, completed: true} + */ + [types.UPDATE_ACTIVE_UPLOADS]: (state, payload) => { + let isNew = true; + let progress = 0; + let toRemove = -1; + + // Collect progress for active uploads + state.activeUploads.forEach((item, idx) => { + if (item.name === payload.name) { + isNew = false; + item.progress = Math.max(item.progress, Math.min(100, payload.progress || 0)); + + if (payload.completed) { + toRemove = idx; + } + } + // Pick element with the smallest progress + if (item.progress > 0) { + progress = !progress ? item.progress : Math.min(progress, item.progress); + } + }); + + // Add new item to the list + if (isNew) { + state.activeUploads.push({ name: payload.name, progress: payload.progress }); + } + + // Remove completed item from the list + if (toRemove !== -1) { + state.activeUploads.splice(toRemove, 1); + } + + state.uploadProgress = progress; + }, }; diff --git a/administrator/components/com_media/resources/scripts/store/state.es6.js b/administrator/components/com_media/resources/scripts/store/state.es6.js index 0b38946fe0701..04b13925becfe 100644 --- a/administrator/components/com_media/resources/scripts/store/state.es6.js +++ b/administrator/components/com_media/resources/scripts/store/state.es6.js @@ -119,4 +119,8 @@ export default { sortBy: storedState && storedState.sortBy ? storedState.sortBy : 'name', // The sorting direction sortDirection: storedState && storedState.sortDirection ? storedState.sortDirection : 'asc', + // Active uploads, [{name: fileName, progress: 0}] + activeUploads: [], + // Upload progress, from 0 to 100 + uploadProgress: 0, }; diff --git a/build/media_source/com_media/scss/components/_media-toolbar.scss b/build/media_source/com_media/scss/components/_media-toolbar.scss index 14e070f8dbedb..8540692a8810c 100644 --- a/build/media_source/com_media/scss/components/_media-toolbar.scss +++ b/build/media_source/com_media/scss/components/_media-toolbar.scss @@ -80,3 +80,17 @@ right: 0; } } + +.media-upload-progress{ + position: absolute; + top: 100%; + right: 0; + left: 0; + + .progress-bar{ + width: 0; + height: 5px; + background-image: $toolbar-loader-color; + transition: width 0.3s ease; + } +} From 838692dfb4b41a0837bfa4f2dfecfc1d871870a7 Mon Sep 17 00:00:00 2001 From: Fedir Zinchuk Date: Mon, 10 Feb 2025 17:28:19 +0200 Subject: [PATCH 06/19] Update administrator/components/com_media/resources/scripts/store/mutation-types.es6.js Co-authored-by: Brian Teeman --- .../com_media/resources/scripts/store/mutation-types.es6.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js b/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js index c89f8da87a766..040008014c8de 100644 --- a/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js +++ b/administrator/components/com_media/resources/scripts/store/mutation-types.es6.js @@ -61,5 +61,5 @@ export const UPDATE_SORT_BY = 'UPDATE_SORT_BY'; // Update sorting direction export const UPDATE_SORT_DIRECTION = 'UPDATE_SORT_DIRECTION'; -// Update list of files currently is uploading and their progress +// Update list of files currently uploading and their progress export const UPDATE_ACTIVE_UPLOADS = 'UPDATE_ACTIVE_UPLOADS'; From 807d21a380020d4c7df503f9daf0727c9c009d8a Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Feb 2025 17:29:13 +0200 Subject: [PATCH 07/19] cs --- .../com_media/scss/components/_media-toolbar.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/media_source/com_media/scss/components/_media-toolbar.scss b/build/media_source/com_media/scss/components/_media-toolbar.scss index 8540692a8810c..44d7ae13727cb 100644 --- a/build/media_source/com_media/scss/components/_media-toolbar.scss +++ b/build/media_source/com_media/scss/components/_media-toolbar.scss @@ -81,16 +81,16 @@ } } -.media-upload-progress{ +.media-upload-progress { position: absolute; top: 100%; right: 0; left: 0; - .progress-bar{ + .progress-bar { width: 0; height: 5px; background-image: $toolbar-loader-color; - transition: width 0.3s ease; + transition: width .3s ease; } } From 7ba8331a59d5efda85f03ad6024beb28093bf8bd Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Feb 2025 17:30:49 +0200 Subject: [PATCH 08/19] cs --- .../components/com_media/resources/scripts/app/Api.es6.js | 4 ++-- .../com_media/resources/scripts/store/actions.es6.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/administrator/components/com_media/resources/scripts/app/Api.es6.js b/administrator/components/com_media/resources/scripts/app/Api.es6.js index 745f487645c80..bf1df918e9488 100644 --- a/administrator/components/com_media/resources/scripts/app/Api.es6.js +++ b/administrator/components/com_media/resources/scripts/app/Api.es6.js @@ -193,9 +193,9 @@ class Api { */ upload(name, parent, content, override, progressCalback) { const url = `${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`; - const data = new FormData; + const data = new FormData(); data.append('name', name); - data.append('content', content) + data.append('content', content); // Append override if (override === true) { diff --git a/administrator/components/com_media/resources/scripts/store/actions.es6.js b/administrator/components/com_media/resources/scripts/store/actions.es6.js index 265cd1aed7773..3927d27daf266 100644 --- a/administrator/components/com_media/resources/scripts/store/actions.es6.js +++ b/administrator/components/com_media/resources/scripts/store/actions.es6.js @@ -135,19 +135,19 @@ export const uploadFile = (context, payload) => { } // Commit the progress - context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, progress: 0}); + context.commit(types.UPDATE_ACTIVE_UPLOADS, { name: payload.name, progress: 0 }); const uploadProgress = (progress) => { - context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, progress}); + context.commit(types.UPDATE_ACTIVE_UPLOADS, { name: payload.name, progress }); }; // Do file upload api.upload(payload.name, payload.parent, payload.content, payload.override || false, uploadProgress) .then((file) => { context.commit(types.UPLOAD_SUCCESS, file); - context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, completed: true}); + context.commit(types.UPDATE_ACTIVE_UPLOADS, { name: payload.name, completed: true }); }) .catch((error) => { - context.commit(types.UPDATE_ACTIVE_UPLOADS, {name: payload.name, completed: true}); + context.commit(types.UPDATE_ACTIVE_UPLOADS, { name: payload.name, completed: true }); // Handle file exists if (error.status === 409) { From c83fbb8e73bc9aa76436ca7fa5c03e7b4ebe4295 Mon Sep 17 00:00:00 2001 From: Fedik Date: Tue, 11 Feb 2025 14:58:05 +0200 Subject: [PATCH 09/19] TmpFileUpload --- .../src/Controller/ApiController.php | 86 ++++++++--- libraries/src/Filesystem/TmpFileUpload.php | 140 ++++++++++++++++++ .../local/src/Adapter/LocalAdapter.php | 36 ++--- 3 files changed, 222 insertions(+), 40 deletions(-) create mode 100644 libraries/src/Filesystem/TmpFileUpload.php diff --git a/administrator/components/com_media/src/Controller/ApiController.php b/administrator/components/com_media/src/Controller/ApiController.php index e52799c96f080..7e019d7451459 100644 --- a/administrator/components/com_media/src/Controller/ApiController.php +++ b/administrator/components/com_media/src/Controller/ApiController.php @@ -11,6 +11,7 @@ namespace Joomla\Component\Media\Administrator\Controller; use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Filesystem\TmpFileUpload; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; @@ -21,6 +22,8 @@ use Joomla\Component\Media\Administrator\Exception\FileExistsException; use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; +use Joomla\Filesystem\File; +use Joomla\Filesystem\Path; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -192,28 +195,48 @@ public function postFiles() $adapter = $this->getAdapter(); $path = $this->getPath(); + $tmpFile = ''; // Get the data depending on the request type if ($this->input->json->count()) { $content = $this->input->json; + $name = $content->getString('name'); $mediaContent = base64_decode($content->get('content', '', 'raw')); - $mediaLength = $mediaContent ? \strlen($mediaContent) : 0; + $mediaLength = 0; + + // Create tmp file + if ($mediaContent) { + $tmpFile = Path::clean($this->app->get('tmp_path') . '/tmp_upload/' . uniqid('tmp-', true)); + $mediaLength = \strlen($mediaContent); + $mediaContent = new TmpFileUpload([ + 'name' => $name, + 'tmp_name' => $tmpFile, + 'size' => $mediaLength, + 'error' => 0, + ]); + + if (!File::write($tmpFile, $mediaContent)) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + } + } } else { $content = $this->input->post; + $name = $content->getString('name'); $mediaContent = null; $mediaLength = 0; $file = $this->input->files->get('content', []); - if ($file && empty($file['error'])) { - // Open the uploaded file as a stream, because whole media API are expecting already loaded data, but we do not want to. - $mediaContent = fopen($file['tmp_name'], 'r'); - $mediaLength = $file['size']; - } elseif (!empty($file['error'])) { - throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + if ($file) { + $file['name'] = $name; + $mediaContent = new TmpFileUpload($file); + $mediaLength = $mediaContent->getSize(); + + if ($mediaContent->getError()) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + } } } - $name = $content->getString('name'); $override = $content->getBool('override', false); if ($mediaContent) { @@ -229,8 +252,11 @@ public function postFiles() $options = []; $options['url'] = $this->input->getBool('url', false); - if (\is_resource($mediaContent)) { - fclose($mediaContent); + if ($tmpFile) { + try { + File::delete($tmpFile); + } catch (\Exception $e) { + } } return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); @@ -285,24 +311,43 @@ public function putFiles() $adapter = $this->getAdapter(); $path = $this->getPath(); + $tmpFile = ''; // Get the data depending on the request type if ($this->input->json->count()) { $content = $this->input->json; + $name = $content->getString('name'); $mediaContent = base64_decode($content->get('content', '', 'raw')); - $mediaLength = $mediaContent ? \strlen($mediaContent) : 0; + $mediaLength = 0; + + // Create tmp file + if ($mediaContent) { + $tmpFile = Path::clean($this->app->get('tmp_path') . '/tmp_upload/' . uniqid('tmp-', true)); + $mediaLength = \strlen($mediaContent); + $mediaContent = new TmpFileUpload([ + 'name' => $name, + 'tmp_name' => $tmpFile, + 'size' => $mediaLength, + 'error' => 0, + ]); + + if (!File::write($tmpFile, $mediaContent)) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + } + } } else { $content = $this->input->post; $mediaContent = null; $mediaLength = 0; $file = $this->input->files->get('content', []); - if ($file && empty($file['error'])) { - // Open the uploaded file as a stream, because whole media API are expecting already loaded data, but we do not want to. - $mediaContent = fopen($file['tmp_name'], 'r'); - $mediaLength = $file['size']; - } elseif (!empty($file['error'])) { - throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + if ($file) { + $mediaContent = new TmpFileUpload($file); + $mediaLength = $mediaContent->getSize(); + + if ($mediaContent->getError()) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + } } } @@ -328,8 +373,11 @@ public function putFiles() $path = $destinationPath; } - if (\is_resource($mediaContent)) { - fclose($mediaContent); + if ($tmpFile) { + try { + File::delete($tmpFile); + } catch (\Exception $e) { + } } return $this->getModel()->getFile($adapter, $path); diff --git a/libraries/src/Filesystem/TmpFileUpload.php b/libraries/src/Filesystem/TmpFileUpload.php new file mode 100644 index 0000000000000..239f8e3578c86 --- /dev/null +++ b/libraries/src/Filesystem/TmpFileUpload.php @@ -0,0 +1,140 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Filesystem; + +\defined('_JEXEC') or die; + +/** + * Class wrapper for uploaded file $_FILE. + * + * @since __DEPLOY_VERSION__ + */ +class TmpFileUpload +{ + /** + * The file name + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected string $name = ''; + + /** + * The file path + * + * @var string + * + * @since __DEPLOY_VERSION__ + */ + protected string $uri = ''; + + /** + * The file size + * + * @var integer + * + * @since __DEPLOY_VERSION__ + */ + protected int $size = 0; + + /** + * The upload error code, if any. + * See https://www.php.net/manual/en/filesystem.constants.php#constant.upload-err-cant-write + * + * @var integer + * + * @since __DEPLOY_VERSION__ + */ + protected int $error = 0; + + /** + * Class constructor. + * + * @param array $file A single $_FILE instance with: name, tmp_name, size, error. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(array $file) + { + $this->name = $file['name'] ?? ''; + $this->uri = $file['tmp_name'] ?? ''; + $this->size = $file['size'] ?? 0; + $this->error = $file['error'] ?? 0; + } + + /** + * Reading the file data while accessing to object as string. + * Made for backward compatibility only. + * + * @return string + * + * @since __DEPLOY_VERSION__ + * + * @deprecated __DEPLOY_VERSION__ will be removed in 7.0 without replacement. + */ + final public function __toString(): string + { + @trigger_error( + 'Stringification and direct accessing to the file content are deprecated, and will be removed in 7.0.', + E_USER_DEPRECATED + ); + + return file_get_contents($this->getUri()) ?: ''; + } + + /** + * Return the name. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getName(): string + { + return $this->name; + } + + /** + * Return the path to the file. + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getUri(): string + { + return $this->uri; + } + + /** + * Return file size. + * + * @return integer + * + * @since __DEPLOY_VERSION__ + */ + public function getSize(): int + { + return $this->size; + } + + /** + * Return upload error code. + * + * @return integer + * + * @since __DEPLOY_VERSION__ + */ + public function getError(): int + { + return $this->error; + } +} diff --git a/plugins/filesystem/local/src/Adapter/LocalAdapter.php b/plugins/filesystem/local/src/Adapter/LocalAdapter.php index 49c3d9fa6a589..8074941542dcb 100644 --- a/plugins/filesystem/local/src/Adapter/LocalAdapter.php +++ b/plugins/filesystem/local/src/Adapter/LocalAdapter.php @@ -12,6 +12,7 @@ use Joomla\CMS\Date\Date; use Joomla\CMS\Factory; +use Joomla\CMS\Filesystem\TmpFileUpload; use Joomla\CMS\Helper\MediaHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Image\Exception\UnparsableImageException; @@ -260,12 +261,12 @@ public function createFile(string $name, string $path, $data): string $this->checkContent($localPath, $data); - try { - // Ensure the file pointer at beginning, before save. - if (\is_resource($data)) { - rewind($data); - } + // Create a stream reference to the file, because we cannot use File::upload() for now. + if ($data instanceof TmpFileUpload) { + $data = fopen($data->getUri(), 'r'); + } + try { File::write($localPath, $data); } catch (FilesystemException $exception) { } @@ -842,15 +843,15 @@ private function getSafeName(string $name): string /** * Performs various check if it is allowed to save the content with the given name. * - * @param string $localPath The local path - * @param mixed $mediaContent The media content as resource or string + * @param string $localPath The local path + * @param string|TmpFileUpload $mediaContent The media content as TmpFileUpload or string * * @return void * * @since 4.0.0 * @throws \Exception */ - private function checkContent(string $localPath, $mediaContent) + private function checkContent(string $localPath, string|TmpFileUpload $mediaContent) { $name = $this->getFileName($localPath); @@ -862,20 +863,13 @@ private function checkContent(string $localPath, $mediaContent) 'size' => 0, 'tmp_name' => '', ]; - $isResource = \is_resource($mediaContent); - - if ($isResource) { - $fstat = fstat($mediaContent); - $fmeta = stream_get_meta_data($mediaContent); - - if (!$fstat) { - throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500); - } + $tmpFile = ''; - $file['size'] = $fstat['size']; - $file['tmp_name'] = $fmeta['uri']; + if ($mediaContent instanceof TmpFileUpload) { + $file['size'] = $mediaContent->getSize(); + $file['tmp_name'] = $mediaContent->getUri(); } else { - // @todo find a better way to check the input, by not writing the file to the disk + // @todo the $mediaContent data should always be TmpFileUpload $tmpFile = Path::clean(\dirname($localPath) . '/' . uniqid() . '.' . strtolower(File::getExt($name))); try { @@ -890,7 +884,7 @@ private function checkContent(string $localPath, $mediaContent) $can = $helper->canUpload($file, 'com_media'); - if (!$isResource) { + if ($tmpFile) { try { File::delete($tmpFile); } catch (FilesystemException $exception) { From 1dd0f54fde7e0b00ff0439ba7ff3f1b330252c18 Mon Sep 17 00:00:00 2001 From: Fedik Date: Tue, 11 Feb 2025 15:05:26 +0200 Subject: [PATCH 10/19] TmpFileUpload --- plugins/filesystem/local/src/Adapter/LocalAdapter.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/filesystem/local/src/Adapter/LocalAdapter.php b/plugins/filesystem/local/src/Adapter/LocalAdapter.php index 8074941542dcb..fba3e4eeaf12e 100644 --- a/plugins/filesystem/local/src/Adapter/LocalAdapter.php +++ b/plugins/filesystem/local/src/Adapter/LocalAdapter.php @@ -307,12 +307,12 @@ public function updateFile(string $name, string $path, $data) $this->checkContent($localPath, $data); - try { - // Ensure the file pointer at beginning, before save. - if (\is_resource($data)) { - rewind($data); - } + // Create a stream reference to the file, because we cannot use File::upload() for now. + if ($data instanceof TmpFileUpload) { + $data = fopen($data->getUri(), 'r'); + } + try { File::write($localPath, $data); } catch (FilesystemException $exception) { } From 18c4bb61ddeffac244669e7191de97624cd14038 Mon Sep 17 00:00:00 2001 From: Fedik Date: Tue, 11 Feb 2025 15:07:02 +0200 Subject: [PATCH 11/19] TmpFileUpload --- libraries/src/Filesystem/TmpFileUpload.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/src/Filesystem/TmpFileUpload.php b/libraries/src/Filesystem/TmpFileUpload.php index 239f8e3578c86..c0dc6d07436da 100644 --- a/libraries/src/Filesystem/TmpFileUpload.php +++ b/libraries/src/Filesystem/TmpFileUpload.php @@ -9,7 +9,9 @@ namespace Joomla\CMS\Filesystem; +// phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Class wrapper for uploaded file $_FILE. From 0905a610e56bce85d2d040ba823d15c8b7b866b5 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sat, 8 Mar 2025 13:19:24 +0200 Subject: [PATCH 12/19] move --- .../components/com_media/src/Controller/ApiController.php | 2 +- .../components/com_media/src/File}/TmpFileUpload.php | 7 ++++--- plugins/filesystem/local/src/Adapter/LocalAdapter.php | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) rename {libraries/src/Filesystem => administrator/components/com_media/src/File}/TmpFileUpload.php (95%) diff --git a/administrator/components/com_media/src/Controller/ApiController.php b/administrator/components/com_media/src/Controller/ApiController.php index 74481c5bd7313..4b2392813ffb8 100644 --- a/administrator/components/com_media/src/Controller/ApiController.php +++ b/administrator/components/com_media/src/Controller/ApiController.php @@ -11,7 +11,6 @@ namespace Joomla\Component\Media\Administrator\Controller; use Joomla\CMS\Component\ComponentHelper; -use Joomla\CMS\Filesystem\TmpFileUpload; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; @@ -22,6 +21,7 @@ use Joomla\Component\Media\Administrator\Exception\FileExistsException; use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; +use Joomla\Component\Media\Administrator\File\TmpFileUpload; use Joomla\Filesystem\File; use Joomla\Filesystem\Path; diff --git a/libraries/src/Filesystem/TmpFileUpload.php b/administrator/components/com_media/src/File/TmpFileUpload.php similarity index 95% rename from libraries/src/Filesystem/TmpFileUpload.php rename to administrator/components/com_media/src/File/TmpFileUpload.php index c0dc6d07436da..ced6ca861b453 100644 --- a/libraries/src/Filesystem/TmpFileUpload.php +++ b/administrator/components/com_media/src/File/TmpFileUpload.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\CMS\Filesystem; +namespace Joomla\Component\Media\Administrator\File; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -18,7 +19,7 @@ * * @since __DEPLOY_VERSION__ */ -class TmpFileUpload +final class TmpFileUpload { /** * The file name diff --git a/plugins/filesystem/local/src/Adapter/LocalAdapter.php b/plugins/filesystem/local/src/Adapter/LocalAdapter.php index 0cc95a96857f3..7cd18aa61d3f8 100644 --- a/plugins/filesystem/local/src/Adapter/LocalAdapter.php +++ b/plugins/filesystem/local/src/Adapter/LocalAdapter.php @@ -12,7 +12,6 @@ use Joomla\CMS\Date\Date; use Joomla\CMS\Factory; -use Joomla\CMS\Filesystem\TmpFileUpload; use Joomla\CMS\Helper\MediaHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Image\Exception\UnparsableImageException; @@ -24,6 +23,7 @@ use Joomla\Component\Media\Administrator\Adapter\AdapterInterface; use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; +use Joomla\Component\Media\Administrator\File\TmpFileUpload; use Joomla\Filesystem\Exception\FilesystemException; use Joomla\Filesystem\File; use Joomla\Filesystem\Folder; From 78fda2f96f32d90665bb0504cbcf81a5fca97065 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sat, 8 Mar 2025 14:18:05 +0200 Subject: [PATCH 13/19] tst --- administrator/components/com_media/src/File/TmpFileUpload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_media/src/File/TmpFileUpload.php b/administrator/components/com_media/src/File/TmpFileUpload.php index ced6ca861b453..f3734f2898f88 100644 --- a/administrator/components/com_media/src/File/TmpFileUpload.php +++ b/administrator/components/com_media/src/File/TmpFileUpload.php @@ -15,7 +15,7 @@ // phpcs:enable PSR1.Files.SideEffects /** - * Class wrapper for uploaded file $_FILE. + * Class wrapper for uploaded file $_FILE.. * * @since __DEPLOY_VERSION__ */ From bb9d53917284e3ca8630ee988d777df4d5cd7191 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sat, 8 Mar 2025 14:18:10 +0200 Subject: [PATCH 14/19] tst --- administrator/components/com_media/src/File/TmpFileUpload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_media/src/File/TmpFileUpload.php b/administrator/components/com_media/src/File/TmpFileUpload.php index f3734f2898f88..ced6ca861b453 100644 --- a/administrator/components/com_media/src/File/TmpFileUpload.php +++ b/administrator/components/com_media/src/File/TmpFileUpload.php @@ -15,7 +15,7 @@ // phpcs:enable PSR1.Files.SideEffects /** - * Class wrapper for uploaded file $_FILE.. + * Class wrapper for uploaded file $_FILE. * * @since __DEPLOY_VERSION__ */ From 7e891b30db661b24733a270a75e2e94dcc1cae55 Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Mar 2025 12:36:25 +0200 Subject: [PATCH 15/19] tst --- administrator/components/com_media/src/File/TmpFileUpload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_media/src/File/TmpFileUpload.php b/administrator/components/com_media/src/File/TmpFileUpload.php index ced6ca861b453..dcd7e31a3f862 100644 --- a/administrator/components/com_media/src/File/TmpFileUpload.php +++ b/administrator/components/com_media/src/File/TmpFileUpload.php @@ -15,7 +15,7 @@ // phpcs:enable PSR1.Files.SideEffects /** - * Class wrapper for uploaded file $_FILE. + * Class wrapper for uploaded file $_FILE.1 * * @since __DEPLOY_VERSION__ */ From 76c69b6c0b5fdc067ec069650597a0398c4565fa Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Mar 2025 12:36:32 +0200 Subject: [PATCH 16/19] tst --- administrator/components/com_media/src/File/TmpFileUpload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administrator/components/com_media/src/File/TmpFileUpload.php b/administrator/components/com_media/src/File/TmpFileUpload.php index dcd7e31a3f862..ced6ca861b453 100644 --- a/administrator/components/com_media/src/File/TmpFileUpload.php +++ b/administrator/components/com_media/src/File/TmpFileUpload.php @@ -15,7 +15,7 @@ // phpcs:enable PSR1.Files.SideEffects /** - * Class wrapper for uploaded file $_FILE.1 + * Class wrapper for uploaded file $_FILE. * * @since __DEPLOY_VERSION__ */ From cf086cfdfb9c759a98ebc81519698a1f3a753ea3 Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Mar 2025 13:32:10 +0200 Subject: [PATCH 17/19] Api controller update --- .../src/Controller/MediaController.php | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index 328e56819a283..d2523455b2321 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -17,8 +17,11 @@ use Joomla\CMS\MVC\Controller\ApiController; use Joomla\Component\Media\Administrator\Exception\FileExistsException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; +use Joomla\Component\Media\Administrator\File\TmpFileUpload; use Joomla\Component\Media\Administrator\Provider\ProviderManagerHelperTrait; use Joomla\Component\Media\Api\Model\MediumModel; +use Joomla\Filesystem\File; +use Joomla\Filesystem\Path; use Joomla\String\Inflector; use Tobscure\JsonApi\Exception\InvalidParameterException; @@ -319,41 +322,61 @@ protected function save($recordKey = null) /** @var MediumModel $model */ $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); - $json = $this->input->json; - - // Decode content, if any - if ($content = base64_decode($json->get('content', '', 'raw'))) { - $this->checkContent(); + $json = $this->input->json; + $name = $json->getString('name'); + $mediaContent = base64_decode($json->get('content', '', 'raw')); + $tmpFile = ''; + + // Create tmp file + if ($mediaContent) { + $tmpFile = Path::clean($this->app->get('tmp_path') . '/tmp_upload/' . uniqid('tmp-', true)); + $mediaLength = \strlen($mediaContent); + $mediaContent = new TmpFileUpload([ + 'name' => $name, + 'tmp_name' => $tmpFile, + 'size' => $mediaLength, + 'error' => 0, + ]); + + $this->checkContent($mediaLength); + + if (!File::write($tmpFile, $mediaContent)) { + throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); + } } // If there is no content, com_media assumes the path refers to a folder. - $this->modelState->set('content', $content); + $this->modelState->set('content', $mediaContent); + + $result = $model->save(); + + if ($tmpFile) { + try { + File::delete($tmpFile); + } catch (\Exception) { + } + } - return $model->save(); + return $result; } /** * Performs various checks to see if it is allowed to save the content. * + * @param integer $fileSize The size of submitted file + * * @return void * * @since 4.1.0 * * @throws \RuntimeException */ - private function checkContent(): void + private function checkContent(int $fileSize): void { - $params = ComponentHelper::getParams('com_media'); - $helper = new \Joomla\CMS\Helper\MediaHelper(); - $serverlength = $this->input->server->getInt('CONTENT_LENGTH'); - - // Check if the size of the request body does not exceed various server imposed limits. - if ( - ($params->get('upload_maxsize', 0) > 0 && $serverlength > ($params->get('upload_maxsize', 0) * 1024 * 1024)) - || $serverlength > $helper->toBytes(\ini_get('upload_max_filesize')) - || $serverlength > $helper->toBytes(\ini_get('post_max_size')) - || $serverlength > $helper->toBytes(\ini_get('memory_limit')) - ) { + $params = ComponentHelper::getParams('com_media'); + $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; + + if ($paramsUploadMaxsize > 0 && $fileSize > $paramsUploadMaxsize) { throw new \RuntimeException(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 400); } } From 7005c58aef7968126b022f564520ccdede151417 Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 10 Mar 2025 14:31:50 +0200 Subject: [PATCH 18/19] Api controller update --- .../com_media/src/Controller/ApiController.php | 10 ++++++---- .../com_media/src/Controller/MediaController.php | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/administrator/components/com_media/src/Controller/ApiController.php b/administrator/components/com_media/src/Controller/ApiController.php index 4b2392813ffb8..758791b77cba5 100644 --- a/administrator/components/com_media/src/Controller/ApiController.php +++ b/administrator/components/com_media/src/Controller/ApiController.php @@ -206,8 +206,9 @@ public function postFiles() // Create tmp file if ($mediaContent) { + $tmpContent = $mediaContent; $tmpFile = Path::clean($this->app->get('tmp_path') . '/tmp_upload/' . uniqid('tmp-', true)); - $mediaLength = \strlen($mediaContent); + $mediaLength = \strlen($tmpContent); $mediaContent = new TmpFileUpload([ 'name' => $name, 'tmp_name' => $tmpFile, @@ -215,7 +216,7 @@ public function postFiles() 'error' => 0, ]); - if (!File::write($tmpFile, $mediaContent)) { + if (!File::write($tmpFile, $tmpContent)) { throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); } } @@ -322,8 +323,9 @@ public function putFiles() // Create tmp file if ($mediaContent) { + $tmpContent = $mediaContent; $tmpFile = Path::clean($this->app->get('tmp_path') . '/tmp_upload/' . uniqid('tmp-', true)); - $mediaLength = \strlen($mediaContent); + $mediaLength = \strlen($tmpContent); $mediaContent = new TmpFileUpload([ 'name' => $name, 'tmp_name' => $tmpFile, @@ -331,7 +333,7 @@ public function putFiles() 'error' => 0, ]); - if (!File::write($tmpFile, $mediaContent)) { + if (!File::write($tmpFile, $tmpContent)) { throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); } } diff --git a/api/components/com_media/src/Controller/MediaController.php b/api/components/com_media/src/Controller/MediaController.php index d2523455b2321..d540b9bf393c6 100644 --- a/api/components/com_media/src/Controller/MediaController.php +++ b/api/components/com_media/src/Controller/MediaController.php @@ -323,14 +323,15 @@ protected function save($recordKey = null) $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]); $json = $this->input->json; - $name = $json->getString('name'); + $name = basename($json->getString('path', '')); $mediaContent = base64_decode($json->get('content', '', 'raw')); $tmpFile = ''; // Create tmp file if ($mediaContent) { + $tmpContent = $mediaContent; $tmpFile = Path::clean($this->app->get('tmp_path') . '/tmp_upload/' . uniqid('tmp-', true)); - $mediaLength = \strlen($mediaContent); + $mediaLength = \strlen($tmpContent); $mediaContent = new TmpFileUpload([ 'name' => $name, 'tmp_name' => $tmpFile, @@ -340,7 +341,7 @@ protected function save($recordKey = null) $this->checkContent($mediaLength); - if (!File::write($tmpFile, $mediaContent)) { + if (!File::write($tmpFile, $tmpContent)) { throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT')); } } From 6ef71e52699a36fa546ff98124befbedbcf69283 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sun, 19 Oct 2025 14:11:26 +0300 Subject: [PATCH 19/19] typo --- .../com_media/resources/scripts/app/Api.es6.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/administrator/components/com_media/resources/scripts/app/Api.es6.js b/administrator/components/com_media/resources/scripts/app/Api.es6.js index bf1df918e9488..40f68bfb82f58 100644 --- a/administrator/components/com_media/resources/scripts/app/Api.es6.js +++ b/administrator/components/com_media/resources/scripts/app/Api.es6.js @@ -184,14 +184,14 @@ class Api { * Upload a file, * In opposite to other API calls the Upload call uses Content-type: application/x-www-form-urlencoded * - * @param {string} name File name - * @param {string} parent Parent folder path - * @param {File} content File instance - * @param {boolean} override whether we should override existing files or not - * @param {Function} progressCalback Progress callback + * @param {string} name File name + * @param {string} parent Parent folder path + * @param {File} content File instance + * @param {boolean} override whether we should override existing files or not + * @param {Function} progressCallback Progress callback * @return {Promise.} */ - upload(name, parent, content, override, progressCalback) { + upload(name, parent, content, override, progressCallback) { const url = `${this.baseUrl}&task=api.files&path=${encodeURIComponent(parent)}`; const data = new FormData(); data.append('name', name); @@ -208,13 +208,13 @@ class Api { data, promise: true, onBefore: (xhr) => { - if (progressCalback) { + if (progressCallback) { xhr.upload.addEventListener('progress', (event) => { let progress = 100; if (event.lengthComputable) { progress = Math.round((event.loaded / event.total) * 100); } - progressCalback(progress); + progressCallback(progress); }); } },