diff --git a/css/main.css b/css/main.css index 3b7d7694..43c4a467 100644 --- a/css/main.css +++ b/css/main.css @@ -121,3 +121,4 @@ .errorMessage { color: var(--color-error); } + diff --git a/lib/Controller/AttachmentsController.php b/lib/Controller/AttachmentsController.php index 8a2ee33c..b7473c10 100644 --- a/lib/Controller/AttachmentsController.php +++ b/lib/Controller/AttachmentsController.php @@ -111,8 +111,6 @@ public function index(ObjectService $objectService): JSONResponse } } - - $filters['_schema'] = 'attachment'; $result = $objectService->findObjects(filters: $filters, config: $dbConfig); @@ -144,15 +142,25 @@ public function show(string|int $id, ObjectService $objectService): JSONResponse } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ + /** + * @NoAdminRequired + * @NoCSRFRequired + * @throws GuzzleException In case the file upload to NextCloud fails. + */ public function create(ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse { - $data = $this->request->getParams(); + $uploadedFile = $this->request->getUploadedFile('_file'); + // Todo: $uploadedFile['content'] does not contain the file content... + $this->fileService->uploadFile(content: $uploadedFile['content'], filePath: $uploadedFile['name']); + $data['downloadUrl'] = $this->fileService->createShareLink(path: $uploadedFile['name']); + $data['type'] = $uploadedFile['type']; + $data['size'] = $uploadedFile['size']; + $explodedName = explode('.', $uploadedFile['name']); + $data['title'] = $explodedName[0]; + $data['extension'] = end($explodedName); + foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); @@ -188,6 +196,16 @@ public function update(string|int $id, ObjectService $objectService, ElasticSear { $data = $this->request->getParams(); + $uploadedFile = $this->request->getUploadedFile('_file'); + // Todo: $uploadedFile['content'] does not contain the file content... + $this->fileService->uploadFile(content: $uploadedFile['content'], filePath: $uploadedFile['name'], update: true); +// $data['downloadUrl'] = $this->fileService->createShareLink(path: $uploadedFile['name']); + $data['type'] = $uploadedFile['type']; + $data['size'] = $uploadedFile['size']; + $explodedName = explode('.', $uploadedFile['name']); + $data['title'] = $explodedName[0]; + $data['extension'] = end($explodedName); + foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); @@ -223,9 +241,15 @@ public function update(string|int $id, ObjectService $objectService, ElasticSear /** * @NoAdminRequired * @NoCSRFRequired + * @throws GuzzleException In case deleting the file from NextCloud fails. + * @throws \OCP\DB\Exception In case deleting attachment from the NextCloud DB fails. */ public function destroy(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse { + $attachment = $this->show($id, $objectService)->getData()->jsonSerialize(); + // Todo: are we sure this is the best way to do this (how do we save the full path to this file in nextCloud) + $this->fileService->deleteFile(filePath: $attachment['title']. '.' .$attachment['extension']); + if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' ) { diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index f2f60655..7c716fce 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -51,11 +51,29 @@ private function getCurrentDomain(): string return $protocol . $host; } + /** + * Gets and returns an array with information about the current user. + * TODO: Username and password used for auth are currently set in config, this should (/could) be dynamic. + * + * @return array An array containing 'username', 'password' for auth and the 'currentUsername'. + */ + private function getUserInfo(): array + { + // Get the current user + $currentUser = $this->userSession->getUser(); + + return [ + 'username' => $this->config->getValueString(app: $this->appName, key: 'adminUsername', default: 'admin'), + 'password' => $this->config->getValueString(app: $this->appName, key: 'adminPassword', default: 'admin'), + 'currentUsername' => $currentUser ? $currentUser->getUID() : 'Guest' + ]; + } + /** * Creates and returns a share link for a file (or folder). * (https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html#create-a-new-share) * - * @param string $path Path to the file/folder which should be shared. + * @param string $path Path (from root) to the file/folder which should be shared. * @param int|null $shareType 0 = user; 1 = group; 3 = public link; 4 = email; 6 = federated cloud share; 7 = circle; 10 = Talk conversation * @param int|null $permissions 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1) * @@ -67,17 +85,12 @@ public function createShareLink(string $path, ?int $shareType = 3, ?int $permiss // API endpoint to create a share $url = "{$this->getCurrentDomain()}/ocs/v2.php/apps/files_sharing/api/v1/shares"; - // Get the admin username & password for auth - $username = $this->config->getValueString(app: $this->appName, key: 'adminUsername', default: 'admin'); - $password = $this->config->getValueString(app: $this->appName, key: 'adminPassword', default: 'admin'); - - // Get the current username - $currentUser = $this->userSession->getUser(); - $currentUsername = $currentUser ? $currentUser->getUID() : 'Guest'; + // Get the admin username & password for auth & get the current username + $userInfo = $this->getUserInfo(); // Data for the POST request $options = [ - 'auth' => [$username, $password], + 'auth' => [$userInfo['username'], $userInfo['password']], 'headers' => [ 'OCS-APIREQUEST' => 'true', 'Content-Type' => 'application/x-www-form-urlencoded' @@ -86,7 +99,7 @@ public function createShareLink(string $path, ?int $shareType = 3, ?int $permiss 'path' => $path, 'shareType' => $shareType, 'permissions' => $permissions, - 'shareWith' => $currentUsername + 'shareWith' => $userInfo['currentUsername'] ] ]; @@ -100,4 +113,74 @@ public function createShareLink(string $path, ?int $shareType = 3, ?int $permiss } } + /** + * Uploads a file to nextCloud. Will overwrite a file if it already exists and create a new one if it doesn't exist. + * + * @param mixed $content The content of the file. + * @param string|null $filePath Path (from root) where to save the file. NOTE: this should include the name and extension/format of the file as well! (example.pdf) + * @param bool|null $update If set to true, the response status code 204 will also be seen as a success result. (NextCloud will return 204 when successfully updating a file) + * + * @return bool True if successful. + * @throws GuzzleException In case the Guzzle call returns an exception. + */ + public function uploadFile(mixed $content, ?string $filePath = '', ?bool $update = false): bool + { + // Get the admin username & password for auth & get the current username + $userInfo = $this->getUserInfo(); + + // API endpoint to upload the file + $url = $this->getCurrentDomain() . '/remote.php/dav/files/' + . $userInfo['currentUsername'] . '/' . ltrim(string: $filePath, characters: '/'); + + try { + $response = $this->client->request('PUT', $url, [ + 'auth' => [$userInfo['username'], $userInfo['password']], + 'body' => $content + ]); + + if ($response->getStatusCode() === 201 || ($update === true && $response->getStatusCode() === 204)) { + return true; + } + } catch (\Exception $e) { + $str = $update === true ? 'update' : 'upload'; + $this->logger->error("File $str failed: " . $e->getMessage()); + throw $e; + } + + return false; + } + + /** + * Deletes a file from nextCloud. + * + * @param string $filePath Path (from root) to the file you want to delete. + * + * @return bool True if successful. + * @throws GuzzleException|Exception In case the Guzzle call returns an exception. + */ + public function deleteFile(string $filePath): bool + { + // Get the admin username & password for auth & get the current username + $userInfo = $this->getUserInfo(); + + // API endpoint to upload the file + $url = $this->getCurrentDomain() . '/remote.php/dav/files/' + . $userInfo['currentUsername'] . '/' . ltrim(string: $filePath, characters: '/'); + + try { + $response = $this->client->request('DELETE', $url, [ + 'auth' => [$userInfo['username'], $userInfo['password']], + ]); + + if ($response->getStatusCode() === 204) { + return true; + } + } catch (\Exception $e) { + $this->logger->error('File deletion failed: ' . $e->getMessage()); + throw $e; + } + + return false; + } + } diff --git a/package-lock.json b/package-lock.json index df64e74f..7a448696 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,9 @@ "@nextcloud/l10n": "^2.0.1", "@nextcloud/router": "^2.0.1", "@nextcloud/vue": "^8.12.0", + "@vueuse/core": "^10.11.0", "apexcharts": "^3.50.0", + "axios": "^1.7.3", "bootstrap-vue": "^2.23.1", "css-loader": "^6.8.1", "lodash": "^4.17.21", @@ -2016,7 +2018,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2039,15 +2040,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2058,7 +2057,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2074,7 +2072,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2087,7 +2084,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2100,7 +2096,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2113,7 +2108,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2176,7 +2170,6 @@ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -2191,7 +2184,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2202,7 +2194,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2228,8 +2219,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -4983,6 +4973,7 @@ "version": "10.11.0", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz", "integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==", + "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.0", @@ -5761,9 +5752,10 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -8171,7 +8163,6 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -8677,7 +8668,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9270,8 +9260,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", @@ -9334,7 +9323,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -9351,7 +9339,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -9364,7 +9351,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -9390,7 +9376,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -9415,7 +9400,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9482,7 +9466,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -9899,7 +9882,6 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, diff --git a/package.json b/package.json index 5e021bd2..c94890cb 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "@nextcloud/l10n": "^2.0.1", "@nextcloud/router": "^2.0.1", "@nextcloud/vue": "^8.12.0", + "@vueuse/core": "^10.11.0", "apexcharts": "^3.50.0", + "axios": "^1.7.3", "bootstrap-vue": "^2.23.1", "css-loader": "^6.8.1", "lodash": "^4.17.21", diff --git a/src/modals/attachment/AddAttachmentModal.vue b/src/modals/attachment/AddAttachmentModal.vue index bd339333..37076b92 100644 --- a/src/modals/attachment/AddAttachmentModal.vue +++ b/src/modals/attachment/AddAttachmentModal.vue @@ -42,25 +42,53 @@ import { navigationStore, publicationStore } from '../../store/store.js' label="Download URL" maxlength="255" :value.sync="publicationStore.attachmentItem.downloadURL" /> +