diff options
author | René Gieling <github@dartcafe.de> | 2020-01-28 21:26:42 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-28 21:26:42 +0300 |
commit | ff0571f7ffb5c5098233890609a8024deddb84db (patch) | |
tree | cd37430594905a7fb7955cd8604a98a1034abcc7 | |
parent | ee078a071ce8af18d59e2034b66ee383c4bb67eb (diff) | |
parent | 0bd70b482a01de419c13cf4b4aca2645aab1e93a (diff) |
Merge pull request #791 from nextcloud/fixAcl
Optimization access to shared polls
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | lib/Controller/PageController.php | 10 | ||||
-rw-r--r-- | lib/Controller/PollController.php | 28 | ||||
-rw-r--r-- | lib/Db/VoteMapper.php | 20 | ||||
-rw-r--r-- | lib/Model/Acl.php | 39 | ||||
-rw-r--r-- | src/js/App.vue | 21 | ||||
-rw-r--r-- | src/js/components/Navigation/Navigation.vue | 9 | ||||
-rw-r--r-- | src/js/router.js | 11 | ||||
-rw-r--r-- | src/js/store/modules/poll.js | 6 | ||||
-rw-r--r-- | src/js/store/modules/polls.js | 3 | ||||
-rw-r--r-- | src/js/views/NotFound.vue | 44 | ||||
-rw-r--r-- | src/js/views/PollList.vue | 3 | ||||
-rw-r--r-- | src/js/views/PublicVote.vue | 172 | ||||
-rw-r--r-- | src/js/views/Vote.vue | 36 |
14 files changed, 185 insertions, 218 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 47b5c6d9..f6533215 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -24,6 +24,7 @@ return [ 'routes' => [ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], + ['name' => 'page#index', 'url' => '/not-found', 'verb' => 'GET', 'postfix' => 'notfound'], ['name' => 'page#index', 'url' => '/list/{id}', 'verb' => 'GET', 'postfix' => 'list'], ['name' => 'page#index', 'url' => '/vote/{id}', 'verb' => 'GET', 'postfix' => 'vote'], ['name' => 'page#vote_public', 'url' => '/s/{token}', 'verb' => 'GET', 'postfix' => 'public'], diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 82932a44..71e0a7cd 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -71,8 +71,14 @@ class PageController extends Controller { * @return PublicTemplateResponse */ public function votePublic() { - return new PublicTemplateResponse('polls', 'polls.tmpl', [ - 'urlGenerator' => $this->urlGenerator]); + if (\OC::$server->getUserSession()->isLoggedIn()) { + return new TemplateResponse('polls', 'polls.tmpl', [ + 'urlGenerator' => $this->urlGenerator]); + } else { + return new PublicTemplateResponse('polls', 'polls.tmpl', [ + 'urlGenerator' => $this->urlGenerator]); + } + } } diff --git a/lib/Controller/PollController.php b/lib/Controller/PollController.php index c3805ecf..99917de3 100644 --- a/lib/Controller/PollController.php +++ b/lib/Controller/PollController.php @@ -115,14 +115,28 @@ class PollController extends Controller { public function list() { if (\OC::$server->getUserSession()->isLoggedIn()) { try { - $polls = array_values(array_filter($this->pollMapper->findAll(), function($item) { - return $this->acl->setPollId($item->getId())->getAllowView(); - })); - return new DataResponse($polls, Http::STATUS_OK); + // $polls = array_values(array_filter($this->pollMapper->findAll(), function($item) { + // return $this->acl->setPollId($item->getId())->getAllowView(); + // })); + + $polls = $this->pollMapper->findAll(); + + foreach ($polls as $poll) { + $combinedPoll = (object) array_merge( + (array) json_decode(json_encode($poll)), (array) json_decode(json_encode($this->acl->setPollId($poll->getId())))); + if ($combinedPoll->allowView) { + $pollList[] = $combinedPoll; + } + } + + return new DataResponse($pollList, Http::STATUS_OK); } catch (DoesNotExistException $e) { return new DataResponse($e, Http::STATUS_NOT_FOUND); } + } else { + return new DataResponse([], Http::STATUS_OK); } + } /** @@ -139,7 +153,9 @@ class PollController extends Controller { $this->acl->setPollId($pollId); } $this->poll = $this->pollMapper->find($pollId); - + if (!$this->acl->getAllowView()) { + return new DataResponse(null, Http::STATUS_UNAUTHORIZED); + } return new DataResponse([ 'poll' => $this->poll, 'acl' => $this->acl @@ -147,7 +163,7 @@ class PollController extends Controller { } catch (DoesNotExistException $e) { $this->logger->info('Poll ' . $pollId . ' not found!', ['app' => 'polls']); - return new DataResponse($e, Http::STATUS_NOT_FOUND); + return new DataResponse(null, Http::STATUS_NOT_FOUND); } } diff --git a/lib/Db/VoteMapper.php b/lib/Db/VoteMapper.php index 2e759d8c..244eaf26 100644 --- a/lib/Db/VoteMapper.php +++ b/lib/Db/VoteMapper.php @@ -98,6 +98,26 @@ class VoteMapper extends QBMapper { /** * @param int $pollId + * @throws \OCP\AppFramework\Db\DoesNotExistException if not found + * @return array + */ + public function findParticipantsVotes($pollId, $userId) { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + ); + + return $this->findEntities($qb); + } + + /** + * @param int $pollId * @param string $userId */ public function deleteByPollAndUser($pollId, $userId) { diff --git a/lib/Model/Acl.php b/lib/Model/Acl.php index 949bbbef..68096490 100644 --- a/lib/Model/Acl.php +++ b/lib/Model/Acl.php @@ -33,6 +33,7 @@ use OCP\ILogger; use OCA\Polls\Db\Poll; use OCA\Polls\Db\Share; use OCA\Polls\Db\PollMapper; +use OCA\Polls\Db\VoteMapper; use OCA\Polls\Db\ShareMapper; /** @@ -80,6 +81,7 @@ class Acl implements JsonSerializable { * @param ILogger $logger * @param IGroupManager $groupManager * @param PollMapper $pollMapper + * @param VoteMapper $voteMapper * @param ShareMapper $shareMapper * @param Poll $pollMapper * @@ -89,6 +91,7 @@ class Acl implements JsonSerializable { ILogger $logger, IGroupManager $groupManager, PollMapper $pollMapper, + VoteMapper $voteMapper, ShareMapper $shareMapper, Poll $poll ) { @@ -96,6 +99,7 @@ class Acl implements JsonSerializable { $this->logger = $logger; $this->groupManager = $groupManager; $this->pollMapper = $pollMapper; + $this->voteMapper = $voteMapper; $this->shareMapper = $shareMapper; $this->poll = $poll; } @@ -177,10 +181,13 @@ class Acl implements JsonSerializable { public function getAllowView(): bool { return ( $this->getIsOwner() - || $this->getIsAdmin() + || $this->getUserHasVoted() + || ($this->getIsAdmin() && $this->poll->getAdminAccess()) || ($this->getGroupShare() && !$this->poll->getDeleted()) || ($this->getPersonalShare() && !$this->poll->getDeleted()) - || $this->poll->getAccess() !== 'hidden' + || ($this->getPublicShare() && !$this->poll->getDeleted()) + || ($this->poll->getAccess() !== 'hidden' && !$this->getPublicShare()) + ); } @@ -202,6 +209,16 @@ class Acl implements JsonSerializable { * @NoAdminRequired * @return bool */ + public function getUserHasVoted(): bool { + return count( + $this->voteMapper->findParticipantsVotes($this->getPollId(), $this->getUserId()) + ); + } + + /** + * @NoAdminRequired + * @return bool + */ public function getPersonalShare(): bool { return count( @@ -217,6 +234,21 @@ class Acl implements JsonSerializable { * @NoAdminRequired * @return bool */ + public function getPublicShare(): bool { + + return count( + array_filter($this->shareMapper->findByPoll($this->getPollId()), function($item) { + if ($item->getType() === 'public' && $item->getToken() === $this->getToken()) { + return true; + } + }) + ); + } + + /** + * @NoAdminRequired + * @return bool + */ public function getExpired(): bool { return ( $this->poll->getExpire() > 0 @@ -233,6 +265,7 @@ class Acl implements JsonSerializable { ($this->getAllowView() || $this->getFoundByToken()) && !$this->getExpired() && !$this->poll->getDeleted() + && $this->userId ) { return true; @@ -366,8 +399,10 @@ class Acl implements JsonSerializable { 'allowEdit' => $this->getAllowEdit(), 'allowSeeUsernames' => $this->getAllowSeeUsernames(), 'allowSeeAllVotes' => $this->getAllowSeeAllVotes(), + 'userHasVoted' => $this->getUserHasVoted(), 'groupShare' => $this->getGroupShare(), 'personalShare' => $this->getPersonalShare(), + 'publicShare' => $this->getPublicShare(), 'foundByToken' => $this->getFoundByToken(), 'accessLevel' => $this->getAccessLevel() ]; diff --git a/src/js/App.vue b/src/js/App.vue index ec588070..cca48a0e 100644 --- a/src/js/App.vue +++ b/src/js/App.vue @@ -22,7 +22,7 @@ <template> <div id="app-polls"> - <Navigation v-if="loadNavigation" /> + <Navigation v-if="OC.currentUser" /> <router-view /> </div> </template> @@ -34,25 +34,8 @@ export default { name: 'App', components: { Navigation - }, - - data() { - return { - loadNavigation: false - } - }, - - computed: { - isPublic() { - return (this.$route.name === 'publicVote') - } - }, - - watch: { - $route() { - this.loadNavigation = (this.$route.name !== 'publicVote') - } } + } </script> diff --git a/src/js/components/Navigation/Navigation.vue b/src/js/components/Navigation/Navigation.vue index be07f1d3..9bd9c5af 100644 --- a/src/js/components/Navigation/Navigation.vue +++ b/src/js/components/Navigation/Navigation.vue @@ -41,6 +41,14 @@ </ul> </AppNavigationItem> + <AppNavigationItem :title="t('polls', 'Participated')" :allow-collapse="true" + icon="icon-user" :to="{ name: 'list', params: {type: 'participated'}}" :open="false"> + <ul> + <PollNavigationItems v-for="(poll) in participatedPolls" :key="poll.id" :poll="poll" + @switchDeleted="switchDeleted(poll.id)" @clonePoll="clonePoll(poll.id)" /> + </ul> + </AppNavigationItem> + <AppNavigationItem :title="t('polls', 'Public polls')" :allow-collapse="true" icon="icon-link" :to="{ name: 'list', params: {type: 'public'}}" :open="false"> <ul> @@ -97,6 +105,7 @@ export default { 'myPolls', 'publicPolls', 'hiddenPolls', + 'participatedPolls', 'deletedPolls' ]), diff --git a/src/js/router.js b/src/js/router.js index d11cbc71..de609ba8 100644 --- a/src/js/router.js +++ b/src/js/router.js @@ -29,7 +29,7 @@ import { generateUrl } from '@nextcloud/router' // Dynamic loading const List = () => import('./views/PollList') const Vote = () => import('./views/Vote') -const PublicVote = () => import('./views/PublicVote') +const NotFound = () => import('./views/NotFound') Vue.use(Router) @@ -56,6 +56,13 @@ export default new Router({ name: 'list' }, { + path: '/not-found', + components: { + default: NotFound + }, + name: 'notfound' + }, + { path: '/vote/:id', components: { default: Vote @@ -70,7 +77,7 @@ export default new Router({ { path: '/s/:token', components: { - default: PublicVote + default: Vote }, props: true, name: 'publicVote' diff --git a/src/js/store/modules/poll.js b/src/js/store/modules/poll.js index 384738f4..65edae5c 100644 --- a/src/js/store/modules/poll.js +++ b/src/js/store/modules/poll.js @@ -100,9 +100,11 @@ const actions = { .then((response) => { context.commit('setPoll', { poll: response.data.poll }) context.commit('acl/setAcl', { acl: response.data.acl }) + return response }, (error) => { - if (error.response.status !== '404') { - console.error('Error loading poll', { error: error.response }, { payload: payload }) + if (error.response.status !== '404' && error.response.status !== '401') { + console.debug('Error loading poll', { error: error.response }, { payload: payload }) + return error.response } throw error }) diff --git a/src/js/store/modules/polls.js b/src/js/store/modules/polls.js index 9618d44d..fd91da7d 100644 --- a/src/js/store/modules/polls.js +++ b/src/js/store/modules/polls.js @@ -51,6 +51,9 @@ const getters = { }, deletedPolls: (state) => { return state.list.filter(poll => (poll.deleted)) + }, + participatedPolls: (state) => { + return state.list.filter(poll => (poll.userHasVoted)) } } diff --git a/src/js/views/NotFound.vue b/src/js/views/NotFound.vue new file mode 100644 index 00000000..60a4d899 --- /dev/null +++ b/src/js/views/NotFound.vue @@ -0,0 +1,44 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - 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 <http://www.gnu.org/licenses/>. + - + --> + +<template> + <AppContent> + <div id="emptycontent"> + <div id="emptycontent-icon" class="icon-search" /> + <h2>{{ t('polls', 'The poll does not exist') }}</h2> + <p class="emptycontent-additional"> + {{ t('polls', 'Enter a poll or start a new one.') }} + </p> + </div> + </AppContent> +</template> + +<script> +import { AppContent } from '@nextcloud/vue' + +export default { + name: 'NotFound', + components: { + AppContent + } +} +</script> diff --git a/src/js/views/PollList.vue b/src/js/views/PollList.vue index 9bba0b68..91640262 100644 --- a/src/js/views/PollList.vue +++ b/src/js/views/PollList.vue @@ -75,6 +75,7 @@ export default { 'myPolls', 'publicPolls', 'hiddenPolls', + 'participatedPolls', 'deletedPolls' ]), @@ -87,6 +88,8 @@ export default { return this.hiddenPolls } else if (this.$route.params.type === 'deleted') { return this.deletedPolls + } else if (this.$route.params.type === 'participated') { + return this.participatedPolls } else { return this.allPolls } diff --git a/src/js/views/PublicVote.vue b/src/js/views/PublicVote.vue deleted file mode 100644 index fcc0b448..00000000 --- a/src/js/views/PublicVote.vue +++ /dev/null @@ -1,172 +0,0 @@ -<!-- - - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> - - - - @author René Gieling <github@dartcafe.de> - - - - @license GNU AGPL version 3 or any later version - - - - 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 <http://www.gnu.org/licenses/>. - - - --> - -<template> - <AppContent> - <div v-if="poll.id > 0" v-show="!isLoading" class="main-container"> - <div class="header-actions"> - <button class="button btn primary" @click="tableMode = !tableMode"> - <span>{{ t('polls', 'Switch view') }}</span> - </button> - <a href="#" class="icon icon-settings active" - :title="t('polls', 'Open Sidebar')" @click="toggleSideBar()" /> - </div> - <PollTitle /> - <PollInformation /> - <VoteHeaderPublic v-if="!acl.loggedIn" /> - <PollDescription /> - <VoteList v-show="!tableMode" /> - <VoteTable v-show="tableMode" /> - - <div class="additional"> - <ParticipantsList v-if="acl.allowSeeUsernames" /> - <!-- <Comments /> --> - </div> - </div> - - <SideBar v-if="sideBarOpen" @closeSideBar="toggleSideBar" /> - <LoadingOverlay v-if="isLoading" /> - </AppContent> -</template> - -<script> -// import Comments from '../components/Comments/Comments' -import { AppContent } from '@nextcloud/vue' -import ParticipantsList from '../components/Base/ParticipantsList' -import PollDescription from '../components/Base/PollDescription' -import PollInformation from '../components/Base/PollInformation' -import PollTitle from '../components/Base/PollTitle' -import LoadingOverlay from '../components/Base/LoadingOverlay' -import VoteHeaderPublic from '../components/VoteTable/VoteHeaderPublic' -import SideBar from '../components/SideBar/SideBar' -import VoteList from '../components/VoteTable/VoteList' -import VoteTable from '../components/VoteTable/VoteTable' -import { mapState } from 'vuex' - -export default { - name: 'Vote', - components: { - AppContent, - ParticipantsList, - PollDescription, - PollInformation, - PollTitle, - LoadingOverlay, - VoteHeaderPublic, - // Comments, - SideBar, - VoteTable, - VoteList - }, - - data() { - return { - voteSaved: false, - delay: 50, - sideBarOpen: false, - isLoading: false, - initialTab: 'comments', - tableMode: true - } - }, - - computed: { - ...mapState({ - poll: state => state.poll, - acl: state => state.acl - }), - - windowTitle: function() { - return t('polls', 'Polls') + ' - ' + this.poll.title - } - - }, - - watch: { - $route() { - this.loadPoll() - } - }, - - mounted() { - this.loadPoll() - }, - - methods: { - loadPoll() { - this.isLoading = true - this.$store.dispatch('loadPollMain', { token: this.$route.params.token }) - .then(() => { - this.$store.dispatch('loadPoll', { token: this.$route.params.token }) - .then(() => { - this.isLoading = false - }) - }) - .catch((error) => { - console.error(error) - this.isLoading = false - }) - }, - - toggleSideBar() { - this.sideBarOpen = !this.sideBarOpen - } - - } -} -</script> - -<style lang="scss" scoped> -.additional { - display: flex; - flex-wrap: wrap; - .participants { - flex: 1; - } - .comments { - flex: 3; - } -} - -.main-container { - position: relative; - flex: 1; - padding: 8px 24px; - margin: 0; - flex-direction: column; - flex-wrap: nowrap; - overflow-x: scroll; -} - -.header-actions { - right: 0; - position: absolute; - display: flex; -} - -.icon.icon-settings.active { - display: block; - width: 44px; - height: 44px; -} - -</style> diff --git a/src/js/views/Vote.vue b/src/js/views/Vote.vue index 6c98e636..8cd15a92 100644 --- a/src/js/views/Vote.vue +++ b/src/js/views/Vote.vue @@ -32,17 +32,21 @@ </div> <PollTitle /> <PollInformation /> + <VoteHeaderPublic v-if="!OC.currentUser" /> <PollDescription /> <VoteList v-show="!tableMode && options.list.length" /> <VoteTable v-show="tableMode && options.list.length" /> <div v-if="!options.list.length" class="emptycontent"> <div class="icon-toggle-filelist" /> - <button @click="openOptions"> + <button v-if="acl.getAllowEdit" @click="openOptions"> {{ t('polls', 'There are no vote options, add some in the options section of the right side bar.') }} </button> + <div v-if="!acl.getAllowEdit"> + {{ t('polls', 'There are no vote options. Maybe the owner did not provide some until now.') }} + </div> </div> - <Subscription /> + <Subscription v-if="OC.currentUser" /> <div class="additional"> <ParticipantsList v-if="acl.allowSeeUsernames" /> </div> @@ -61,6 +65,7 @@ import PollDescription from '../components/Base/PollDescription' import PollInformation from '../components/Base/PollInformation' import PollTitle from '../components/Base/PollTitle' import LoadingOverlay from '../components/Base/LoadingOverlay' +import VoteHeaderPublic from '../components/VoteTable/VoteHeaderPublic' import SideBar from '../components/SideBar/SideBar' import VoteList from '../components/VoteTable/VoteList' import VoteTable from '../components/VoteTable/VoteTable' @@ -76,6 +81,7 @@ export default { PollInformation, PollTitle, LoadingOverlay, + VoteHeaderPublic, SideBar, VoteTable, VoteList @@ -85,10 +91,9 @@ export default { return { voteSaved: false, delay: 50, - sideBarOpen: false, + sideBarOpen: true, isLoading: false, initialTab: 'comments', - newName: '', tableMode: true, activeTab: t('polls', 'Comments').toLowerCase() } @@ -130,19 +135,24 @@ export default { loadPoll() { this.isLoading = true - this.$store.dispatch({ type: 'loadPollMain', pollId: this.$route.params.id }) - .then(() => { - this.$store.dispatch({ type: 'loadPoll', pollId: this.$route.params.id }) - .then(() => { - if (this.acl.allowEdit && moment.unix(this.poll.created).diff() > -10000) { - this.openConfiguration() - } - this.isLoading = false - }) + this.$store.dispatch({ type: 'loadPollMain', pollId: this.$route.params.id, token: this.$route.params.token }) + .then((response) => { + if (response.status === 200) { + this.$store.dispatch({ type: 'loadPoll', pollId: this.$route.params.id, token: this.$route.params.token }) + .then(() => { + if (this.acl.allowEdit && moment.unix(this.poll.created).diff() > -10000) { + this.openConfiguration() + } + this.isLoading = false + }) + } else { + this.$router.replace({ name: 'notfound' }) + } }) .catch((error) => { console.error(error) this.isLoading = false + this.$router.replace({ name: 'notfound' }) }) }, |