From 46b53063fa5de621dedf958e95c61e788f790e32 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Sun, 5 Dec 2021 00:40:57 +0100 Subject: [PATCH 1/8] Move copyShareLink to separate Mixin Signed-off-by: Jonas Rittershofer --- src/mixins/ShareLinkMixin.js | 48 ++++++++++++++++++++++++++++++++++++ src/mixins/ViewsMixin.js | 15 ++--------- src/views/Create.vue | 3 ++- src/views/Results.vue | 3 ++- 4 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 src/mixins/ShareLinkMixin.js diff --git a/src/mixins/ShareLinkMixin.js b/src/mixins/ShareLinkMixin.js new file mode 100644 index 000000000..a5d69c101 --- /dev/null +++ b/src/mixins/ShareLinkMixin.js @@ -0,0 +1,48 @@ +/** + * @copyright Copyright (c) 2021 Jonas Rittershofer + * + * @author Jonas Rittershofer + * + * @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 . + * + */ +import { generateUrl } from '@nextcloud/router' +import { showError, showSuccess } from '@nextcloud/dialogs' +import Clipboard from 'v-clipboard' +import Vue from 'vue' + +Vue.use(Clipboard) + +export default { + methods: { + /** + * Copy the share-link to clipboard. + * Where imported, this method requires the property 'form' to be set! + * + * @param {object} event Origin event of function call. + */ + async copyShareLink(event) { + const shareLink = window.location.protocol + '//' + window.location.host + generateUrl(`/apps/forms/${this.form.hash}`) + if (this.$clipboard(shareLink)) { + showSuccess(t('forms', 'Form link copied')) + } else { + showError(t('forms', 'Cannot copy, please copy the link manually')) + } + // Set back focus as clipboard removes focus + event.target.focus() + }, + }, +} diff --git a/src/mixins/ViewsMixin.js b/src/mixins/ViewsMixin.js index 6ae6dbb23..42f35a98c 100644 --- a/src/mixins/ViewsMixin.js +++ b/src/mixins/ViewsMixin.js @@ -20,8 +20,8 @@ * */ -import { generateUrl, generateOcsUrl } from '@nextcloud/router' -import { showError, showSuccess } from '@nextcloud/dialogs' +import { generateOcsUrl } from '@nextcloud/router' +import { showError } from '@nextcloud/dialogs' import axios from '@nextcloud/axios' import Clipboard from 'v-clipboard' import Vue from 'vue' @@ -120,16 +120,5 @@ export default { console.error(error) } }, - - copyShareLink(event) { - const $shareLink = window.location.protocol + '//' + window.location.host + generateUrl(`/apps/forms/${this.form.hash}`) - if (this.$clipboard($shareLink)) { - showSuccess(t('forms', 'Form link copied')) - } else { - showError(t('forms', 'Cannot copy, please copy the link manually')) - } - // Set back focus as clipboard removes focus - event.target.focus() - }, }, } diff --git a/src/views/Create.vue b/src/views/Create.vue index c40c7b965..7fbb69ecd 100644 --- a/src/views/Create.vue +++ b/src/views/Create.vue @@ -142,6 +142,7 @@ import QuestionLong from '../components/Questions/QuestionLong.vue' import QuestionMultiple from '../components/Questions/QuestionMultiple.vue' import QuestionShort from '../components/Questions/QuestionShort.vue' import TopBar from '../components/TopBar.vue' +import ShareLinkMixin from '../mixins/ShareLinkMixin.js' import ViewsMixin from '../mixins/ViewsMixin.js' import SetWindowTitle from '../utils/SetWindowTitle.js' import OcsResponse2Data from '../utils/OcsResponse2Data.js' @@ -163,7 +164,7 @@ export default { TopBar, }, - mixins: [ViewsMixin], + mixins: [ViewsMixin, ShareLinkMixin], props: { sidebarOpened: { diff --git a/src/views/Results.vue b/src/views/Results.vue index c5e9a8c2d..b71f95385 100644 --- a/src/views/Results.vue +++ b/src/views/Results.vue @@ -136,6 +136,7 @@ import EmptyContent from '../components/EmptyContent.vue' import Summary from '../components/Results/Summary.vue' import Submission from '../components/Results/Submission.vue' import TopBar from '../components/TopBar.vue' +import ShareLinkMixin from '../mixins/ShareLinkMixin.js' import ViewsMixin from '../mixins/ViewsMixin.js' import answerTypes from '../models/AnswerTypes.js' import SetWindowTitle from '../utils/SetWindowTitle.js' @@ -162,7 +163,7 @@ export default { TopBar, }, - mixins: [ViewsMixin], + mixins: [ViewsMixin, ShareLinkMixin], data() { return { From 89637d9742f7a5ead7d599bbba7bf5d6cbb9b338 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Fri, 29 Apr 2022 22:16:01 +0200 Subject: [PATCH 2/8] Rewrite Sidebar Sharing Signed-off-by: Jonas Rittershofer --- appinfo/info.xml | 2 +- appinfo/routes.php | 18 + lib/Constants.php | 10 + lib/Controller/ApiController.php | 37 +- lib/Controller/ShareApiController.php | 200 +++++++++ lib/Db/Share.php | 64 +++ lib/Db/ShareMapper.php | 108 +++++ .../Version030000Date20211206213004.php | 184 ++++++++ lib/Service/FormsService.php | 163 +++++--- package-lock.json | 4 +- package.json | 2 +- src/components/ShareDiv.vue | 394 ------------------ .../SidebarTabs/SettingsSidebarTab.vue | 206 +++++++++ .../SidebarTabs/SharingSearchDiv.vue | 314 ++++++++++++++ .../SidebarTabs/SharingShareDiv.vue | 94 +++++ .../SidebarTabs/SharingSidebarTab.vue | 241 +++++++++++ src/components/UserDiv.vue | 98 ----- src/mixins/ShareTypes.js | 38 ++ src/views/Sidebar.vue | 297 ++----------- 19 files changed, 1642 insertions(+), 832 deletions(-) create mode 100644 lib/Controller/ShareApiController.php create mode 100644 lib/Db/Share.php create mode 100644 lib/Db/ShareMapper.php create mode 100644 lib/Migration/Version030000Date20211206213004.php delete mode 100644 src/components/ShareDiv.vue create mode 100644 src/components/SidebarTabs/SettingsSidebarTab.vue create mode 100644 src/components/SidebarTabs/SharingSearchDiv.vue create mode 100644 src/components/SidebarTabs/SharingShareDiv.vue create mode 100644 src/components/SidebarTabs/SharingSidebarTab.vue delete mode 100644 src/components/UserDiv.vue diff --git a/appinfo/info.xml b/appinfo/info.xml index d5f69dab7..886eb0a93 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -11,7 +11,7 @@ - **🔒 Data under your control!** Unlike in Google Forms, Typeform, Doodle and others, the survey info and responses are kept private on your instance. - **🙋 Get involved!** We have lots of stuff planned like more question types, collaboration on forms, [and much more](https://github.com/nextcloud/forms/milestones)! ]]> - 2.5.0 + 3.0.0-alpha.0 agpl Affan Hussain diff --git a/appinfo/routes.php b/appinfo/routes.php index c8662fe62..0c5bbb03b 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -164,6 +164,24 @@ ] ], + // Shares + [ + 'name' => 'shareApi#newShare', + 'url' => '/api/{apiVersion}/share', + 'verb' => 'POST', + 'requirements' => [ + 'apiVersion' => 'v2' + ] + ], + [ + 'name' => 'shareApi#deleteShare', + 'url' => '/api/{apiVersion}/share/{id}', + 'verb' => 'DELETE', + 'requirements' => [ + 'apiVersion' => 'v2' + ] + ], + // Submissions [ 'name' => 'api#getSubmissions', diff --git a/lib/Constants.php b/lib/Constants.php index 39ddb65a2..51ee48282 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -23,6 +23,8 @@ namespace OCA\Forms; +use OCP\Share\IShare; + class Constants { /** * Maximum String lengths, the database is set to store. @@ -82,4 +84,12 @@ class Constants { self::ANSWER_TYPE_DATETIME => 'Y-m-d H:i', self::ANSWER_TYPE_TIME => 'H:i' ]; + + /** + * !! Keep in sync with src/mixins/ShareTypes.js !! + */ + public const SHARE_TYPES_USED = [ + IShare::TYPE_USER, + IShare::TYPE_GROUP + ]; } diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 307887075..d13f1113a 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -27,8 +27,6 @@ namespace OCA\Forms\Controller; -use Exception; - use OCA\Forms\Activity\ActivityManager; use OCA\Forms\Constants; use OCA\Forms\Db\Answer; @@ -173,11 +171,8 @@ public function getSharedForms(): DataResponse { $result = []; foreach ($forms as $form) { - // Don't add if user is owner, user has no access, form has expired, form is link-shared - if ($form->getOwnerId() === $this->currentUser->getUID() - || !$this->formsService->hasUserAccess($form->getId()) - || $this->formsService->hasFormExpired($form->getId()) - || $form->getAccess()['type'] === 'public') { + // Check if the form should be shown on sidebar + if (!$this->formsService->isSharedFormShown($form->getId())) { continue; } @@ -246,9 +241,8 @@ public function newForm(): DataResponse { $form->setTitle(''); $form->setDescription(''); $form->setAccess([ - 'type' => 'public', - 'users' => [], - 'groups' => [] + 'permitAllUsers' => false, + 'showToAllUsers' => false, ]); $form->setSubmitOnce(true); @@ -257,6 +251,7 @@ public function newForm(): DataResponse { // Return like getForm(), just without loading Questions (as there are none). $result = $form->read(); $result['questions'] = []; + $result['shares'] = []; return new DataResponse($result); } @@ -371,28 +366,6 @@ public function updateForm(int $id, array $keyValuePairs): DataResponse { throw new OCSForbiddenException(); } - // Handle access-changes - if (array_key_exists('access', $keyValuePairs)) { - // Make sure we only store id of shares - try { - $keyValuePairs['access']['users'] = array_map(function (array $user): string { - return $user['shareWith']; - }, $keyValuePairs['access']['users']); - $keyValuePairs['access']['groups'] = array_map(function (array $group): string { - return $group['shareWith']; - }, $keyValuePairs['access']['groups']); - } catch (Exception $e) { - $this->logger->debug('Malformed access'); - throw new OCSBadRequestException('Malformed access'); - } - - // For selected sharing, notify users (creates Activity) - if ($keyValuePairs['access']['type'] === 'selected') { - $oldAccess = $form->getAccess(); - $this->formsService->notifyNewShares($form, $oldAccess, $keyValuePairs['access']); - } - } - // Create FormEntity with given Params & Id. $form = Form::fromParams($keyValuePairs); $form->setId($id); diff --git a/lib/Controller/ShareApiController.php b/lib/Controller/ShareApiController.php new file mode 100644 index 000000000..a1ebda003 --- /dev/null +++ b/lib/Controller/ShareApiController.php @@ -0,0 +1,200 @@ + + * + * @author Jonas Rittershofer + * + * @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\Forms\Controller; + +use OCA\Forms\Constants; +use OCA\Forms\Db\Form; +use OCA\Forms\Db\FormMapper; +use OCA\Forms\Db\Share; +use OCA\Forms\Db\ShareMapper; +use OCA\Forms\Service\FormsService; + +use OCP\AppFramework\OCSController; +use OCP\AppFramework\Db\IMapperException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\ILogger; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; + +class ShareApiController extends OCSController { + protected $appName; + + /** @var FormMapper */ + private $formMapper; + + /** @var ShareMapper */ + private $shareMapper; + + /** @var FormsService */ + private $formsService; + + /** @var IGroupManager */ + private $groupManager; + + /** @var ILogger */ + private $logger; + + /** @var IUserManager */ + private $userManager; + + /** @var IUser */ + private $currentUser; + + public function __construct(string $appName, + FormMapper $formMapper, + ShareMapper $shareMapper, + FormsService $formsService, + IGroupManager $groupManager, + ILogger $logger, + IRequest $request, + IUserManager $userManager, + IUserSession $userSession) { + parent::__construct($appName, $request); + $this->appName = $appName; + $this->formMapper = $formMapper; + $this->shareMapper = $shareMapper; + $this->formsService = $formsService; + $this->groupManager = $groupManager; + $this->logger = $logger; + $this->userManager = $userManager; + + $this->currentUser = $userSession->getUser(); + } + + /** + * @NoAdminRequired + * + * Add a new share + * + * @param int $formId The form to share + * @param int $shareType Nextcloud-ShareType + * @param string $shareWith ID of user/group/... to share with + * @return DataResponse + * @throws OCSBadRequestException + * @throws OCSForbiddenException + */ + public function newShare(int $formId, int $shareType, string $shareWith): DataResponse { + $this->logger->debug('Adding new share: formId: {formId}, shareType: {shareType}, shareWith: {shareWith}', [ + 'formId' => $formId, + 'shareType' => $shareType, + 'shareWith' => $shareWith, + ]); + + // Only accept usable shareTypes + if (array_search($shareType, Constants::SHARE_TYPES_USED) === false) { + $this->logger->debug('Invalid shareType'); + throw new OCSBadRequestException('Invalid shareType'); + } + + try { + $form = $this->formMapper->findById($formId); + } catch (IMapperException $e) { + $this->logger->debug('Could not find form', ['exception' => $e]); + throw new OCSBadRequestException('Could not find form'); + } + + // Check for permission to share form + if ($form->getOwnerId() !== $this->currentUser->getUID()) { + $this->logger->debug('This form is not owned by the current user'); + throw new OCSForbiddenException(); + } + + // Check for valid shareWith, needs to be done separately per shareType + switch ($shareType) { + case IShare::TYPE_USER: + if (!($this->userManager->get($shareWith) instanceof IUser)) { + $this->logger->debug('Invalid user to share with.'); + throw new OCSBadRequestException('Invalid user to share with.'); + } + break; + + case IShare::TYPE_GROUP: + if (!($this->groupManager->get($shareWith) instanceof IGroup)) { + $this->logger->debug('Invalid group to share with.'); + throw new OCSBadRequestException('Invalid group to share with.'); + } + break; + + default: + // This passed the check for used shareTypes, but has not been found here. + $this->logger->warning('Unknown, but used shareType: {shareType}. Please file an issue on GitHub.', [ 'shareType' => $shareType ]); + throw new OCSException('Unknown shareType.'); + } + + $share = new Share(); + $share->setFormId($formId); + $share->setShareType($shareType); + $share->setShareWith($shareWith); + + $share = $this->shareMapper->insert($share); + + // Append displayName for Frontend + $shareData = $share->read(); + $shareData['displayName'] = $this->formsService->getShareDisplayName($shareData); + + return new DataResponse($shareData); + } + + /** + * @NoAdminRequired + * + * Delete a share + * + * @param int $id of the share to delete + * @return DataResponse + * @throws OCSBadRequestException + * @throws OCSForbiddenException + */ + public function deleteShare(int $id): DataResponse { + $this->logger->debug('Deleting share: {id}', [ + 'id' => $id + ]); + + try { + $share = $this->shareMapper->findById($id); + $form = $this->formMapper->findById($share->getFormId()); + } catch (IMapperException $e) { + $this->logger->debug('Could not find share', ['exception' => $e]); + throw new OCSBadRequestException('Could not find share'); + } + + if ($form->getOwnerId() !== $this->currentUser->getUID()) { + $this->logger->debug('This form is not owned by the current user'); + throw new OCSForbiddenException(); + } + + $this->shareMapper->deleteById($id); + + return new DataResponse($id); + } +} diff --git a/lib/Db/Share.php b/lib/Db/Share.php new file mode 100644 index 000000000..c4861499d --- /dev/null +++ b/lib/Db/Share.php @@ -0,0 +1,64 @@ + + * + * @author Jonas Rittershofer + * + * @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\Forms\Db; + +use OCP\AppFramework\Db\Entity; + +/** + * @method integer getFormId() + * @method void setFormId(integer $value) + * @method integer getShareType() + * @method void setShareType(integer $value) + * @method string getShareWith() + * @method void setShareWith(string $value) + */ +class Share extends Entity { + /** @var int */ + protected $formId; + /** @var int */ + protected $shareType; + /** @var string */ + protected $shareWith; + + /** + * Option constructor. + */ + public function __construct() { + $this->addType('formId', 'integer'); + $this->addType('shareType', 'integer'); + $this->addType('shareWith', 'string'); + } + + public function read(): array { + return [ + 'id' => $this->getId(), + 'formId' => $this->getFormId(), + 'shareType' => $this->getShareType(), + 'shareWith' => $this->getShareWith(), + ]; + } +} diff --git a/lib/Db/ShareMapper.php b/lib/Db/ShareMapper.php new file mode 100644 index 000000000..af8f0bf49 --- /dev/null +++ b/lib/Db/ShareMapper.php @@ -0,0 +1,108 @@ + + * + * @author Jonas Rittershofer + * + * @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\Forms\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class ShareMapper extends QBMapper { + /** + * ShareMapper constructor. + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + parent::__construct($db, 'forms_v2_shares', Share::class); + } + + /** + * Find a Share + * @param int $id + * @return Share + * @throws MultipleObjectsReturnedException if more than one result + * @throws DoesNotExistException if not found + */ + public function findById(int $id): Share { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity($qb); + } + + /** + * Find Shares corresponding to a form. + * @param int $formId + * @return Share[] + */ + public function findByForm(int $formId): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) + ) + ->orderBy('share_type', 'ASC'); //Already order by ShareType + + return $this->findEntities($qb); + } + + /** + * Delete a share + * @param int $id of the share. + */ + public function deleteById(int $id): void { + $qb = $this->db->getQueryBuilder(); + + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + $qb->execute(); + } + + /** + * Delete all Shares of a form. + * @param int $formId + */ + public function deleteByForm(int $formId): void { + $qb = $this->db->getQueryBuilder(); + + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) + ); + $qb->execute(); + } +} diff --git a/lib/Migration/Version030000Date20211206213004.php b/lib/Migration/Version030000Date20211206213004.php new file mode 100644 index 000000000..d541f463d --- /dev/null +++ b/lib/Migration/Version030000Date20211206213004.php @@ -0,0 +1,184 @@ + + * + * @author Jonas Rittershofer + * + * @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\Forms\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\Types; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use OCP\Share\IShare; + +class Version030000Date20211206213004 extends SimpleMigrationStep { + + /** @var IDBConnection */ + protected $connection; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + // Abort if already existing. + if ($schema->hasTable('forms_v2_shares')) { + return null; + } + + // Create table + $table = $schema->createTable('forms_v2_shares'); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('form_id', Types::INTEGER, [ + 'notnull' => true, + ]); + $table->addColumn('share_type', Types::SMALLINT, [ + 'notnull' => true, + ]); + $table->addColumn('share_with', Types::STRING, [ + 'length' => 256, + ]); + + $table->setPrimaryKey(['id']) + ->addIndex(['form_id'], 'forms_shares_form') + ->addIndex(['share_type'], 'forms_shares_type') + ->addIndex(['share_with'], 'forms_shares_with'); + + return $schema; + } + + /** + * Migrate the currently active old sharing to the new sharing. + * -> 'public' gets mapped to: legacy-link + * -> 'registered' mapped to: permit & show to all + * -> 'selected' mapped to: selected shares + * + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $qbFetch = $this->connection->getQueryBuilder(); + $qbUpdateAccess = $this->connection->getQueryBuilder(); + $qbInsertShares = $this->connection->getQueryBuilder(); + + // Prepare Queries + $qbFetch->select('id', 'access_json') + ->from('forms_v2_forms'); + + $qbUpdateAccess->update('forms_v2_forms') + ->set('access_json', $qbUpdateAccess->createParameter('access_json')) + ->where($qbUpdateAccess->expr()->eq('id', $qbUpdateAccess->createParameter('id'))); + + $qbInsertShares->insert('forms_v2_shares') + ->values([ + 'form_id' => $qbInsertShares->createParameter('form_id'), + 'share_type' => $qbInsertShares->createParameter('share_type'), + 'share_with' => $qbInsertShares->createParameter('share_with'), + ]); + + // Fetch Forms... + $cursor = $qbFetch->executeQuery(); + + // ... then handle each existing form and translate its sharing settings. + while ($row = $cursor->fetch()) { + // Decode access to array (param assoc=true) + $access = json_decode($row['access_json'], true); + + // In case there are already migrated forms, just skip. + if (array_key_exists('permitAllUsers', $access)) { + $output->warning('Already migrated form: ' . $row['id'] . ', access: ' . $row['access_json']); + continue; + } + + switch ($access['type']) { + case 'public': + $newAccess = [ + 'permitAllUsers' => false, + 'showToAllUsers' => false, + ]; + $qbUpdateAccess->setParameter('id', $row['id'], IQueryBuilder::PARAM_INT) + ->setParameter('access_json', json_encode($newAccess), IQueryBuilder::PARAM_STR) + ->executeStatement(); + break; + + case 'registered': + $newAccess = [ + 'permitAllUsers' => true, + 'showToAllUsers' => true, + ]; + $qbUpdateAccess->setParameter('id', $row['id'], IQueryBuilder::PARAM_INT) + ->setParameter('access_json', json_encode($newAccess), IQueryBuilder::PARAM_STR) + ->executeStatement(); + break; + + case 'selected': + $newAccess = [ + 'permitAllUsers' => false, + 'showToAllUsers' => false, + ]; + $qbUpdateAccess->setParameter('id', $row['id'], IQueryBuilder::PARAM_INT) + ->setParameter('access_json', json_encode($newAccess), IQueryBuilder::PARAM_STR) + ->executeStatement(); + + // Insert single selected shares. + foreach ($access['users'] as $user) { + $qbInsertShares->setParameter('form_id', $row['id'], IQueryBuilder::PARAM_INT) + ->setParameter('share_type', IShare::TYPE_USER, IQueryBuilder::PARAM_INT) + ->setParameter('share_with', $user, IQueryBuilder::PARAM_STR) + ->executeStatement(); + } + foreach ($access['groups'] as $group) { + $qbInsertShares->setParameter('form_id', $row['id'], IQueryBuilder::PARAM_INT) + ->setParameter('share_type', IShare::TYPE_GROUP, IQueryBuilder::PARAM_INT) + ->setParameter('share_with', $group, IQueryBuilder::PARAM_STR) + ->executeStatement(); + } + break; + + default: + $output->warning('Unknown access property on form. ID: ' . $row['id'] . ', access: ' . $row['access_json']); + } + } + $cursor->closeCursor(); + } +} diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php index 9d90c8aad..2b1324fde 100644 --- a/lib/Service/FormsService.php +++ b/lib/Service/FormsService.php @@ -29,6 +29,7 @@ use OCA\Forms\Db\FormMapper; use OCA\Forms\Db\OptionMapper; use OCA\Forms\Db\QuestionMapper; +use OCA\Forms\Db\ShareMapper; use OCA\Forms\Db\SubmissionMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\IMapperException; @@ -57,6 +58,9 @@ class FormsService { /** @var QuestionMapper */ private $questionMapper; + /** @var ShareMapper */ + private $shareMapper; + /** @var SubmissionMapper */ private $submissionMapper; @@ -76,6 +80,7 @@ public function __construct(ActivityManager $activityManager, FormMapper $formMapper, OptionMapper $optionMapper, QuestionMapper $questionMapper, + ShareMapper $shareMapper, SubmissionMapper $submissionMapper, IGroupManager $groupManager, ILogger $logger, @@ -85,6 +90,7 @@ public function __construct(ActivityManager $activityManager, $this->formMapper = $formMapper; $this->optionMapper = $optionMapper; $this->questionMapper = $questionMapper; + $this->shareMapper = $shareMapper; $this->submissionMapper = $submissionMapper; $this->groupManager = $groupManager; $this->logger = $logger; @@ -135,6 +141,25 @@ public function getQuestions(int $formId): array { } } + /** + * Load shares corresponding to form + * + * @param integer $formId + * @return array + */ + public function getShares(int $formId): array { + $shareList = []; + + $shareEntities = $this->shareMapper->findByForm($formId); + foreach ($shareEntities as $shareEntity) { + $share = $shareEntity->read(); + $share['displayName'] = $this->getShareDisplayName($share); + $shareList[] = $share; + } + + return $shareList; + } + /** * Get a form data * @@ -146,13 +171,7 @@ public function getForm(int $id): array { $form = $this->formMapper->findById($id); $result = $form->read(); $result['questions'] = $this->getQuestions($id); - - // Set proper user/groups properties - // Make sure we have the bare minimum - $result['access'] = array_merge(['users' => [], 'groups' => []], $result['access']); - // Properly format users & groups - $result['access']['users'] = array_map([$this, 'formatUsers'], $result['access']['users']); - $result['access']['groups'] = array_map([$this, 'formatGroups'], $result['access']['groups']); + $result['shares'] = $this->getShares($id); // Append canSubmit, to be able to show proper EmptyContent on internal view. $result['canSubmit'] = $this->canSubmit($form->getId()); @@ -173,6 +192,7 @@ public function getPublicForm(int $id): array { // Remove sensitive data unset($form['access']); unset($form['ownerId']); + unset($form['shares']); return $form; } @@ -232,25 +252,83 @@ public function hasUserAccess(int $formId): bool { return true; } - // Now all remaining users are allowed, if access-type 'registered'. - if ($access['type'] === 'registered') { + // Now all remaining users are allowed, if permitAll is set. + if ($access['permitAllUsers']) { return true; } // Selected Access remains. - // Grant Access, if user is in users-Array. - if (in_array($this->currentUser->getUID(), $access['users'])) { + if ($this->isSharedToUser($formId)) { return true; } - // Check if access granted by group. - foreach ($access['groups'] as $group) { - if ($this->groupManager->isInGroup($this->currentUser->getUID(), $group)) { - return true; + // None of the possible access-options matched. + return false; + } + + /** + * Is the form shown on sidebar to the user. + * + * @param int $formId + * @return bool + */ + public function isSharedFormShown(int $formId): bool { + $form = $this->formMapper->findById($formId); + $access = $form->getAccess(); + + // Dont show here to owner, as its in the owned list anyways. + if ($form->getOwnerId() === $this->currentUser->getUID()) { + return false; + } + + // Dont show expired forms. + if ($this->hasFormExpired($form->getId())) { + return false; + } + + // Shown if permitall and showntoall are both set. + if ($access['permitAllUsers'] && $access['showToAllUsers']) { + return true; + } + + // Shown if user in List of Shared Users/Groups + if ($this->isSharedToUser($formId)) { + return true; + } + + // No Reason found to show form. + return false; + } + + /** + * Checking all selected shares + * + * @param $formId + * @return bool + */ + public function isSharedToUser(int $formId): bool { + $shareEntities = $this->shareMapper->findByForm($formId); + foreach ($shareEntities as $shareEntity) { + $share = $shareEntity->read(); + + // Needs different handling for shareTypes + switch ($share['shareType']) { + case IShare::TYPE_USER: + if ($share['shareWith'] === $this->currentUser->getUID()) { + return true; + } + break; + case IShare::TYPE_GROUP: + if ($this->groupManager->isInGroup($this->currentUser->getUID(), $share['shareWith'])) { + return true; + } + break; + default: + // Return false below } } - // None of the possible access-options matched. + // No share found. return false; } @@ -266,45 +344,32 @@ public function hasFormExpired(int $formId): bool { } /** - * Format users access + * Get DisplayNames to Shares * - * @param string $userId - * @return array + * @param array $share + * @return string */ - private function formatUsers(string $userId): array { + public function getShareDisplayName(array $share): string { $displayName = ''; - $user = $this->userManager->get($userId); - if ($user instanceof IUser) { - $displayName = $user->getDisplayName(); - } - - return [ - 'shareWith' => $userId, - 'displayName' => $displayName, - 'shareType' => IShare::TYPE_USER - ]; - } - - /** - * Format groups access - * - * @param string $groupId - * @return array - */ - private function formatGroups(string $groupId): array { - $displayName = ''; - - $group = $this->groupManager->get($groupId); - if ($group instanceof IGroup) { - $displayName = $group->getDisplayName(); + switch ($share['shareType']) { + case IShare::TYPE_USER: + $user = $this->userManager->get($share['shareWith']); + if ($user instanceof IUser) { + $displayName = $user->getDisplayName(); + } + break; + case IShare::TYPE_GROUP: + $group = $this->groupManager->get($share['shareWith']); + if ($group instanceof IGroup) { + $displayName = $group->getDisplayName(); + } + break; + default: + // Preset Empty. } - return [ - 'shareWith' => $groupId, - 'displayName' => $displayName, - 'shareType' => IShare::TYPE_GROUP - ]; + return $displayName; } /** diff --git a/package-lock.json b/package-lock.json index 6d10acc57..e2e5121ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forms", - "version": "2.5.0", + "version": "3.0.0-alpha.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "forms", - "version": "2.5.0", + "version": "3.0.0-alpha.0", "license": "AGPL-3.0", "dependencies": { "@nextcloud/auth": "^1.3.0", diff --git a/package.json b/package.json index f823ff14c..28872c9df 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "forms", "description": "Forms app for nextcloud", - "version": "2.5.0", + "version": "3.0.0-alpha.0", "repository": { "type": "git", "url": "git+https://github.com/nextcloud/forms.git" diff --git a/src/components/ShareDiv.vue b/src/components/ShareDiv.vue deleted file mode 100644 index 0d4419462..000000000 --- a/src/components/ShareDiv.vue +++ /dev/null @@ -1,394 +0,0 @@ - - - - - - - diff --git a/src/components/SidebarTabs/SettingsSidebarTab.vue b/src/components/SidebarTabs/SettingsSidebarTab.vue new file mode 100644 index 000000000..934e448a8 --- /dev/null +++ b/src/components/SidebarTabs/SettingsSidebarTab.vue @@ -0,0 +1,206 @@ + + + + + + + diff --git a/src/components/SidebarTabs/SharingSearchDiv.vue b/src/components/SidebarTabs/SharingSearchDiv.vue new file mode 100644 index 000000000..de3f85b10 --- /dev/null +++ b/src/components/SidebarTabs/SharingSearchDiv.vue @@ -0,0 +1,314 @@ + + + + + + + diff --git a/src/components/SidebarTabs/SharingShareDiv.vue b/src/components/SidebarTabs/SharingShareDiv.vue new file mode 100644 index 000000000..cd1460109 --- /dev/null +++ b/src/components/SidebarTabs/SharingShareDiv.vue @@ -0,0 +1,94 @@ + + + + + + + diff --git a/src/components/SidebarTabs/SharingSidebarTab.vue b/src/components/SidebarTabs/SharingSidebarTab.vue new file mode 100644 index 000000000..066c73b22 --- /dev/null +++ b/src/components/SidebarTabs/SharingSidebarTab.vue @@ -0,0 +1,241 @@ + + + + + + + diff --git a/src/components/UserDiv.vue b/src/components/UserDiv.vue deleted file mode 100644 index f00950a20..000000000 --- a/src/components/UserDiv.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - diff --git a/src/mixins/ShareTypes.js b/src/mixins/ShareTypes.js index 18b793f54..e9c8eb6d2 100644 --- a/src/mixins/ShareTypes.js +++ b/src/mixins/ShareTypes.js @@ -34,6 +34,44 @@ export default { SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP, SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM, }, + + /** + * !!! Keep in Sync with lib/Constants.php !! + */ + SHARE_TYPES_USED: [ + OC.Share.SHARE_TYPE_USER, + OC.Share.SHARE_TYPE_GROUP, + ], } }, + + methods: { + /** + * Get the icon based on the share type + * Default share is a user, other icons are here to differenciate from it, so let's not display the user icon. + * + * @param {number} type the share type + * @return {string} the icon class + */ + shareTypeToIcon(type) { + switch (type) { + case this.SHARE_TYPES.SHARE_TYPE_GUEST: + // case this.SHARE_TYPES.SHARE_TYPE_REMOTE: + // case this.SHARE_TYPES.SHARE_TYPE_USER: + return 'icon-user' + case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP: + case this.SHARE_TYPES.SHARE_TYPE_GROUP: + return 'icon-group' + case this.SHARE_TYPES.SHARE_TYPE_EMAIL: + return 'icon-mail' + case this.SHARE_TYPES.SHARE_TYPE_CIRCLE: + return 'icon-circle' + case this.SHARE_TYPES.SHARE_TYPE_ROOM: + return 'icon-room' + + default: + return '' + } + }, + }, } diff --git a/src/views/Sidebar.vue b/src/views/Sidebar.vue index d220121d8..8f39337f1 100644 --- a/src/views/Sidebar.vue +++ b/src/views/Sidebar.vue @@ -22,115 +22,34 @@ From e2e1f0b8808a3018d56e0bcf2fbebb2f72fd4eab Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Sat, 18 Dec 2021 17:56:04 +0100 Subject: [PATCH 3/8] Add legacy link, new public link sharing, fix internal view Signed-off-by: Jonas Rittershofer --- appinfo/routes.php | 11 ++- lib/Constants.php | 3 +- lib/Controller/PageController.php | 67 +++++++++++--- lib/Controller/ShareApiController.php | 37 +++++++- lib/Db/ShareMapper.php | 23 +++++ .../Version030000Date20211206213004.php | 1 + lib/Service/FormsService.php | 33 ++++++- .../SidebarTabs/SettingsSidebarTab.vue | 5 +- .../SidebarTabs/SharingSidebarTab.vue | 92 ++++++++++++++++++- src/mixins/ShareLinkMixin.js | 31 ++++++- src/mixins/ShareTypes.js | 1 + 11 files changed, 269 insertions(+), 35 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 0c5bbb03b..b765cb3ba 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -25,15 +25,22 @@ return [ 'routes' => [ + // Public Share Link + [ + 'name' => 'page#public_link_view', + 'url' => '/s/{hash}', + 'verb' => 'GET' + + ], // Internal views [ 'name' => 'page#views', 'url' => '/{hash}/{view}', 'verb' => 'GET' ], - // Share-Link & public submit + // Internal Form Link [ - 'name' => 'page#goto_form', + 'name' => 'page#internal_link_view', 'url' => '/{hash}', 'verb' => 'GET' ], diff --git a/lib/Constants.php b/lib/Constants.php index 51ee48282..8447a4e02 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -90,6 +90,7 @@ class Constants { */ public const SHARE_TYPES_USED = [ IShare::TYPE_USER, - IShare::TYPE_GROUP + IShare::TYPE_GROUP, + IShare::TYPE_LINK ]; } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 06c375a57..313f4b628 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -29,6 +29,7 @@ use OCA\Forms\Constants; use OCA\Forms\Db\Form; use OCA\Forms\Db\FormMapper; +use OCA\Forms\Db\ShareMapper; use OCA\Forms\Service\FormsService; use OCP\Accounts\IAccountManager; @@ -59,7 +60,10 @@ class PageController extends Controller { /** @var FormMapper */ private $formMapper; - + + /** @var ShareMapper */ + private $shareMapper; + /** @var FormsService */ private $formsService; @@ -93,6 +97,7 @@ class PageController extends Controller { public function __construct(string $appName, IRequest $request, FormMapper $formMapper, + ShareMapper $shareMapper, FormsService $formsService, IAccountManager $accountManager, IGroupManager $groupManager, @@ -107,6 +112,7 @@ public function __construct(string $appName, $this->appName = $appName; $this->formMapper = $formMapper; + $this->shareMapper = $shareMapper; $this->formsService = $formsService; $this->accountManager = $accountManager; @@ -149,29 +155,60 @@ public function views(): TemplateResponse { * @NoCSRFRequired * @PublicPage * @param string $hash - * @return RedirectResponse|TemplateResponse Redirect for logged-in users, public template otherwise. + * @return RedirectResponse|TemplateResponse Redirect to login or internal view. */ - public function gotoForm(string $hash): Response { - // Inject style on all templates - Util::addStyle($this->appName, 'forms'); + public function internalLinkView(string $hash): Response { + $internalView = $this->urlGenerator->linkToRoute('forms.page.views', ['hash' => $hash, 'view' => 'submit']); + + if ($this->userSession->isLoggedIn()) { + // Redirect to internal Submit View + return new RedirectResponse($internalView); + } + // For legacy-support, show public template try { $form = $this->formMapper->findByHash($hash); } catch (DoesNotExistException $e) { return $this->provideTemplate(self::TEMPLATE_NOTFOUND); } + if (isset($form->getAccess()['legacyLink'])) { + // Inject style on all templates + Util::addStyle($this->appName, 'forms'); - // If not link-shared, redirect to internal route - if ($form->getAccess()['type'] !== 'public') { - $internalLink = $this->urlGenerator->linkToRoute('forms.page.views', ['hash' => $hash, 'view' => 'submit']); - - if ($this->userSession->isLoggedIn()) { - // Directly internal view - return new RedirectResponse($internalLink); - } else { - // Internal through login - return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', ['redirect_url' => $internalLink])); + // Has form expired + if ($this->formsService->hasFormExpired($form->getId())) { + return $this->provideTemplate(self::TEMPLATE_EXPIRED, $form); } + + // Public Template to fill the form + Util::addScript($this->appName, 'forms-submit'); + $this->insertHeaderOnIos(); + $this->initialStateService->provideInitialState($this->appName, 'form', $this->formsService->getPublicForm($form->getId())); + $this->initialStateService->provideInitialState($this->appName, 'isLoggedIn', $this->userSession->isLoggedIn()); + $this->initialStateService->provideInitialState($this->appName, 'maxStringLengths', Constants::MAX_STRING_LENGTHS); + return $this->provideTemplate(self::TEMPLATE_MAIN, $form); + } + + // Otherwise Redirect to login (& then internal view) + return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', ['redirect_url' => $internalView])); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @param string $hash Public sharing hash. + * @return TemplateResponse Public template. + */ + public function publicLinkView(string $hash): Response { + // Inject style on all templates + Util::addStyle($this->appName, 'forms'); + + try { + $share = $this->shareMapper->findPublicShareByHash($hash); + $form = $this->formMapper->findById($share->getFormId()); + } catch (DoesNotExistException $e) { + return $this->provideTemplate(self::TEMPLATE_NOTFOUND); } // Has form expired diff --git a/lib/Controller/ShareApiController.php b/lib/Controller/ShareApiController.php index a1ebda003..4b84faf7e 100644 --- a/lib/Controller/ShareApiController.php +++ b/lib/Controller/ShareApiController.php @@ -34,9 +34,11 @@ use OCA\Forms\Service\FormsService; use OCP\AppFramework\OCSController; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\IMapperException; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\IGroup; use OCP\IGroupManager; @@ -45,6 +47,8 @@ use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Share\IShare; class ShareApiController extends OCSController { protected $appName; @@ -70,6 +74,9 @@ class ShareApiController extends OCSController { /** @var IUser */ private $currentUser; + /** @var ISecureRandom */ + private $secureRandom; + public function __construct(string $appName, FormMapper $formMapper, ShareMapper $shareMapper, @@ -78,7 +85,8 @@ public function __construct(string $appName, ILogger $logger, IRequest $request, IUserManager $userManager, - IUserSession $userSession) { + IUserSession $userSession, + ISecureRandom $secureRandom) { parent::__construct($appName, $request); $this->appName = $appName; $this->formMapper = $formMapper; @@ -87,6 +95,7 @@ public function __construct(string $appName, $this->groupManager = $groupManager; $this->logger = $logger; $this->userManager = $userManager; + $this->secureRandom = $secureRandom; $this->currentUser = $userSession->getUser(); } @@ -98,12 +107,12 @@ public function __construct(string $appName, * * @param int $formId The form to share * @param int $shareType Nextcloud-ShareType - * @param string $shareWith ID of user/group/... to share with + * @param string $shareWith ID of user/group/... to share with. For Empty shareWith and shareType Link, this will be set as RandomID. * @return DataResponse * @throws OCSBadRequestException * @throws OCSForbiddenException */ - public function newShare(int $formId, int $shareType, string $shareWith): DataResponse { + public function newShare(int $formId, int $shareType, string $shareWith = ''): DataResponse { $this->logger->debug('Adding new share: formId: {formId}, shareType: {shareType}, shareWith: {shareWith}', [ 'formId' => $formId, 'shareType' => $shareType, @@ -129,6 +138,14 @@ public function newShare(int $formId, int $shareType, string $shareWith): DataRe throw new OCSForbiddenException(); } + // Create public-share hash, if necessary. + if ($shareType === IShare::TYPE_LINK) { + $shareWith = $this->secureRandom->generate( + 24, + ISecureRandom::CHAR_HUMAN_READABLE + ); + } + // Check for valid shareWith, needs to be done separately per shareType switch ($shareType) { case IShare::TYPE_USER: @@ -145,6 +162,20 @@ public function newShare(int $formId, int $shareType, string $shareWith): DataRe } break; + case IShare::TYPE_LINK: + // Check if hash already exists. (Unfortunately not possible here by unique index on db.) + try { + // Try loading a share to the hash. + $nonex = $this->shareMapper->findPublicShareByHash($shareWith); + + // If we come here, a share has been found --> The share hash already exists, thus aborting. + $this->logger->debug('Share Hash already exists.'); + throw new OCSException('Share Hash exists. Please retry.'); + } catch (DoesNotExistException $e) { + // Just continue, this is what we expect to happen (share hash not existing yet). + } + break; + default: // This passed the check for used shareTypes, but has not been found here. $this->logger->warning('Unknown, but used shareType: {shareType}. Please file an issue on GitHub.', [ 'shareType' => $shareType ]); diff --git a/lib/Db/ShareMapper.php b/lib/Db/ShareMapper.php index af8f0bf49..726fbdacc 100644 --- a/lib/Db/ShareMapper.php +++ b/lib/Db/ShareMapper.php @@ -31,6 +31,7 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\Share\IShare; class ShareMapper extends QBMapper { /** @@ -78,6 +79,28 @@ public function findByForm(int $formId): array { return $this->findEntities($qb); } + /** + * Find Public Share by Hash + * @param string $hash + * @return Share + * @throws MultipleObjectsReturnedException if more than one result + * @throws DoesNotExistException if not found + */ + public function findPublicShareByHash(string $hash): Share { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('share_with', $qb->createNamedParameter($hash, IQueryBuilder::PARAM_STR)) + ); + + return $this->findEntity($qb); + } + /** * Delete a share * @param int $id of the share. diff --git a/lib/Migration/Version030000Date20211206213004.php b/lib/Migration/Version030000Date20211206213004.php index d541f463d..2e79e406f 100644 --- a/lib/Migration/Version030000Date20211206213004.php +++ b/lib/Migration/Version030000Date20211206213004.php @@ -133,6 +133,7 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array switch ($access['type']) { case 'public': $newAccess = [ + 'legacyLink' => true, 'permitAllUsers' => false, 'showToAllUsers' => false, ]; diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php index 2b1324fde..e2d6e4ec9 100644 --- a/lib/Service/FormsService.php +++ b/lib/Service/FormsService.php @@ -202,10 +202,9 @@ public function getPublicForm(int $id): array { */ public function canSubmit($formId) { $form = $this->formMapper->findById($formId); - $access = $form->getAccess(); - // We cannot control how many time users can submit in public mode - if ($access['type'] === 'public') { + // We cannot control how many time users can submit if public link / legacyLink available + if ($this->hasPublicLink($formId)) { return true; } @@ -227,6 +226,30 @@ public function canSubmit($formId) { return true; } + /** + * Searching Shares for public link + * + * @param integer $formId + * @return boolean + */ + public function hasPublicLink($formId): bool { + $form = $this->formMapper->findById($formId); + $access = $form->getAccess(); + + if (isset($access['legacyLink'])) { + return true; + } + + $shareEntities = $this->shareMapper->findByForm($form->getId()); + foreach ($shareEntities as $shareEntity) { + if ($shareEntity->getShareType() === IShare::TYPE_LINK) { + return true; + } + } + + return false; + } + /** * Check if user has access to this form * @@ -238,10 +261,10 @@ public function hasUserAccess(int $formId): bool { $access = $form->getAccess(); $ownerId = $form->getOwnerId(); - if ($access['type'] === 'public') { + if ($this->hasPublicLink($formId)) { return true; } - + // Refuse access, if not public and no user logged in. if (!$this->currentUser) { return false; diff --git a/src/components/SidebarTabs/SettingsSidebarTab.vue b/src/components/SidebarTabs/SettingsSidebarTab.vue index 934e448a8..5e92643d5 100644 --- a/src/components/SidebarTabs/SettingsSidebarTab.vue +++ b/src/components/SidebarTabs/SettingsSidebarTab.vue @@ -62,6 +62,7 @@ import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch' import DatetimePicker from '@nextcloud/vue/dist/Components/DatetimePicker' import moment from '@nextcloud/moment' +import ShareTypes from '../../mixins/ShareTypes.js' export default { components: { @@ -69,6 +70,8 @@ export default { DatetimePicker, }, + mixins: [ShareTypes], + props: { form: { type: Object, @@ -90,7 +93,7 @@ export default { * Submit Multiple is disabled, if it cannot be controlled. */ disableSubmitMultiple() { - return this.hasPublicLink || this.form.isAnonymous + return this.hasPublicLink || this.form.access.legacyLink || this.form.isAnonymous }, disableSubmitMultipleExplanation() { if (this.disableSubmitMultiple) { diff --git a/src/components/SidebarTabs/SharingSidebarTab.vue b/src/components/SidebarTabs/SharingSidebarTab.vue index 066c73b22..c94296950 100644 --- a/src/components/SidebarTabs/SharingSidebarTab.vue +++ b/src/components/SidebarTabs/SharingSidebarTab.vue @@ -35,13 +35,59 @@ {{ t('forms', 'Only works for logged in users who can access this form') }} - + {{ t('forms', 'Copy to clipboard') }} - + +