diff --git a/appinfo/routes.php b/appinfo/routes.php index afdb06ba3..5d1299e5b 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -55,6 +55,7 @@ ['name' => 'poll#list', 'url' => '/polls/list/', 'verb' => 'GET'], ['name' => 'poll#get', 'url' => '/polls/get/{pollId}', 'verb' => 'GET'], ['name' => 'poll#delete', 'url' => '/polls/delete/{pollId}', 'verb' => 'GET'], + ['name' => 'poll#deletePermanently', 'url' => '/polls/delete/permanent/{pollId}', 'verb' => 'GET'], ['name' => 'poll#write', 'url' => '/polls/write/', 'verb' => 'POST'], ['name' => 'poll#clone', 'url' => '/polls/clone/{pollId}', 'verb' => 'get'], ['name' => 'poll#getByToken', 'url' => '/polls/get/s/{token}', 'verb' => 'GET'], diff --git a/lib/Controller/PollController.php b/lib/Controller/PollController.php index 84203ae80..c0a14365f 100644 --- a/lib/Controller/PollController.php +++ b/lib/Controller/PollController.php @@ -223,6 +223,38 @@ public function delete($pollId) { } } + /** + * deletePermanently + * @NoAdminRequired + * @param Array $poll + * @return DataResponse + */ + + public function deletePermanently($pollId) { + + try { + // Find existing poll + $this->poll = $this->pollMapper->find($pollId); + $this->acl->setPollId($this->poll->getId()); + + if (!$this->acl->getAllowEdit()) { + $this->logger->alert('Unauthorized delete attempt from user ' . $this->userId); + return new DataResponse(['message' => 'Unauthorized write attempt.'], Http::STATUS_UNAUTHORIZED); + } + + if (!$this->poll->getDeleted()) { + $this->logger->alert('user ' . $this->userId . ' trying to permanently delete active poll'); + return new DataResponse(['message' => 'Permanent deletion of active poll.'], Http::STATUS_CONFLICT); + } + + $this->pollMapper->delete($this->poll); + return new DataResponse([], Http::STATUS_OK); + + } catch (Exception $e) { + return new DataResponse($e, Http::STATUS_NOT_FOUND); + } + } + /** * write * @NoAdminRequired diff --git a/lib/Migration/Version0104Date20200205104800.php b/lib/Migration/Version0104Date20200205104800.php new file mode 100644 index 000000000..ecc002809 --- /dev/null +++ b/lib/Migration/Version0104Date20200205104800.php @@ -0,0 +1,118 @@ + + * + * @author René Gieling + * + * @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 . + * + */ + +namespace OCA\Polls\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +/** + * Installation class for the polls app. + * Initial db creation + */ +class Version0104Date20200205104800 extends SimpleMigrationStep { + + /** @var IDBConnection */ + protected $connection; + + /** @var IConfig */ + protected $config; + + /** @var array */ + protected $childTables = [ + 'polls_comments', + 'polls_log', + 'polls_notif', + 'polls_options', + 'polls_share', + 'polls_votes', + ]; + + /** + * @param IDBConnection $connection + * @param IConfig $config + */ + public function __construct(IDBConnection $connection, IConfig $config) { + $this->connection = $connection; + $this->config = $config; + } + + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null + * @since 13.0.0 + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + // delete all orphaned entries by selecting all rows + // those poll_ids are not present in the polls table + // + // we have to use a raw query, because NOT EXISTS is not + // part of doctrine's expression builder + // + // get table prefix, as we are running a raw query + $prefix = $this->config->getSystemValue('dbtableprefix', 'oc_'); + // check for orphaned entries in all tables referencing + // the main polls table + foreach($this->childTables as $tbl) { + $child = "$prefix$tbl"; + $query = "DELETE + FROM $child + WHERE NOT EXISTS ( + SELECT NULL + FROM {$prefix}polls_polls polls + WHERE polls.id = {$child}.poll_id + )"; + $stmt = $this->connection->prepare($query); + $stmt->execute(); + } + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + // add an on delete fk contraint to all tables referencing the main polls table + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $eventTable = $schema->getTable('polls_polls'); + foreach($this->childTables as $tbl) { + $table = $schema->getTable($tbl); + + $table->addForeignKeyConstraint($eventTable, ['poll_id'], ['id'], ['onDelete' => 'CASCADE']); + } + + return $schema; + } +} diff --git a/src/js/components/Navigation/Navigation.vue b/src/js/components/Navigation/Navigation.vue index 95d32a6d0..0bd476830 100644 --- a/src/js/components/Navigation/Navigation.vue +++ b/src/js/components/Navigation/Navigation.vue @@ -29,7 +29,8 @@ icon="icon-folder" :to="{ name: 'list', params: {type: 'all'}}" :open="true"> @@ -37,7 +38,8 @@ icon="icon-user" :to="{ name: 'list', params: {type: 'my'}}" :open="false"> @@ -45,7 +47,8 @@ icon="icon-user" :to="{ name: 'list', params: {type: 'participated'}}" :open="false"> @@ -53,7 +56,8 @@ icon="icon-link" :to="{ name: 'list', params: {type: 'public'}}" :open="false"> @@ -61,7 +65,8 @@ icon="icon-password" :to="{ name: 'list', params: {type: 'hidden'}}" :open="false"> @@ -69,7 +74,8 @@ icon="icon-delete" :to="{ name: 'list', params: {type: 'deleted'}}" :open="false"> @@ -164,6 +170,20 @@ export default { }, + deletePermanently(pollId) { + this.$store + .dispatch('deletePermanently', { pollId: pollId }) + .then((response) => { + // if we permanently delete current selected poll, + // reload deleted polls route + if(this.$route.params.id && this.$route.params.id == pollId) { + this.$router.push({name: 'list', params: {type: 'deleted'}}) + } + this.refreshPolls() + }) + + }, + refreshPolls() { if (this.$route.name !== 'publicVote') { diff --git a/src/js/components/Navigation/PollNavigationItems.vue b/src/js/components/Navigation/PollNavigationItems.vue index 8fe69b452..476979d64 100644 --- a/src/js/components/Navigation/PollNavigationItems.vue +++ b/src/js/components/Navigation/PollNavigationItems.vue @@ -34,6 +34,10 @@ {{ (poll.isAdmin) ? t('polls', 'Restore poll as admin') : t('polls', 'Restore poll') }} + + + {{ (poll.isAdmin) ? t('polls', 'Delete poll permanently as admin') : t('polls', 'Delete poll permanently') }} + diff --git a/src/js/components/PollList/PollListItem.vue b/src/js/components/PollList/PollListItem.vue index c96c09e4d..5380ae935 100644 --- a/src/js/components/PollList/PollListItem.vue +++ b/src/js/components/PollList/PollListItem.vue @@ -74,6 +74,10 @@ {{ (poll.isAdmin) ? t('polls', 'Restore poll as admin') : t('polls', 'Restore poll') }} + + + {{ (poll.isAdmin) ? t('polls', 'Delete poll permanently as admin') : t('polls', 'Delete poll permanently') }} +
@@ -183,6 +187,14 @@ export default { this.hideMenu() }, + deletePermanently() { + this.$store.dispatch('deletePermanently', { pollId: this.poll.id }) + .then((response) => { + this.refreshPolls() + }) + this.hideMenu() + }, + clonePoll() { this.$store.dispatch('clonePoll', { pollId: this.poll.id }) .then((response) => { diff --git a/src/js/components/SideBar/SideBarTabConfiguration.vue b/src/js/components/SideBar/SideBarTabConfiguration.vue index 2a3d89dd5..35ae63955 100644 --- a/src/js/components/SideBar/SideBarTabConfiguration.vue +++ b/src/js/components/SideBar/SideBarTabConfiguration.vue @@ -71,8 +71,10 @@
- + @@ -255,6 +257,16 @@ export default { }, + deletePermanently() { + if(!this.poll.deleted) return; + + this.$store + .dispatch('deletePermanently', { pollId: this.poll.id }) + .then((response) => { + this.$router.push({name: 'list', params: {type: 'deleted'}}) + }) + }, + writePoll() { if (this.titleEmpty) { OC.Notification.showTemporary(t('polls', 'Title must not be empty!'), { type: 'success' }) diff --git a/src/js/store/modules/polls.js b/src/js/store/modules/polls.js index 8fe6dd39f..69495bfb6 100644 --- a/src/js/store/modules/polls.js +++ b/src/js/store/modules/polls.js @@ -81,6 +81,18 @@ const actions = { }) }, + deletePermanently(context, payload) { + const endPoint = 'apps/polls/polls/delete/permanent/' + return axios.get(OC.generateUrl(endPoint + payload.pollId)) + .then((response) => { + OC.Notification.showTemporary(t('polls', 'Deleted poll permanently.'), { type: 'success' }) + return response + }, (error) => { + OC.Notification.showTemporary(t('polls', 'Error deleting poll.'), { type: 'error' }) + console.error('Error deleting poll', { error: error.response }, { payload: payload }) + }) + }, + clonePoll(context, payload) { const endPoint = 'apps/polls/polls/clone/' return axios.get(OC.generateUrl(endPoint + payload.pollId))