diff --git a/appinfo/routes.php b/appinfo/routes.php index e85cf5f3..10330fdb 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -84,6 +84,10 @@ return [ ['name' => 'Download#file', 'url' => '/api/download/{handle}', 'verb' => 'GET'], ['name' => 'Download#one', 'url' => '/api/stream/{fileid}', 'verb' => 'GET'], + ['name' => 'Share#links', 'url' => '/api/share/links', 'verb' => 'GET'], + ['name' => 'Share#createNode', 'url' => '/api/share/node', 'verb' => 'POST'], + ['name' => 'Share#deleteShare', 'url' => '/api/share/delete', 'verb' => 'POST'], + // Config API ['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'], diff --git a/lib/Controller/ShareController.php b/lib/Controller/ShareController.php new file mode 100644 index 00000000..049eb1c8 --- /dev/null +++ b/lib/Controller/ShareController.php @@ -0,0 +1,160 @@ + + * @author Varun Patil + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Memories\Controller; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; + +class ShareController extends ApiBase +{ + /** + * @NoAdminRequired + * + * Get the tokens of a node shared using an external link + */ + public function links($id, $path) + { + $file = $this->getNodeByIdOrPath($id, $path); + if (!$file) { + return new JSONResponse([ + 'message' => 'File not found', + ], Http::STATUS_FORBIDDEN); + } + + /** @var \OCP\Share\IManager $shareManager */ + $shareManager = \OC::$server->get(\OCP\Share\IManager::class); + + $shares = $shareManager->getSharesBy($this->getUID(), \OCP\Share\IShare::TYPE_LINK, $file, true, 50, 0); + if (empty($shares)) { + return new JSONResponse([ + 'message' => 'No external links found', + ], Http::STATUS_NOT_FOUND); + } + + /** @var \OCP\IURLGenerator $urlGenerator */ + $urlGenerator = \OC::$server->get(\OCP\IURLGenerator::class); + + $links = array_map(function (\OCP\Share\IShare $share) use ($urlGenerator) { + $tok = $share->getToken(); + $exp = $share->getExpirationDate(); + $url = $urlGenerator->linkToRouteAbsolute('memories.Public.showShare', [ + 'token' => $tok, + ]); + + return [ + 'id' => $share->getFullId(), + 'label' => $share->getLabel(), + 'token' => $tok, + 'url' => $url, + 'hasPassword' => $share->getPassword() ? true : false, + 'expiration' => $exp ? $exp->getTimestamp() : null, + 'editable' => $share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE, + ]; + }, $shares); + + return new JSONResponse($links, Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * + * Share a node using an external link + */ + public function createNode($id, $path) + { + $file = $this->getNodeByIdOrPath($id, $path); + if (!$file) { + return new JSONResponse([ + 'message' => 'You are not allowed to share this file', + ], Http::STATUS_FORBIDDEN); + } + + /** @var \OCP\Share\IManager $shareManager */ + $shareManager = \OC::$server->get(\OCP\Share\IManager::class); + + /** @var \OCP\Share\IShare $share */ + $share = $shareManager->newShare(); + $share->setNode($file); + $share->setShareType(\OCP\Share\IShare::TYPE_LINK); + $share->setSharedBy($this->userSession->getUser()->getUID()); + $share->setPermissions(\OCP\Constants::PERMISSION_READ); + + $shareManager->createShare($share); + + return new JSONResponse([ + 'token' => $share->getToken(), + ], Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * + * Delete an external link share + */ + public function deleteShare(string $id) + { + $uid = $this->getUID(); + if (!$uid) { + return new JSONResponse([ + 'message' => 'You are not logged in', + ], Http::STATUS_FORBIDDEN); + } + + /** @var \OCP\Share\IManager $shareManager */ + $shareManager = \OC::$server->get(\OCP\Share\IManager::class); + + $share = $shareManager->getShareById($id); + + if ($share->getSharedBy() !== $uid) { + return new JSONResponse([ + 'message' => 'You are not the owner of this share', + ], Http::STATUS_FORBIDDEN); + } + + $shareManager->deleteShare($share); + + return new JSONResponse([], Http::STATUS_OK); + } + + private function getNodeByIdOrPath($id, $path) { + $uid = $this->getUID(); + if (!$uid) { + return null; + } + + $file = null; + if ($id) { + $file = $this->getUserFile($id); + } else if ($path) { + $userFolder = $this->rootFolder->getUserFolder($uid); + $file = $userFolder->get($path); + } + + if (!$file || !$file->isShareable()) { + return null; + } + + return $file; + } +} diff --git a/src/components/modal/FolderShareModal.vue b/src/components/modal/FolderShareModal.vue index a6a0615d..6ac5147b 100644 --- a/src/components/modal/FolderShareModal.vue +++ b/src/components/modal/FolderShareModal.vue @@ -6,36 +6,65 @@ :sidebar="!isRoot ? this.folderPath : null" >
{{ t("memories", "You cannot share the root folder") }}
- {{ t("memories", "Use the sidebar to share this folder.") }}
{{ t( "memories", - "After creating a public share link in the sidebar, click Refresh and a corresponding link to Memories will be shown below." + "Public link shares are available to people outside Nextcloud." ) }} +
+ {{ + t( + "memories", + "You may create or update permissions on public links using the sidebar." + ) + }} +
+ {{ t("memories", "Click a link to copy to clipboard.") }}
+ + @@ -46,20 +75,44 @@ import { defineComponent } from "vue"; import axios from "@nextcloud/axios"; -import { generateOcsUrl, generateUrl } from "@nextcloud/router"; +import { showSuccess } from "@nextcloud/dialogs"; +import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import UserConfig from "../../mixins/UserConfig"; import NcButton from "@nextcloud/vue/dist/Components/NcButton"; +import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; +const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem"); +import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import * as utils from "../../services/Utils"; import Modal from "./Modal.vue"; -import { Type } from "@nextcloud/sharing"; + +import { API } from "../../services/API"; + +import CloseIcon from "vue-material-design-icons/Close.vue"; +import LinkIcon from "vue-material-design-icons/LinkVariant.vue"; + +type IShare = { + id: string; + label: string; + token: string; + url: string; + hasPassword: boolean; + expiration: number | null; + editable: number; +}; export default defineComponent({ name: "FolderShareModal", components: { Modal, NcButton, + NcLoadingIcon, + NcListItem, + NcActionButton, + + CloseIcon, + LinkIcon, }, mixins: [UserConfig], @@ -67,7 +120,8 @@ export default defineComponent({ data: () => ({ show: false, folderPath: "", - links: [] as { url: string }[], + loading: false, + shares: [] as IShare[], }), computed: { @@ -76,6 +130,14 @@ export default defineComponent({ }, }, + created() { + subscribe("update:share", this.refreshUrls); + }, + + beforeDestroy() { + unsubscribe("update:share", this.refreshUrls); + }, + methods: { close() { this.show = false; @@ -90,22 +152,63 @@ export default defineComponent({ }, async refreshUrls() { - const query = `format=json&path=${encodeURIComponent( - this.folderPath - )}&reshares=true`; - const url = generateOcsUrl(`/apps/files_sharing/api/v1/shares?${query}`); - const response = await axios.get(url); - const data = response.data?.ocs?.data; - if (data) { - this.links = data - .filter((s) => s.share_type === Type.SHARE_TYPE_LINK && s.token) - .map((share: any) => ({ - url: - window.location.origin + - generateUrl(`/apps/memories/s/${share.token}`), - })); + this.loading = true; + try { + this.shares = ( + await axios.get(API.Q(API.SHARE_LINKS(), { path: this.folderPath })) + ).data; + } finally { + this.loading = false; } }, + + getShareLabels(share: IShare): string { + const labels = []; + if (share.hasPassword) { + labels.push(this.t("memories", "Password protected")); + } + + if (share.expiration) { + const exp = utils.getLongDateStr(new Date(share.expiration * 1000)); + const kw = this.t("memories", "Expires"); + labels.push(`${kw} ${exp}`); + } + + if (share.editable) { + labels.push(this.t("memories", "Editable")); + } + + if (labels.length > 0) { + return `${labels.join(", ")}`; + } + + return this.t("memories", "Read only"); + }, + + async createLink() { + this.loading = true; + try { + await axios.post(API.SHARE_NODE(), { path: this.folderPath }); + } finally { + this.loading = false; + } + this.refreshUrls(); + }, + + async deleteLink(share: IShare) { + this.loading = true; + try { + await axios.post(API.SHARE_DELETE(), { id: share.id }); + } finally { + this.loading = false; + } + this.refreshUrls(); + }, + + copy(url: string) { + window.navigator.clipboard.writeText(url); + showSuccess(this.t("memories", "Link copied to clipboard")); + }, }, }); @@ -113,10 +216,9 @@ export default defineComponent({ diff --git a/src/services/API.ts b/src/services/API.ts index abecee89..baa71087 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -18,16 +18,24 @@ function tok(url: string) { } export class API { - static Q(url: string, query: string | URLSearchParams | undefined | null) { + static Q( + url: string, + query: string | URLSearchParams | Object | undefined | null + ) { if (!query) return url; - let queryStr = typeof query === "string" ? query : query.toString(); - if (!queryStr) return url; + if (query instanceof URLSearchParams) { + query = query.toString(); + } else if (typeof query === "object") { + query = new URLSearchParams(query as any).toString(); + } + + if (!query) return url; if (url.indexOf("?") > -1) { - return `${url}&${queryStr}`; + return `${url}&${query}`; } else { - return `${url}?${queryStr}`; + return `${url}?${query}`; } } @@ -127,6 +135,18 @@ export class API { return tok(gen(`${BASE}/stream/{id}`, { id })); } + static SHARE_LINKS() { + return gen(`${BASE}/share/links`); + } + + static SHARE_NODE() { + return gen(`${BASE}/share/node`); + } + + static SHARE_DELETE() { + return gen(`${BASE}/share/delete`); + } + static CONFIG(setting: string) { return gen(`${BASE}/config/{setting}`, { setting }); }