Merge pull request #3096 from nextcloud/enh/revoke-shares
revoke shares instead of deleting
This commit is contained in:
Коммит
db9295effc
|
@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
|
||||||
### New
|
### New
|
||||||
- Reveal hidden voters if hidden in case of performance concerns
|
- Reveal hidden voters if hidden in case of performance concerns
|
||||||
- Support better readability of vote page
|
- Support better readability of vote page
|
||||||
|
- added revoking of shares
|
||||||
|
- changed the behavior of share deletion
|
||||||
### Changes
|
### Changes
|
||||||
- Improved username check for public polls with a large number of groups in the backend
|
- Improved username check for public polls with a large number of groups in the backend
|
||||||
## [5.3.2] - 2023-09-11
|
## [5.3.2] - 2023-09-11
|
||||||
|
|
|
@ -98,6 +98,8 @@ return [
|
||||||
['name' => 'share#admin_to_user', 'url' => '/share/{token}/user', 'verb' => 'PUT'],
|
['name' => 'share#admin_to_user', 'url' => '/share/{token}/user', 'verb' => 'PUT'],
|
||||||
['name' => 'share#set_label', 'url' => '/share/{token}/setlabel', 'verb' => 'PUT'],
|
['name' => 'share#set_label', 'url' => '/share/{token}/setlabel', 'verb' => 'PUT'],
|
||||||
['name' => 'share#set_public_poll_email', 'url' => '/share/{token}/publicpollemail/{value}', 'verb' => 'PUT'],
|
['name' => 'share#set_public_poll_email', 'url' => '/share/{token}/publicpollemail/{value}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'share#revoke', 'url' => '/share/{token}/revoke', 'verb' => 'PUT'],
|
||||||
|
['name' => 'share#re_revoke', 'url' => '/share/{token}/rerevoke', 'verb' => 'PUT'],
|
||||||
|
|
||||||
['name' => 'settings#getAppSettings', 'url' => '/settings/app', 'verb' => 'GET'],
|
['name' => 'settings#getAppSettings', 'url' => '/settings/app', 'verb' => 'GET'],
|
||||||
['name' => 'settings#writeAppSettings', 'url' => '/settings/app', 'verb' => 'POST'],
|
['name' => 'settings#writeAppSettings', 'url' => '/settings/app', 'verb' => 'POST'],
|
||||||
|
@ -150,6 +152,8 @@ return [
|
||||||
['name' => 'share_api#add', 'url' => '/api/v1.0/poll/{pollId}/share/{type}', 'verb' => 'POST'],
|
['name' => 'share_api#add', 'url' => '/api/v1.0/poll/{pollId}/share/{type}', 'verb' => 'POST'],
|
||||||
['name' => 'share_api#delete', 'url' => '/api/v1.0/share/{token}', 'verb' => 'DELETE'],
|
['name' => 'share_api#delete', 'url' => '/api/v1.0/share/{token}', 'verb' => 'DELETE'],
|
||||||
['name' => 'share_api#sendInvitation', 'url' => '/api/v1.0/share/send/{token}', 'verb' => 'PUT'],
|
['name' => 'share_api#sendInvitation', 'url' => '/api/v1.0/share/send/{token}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'share_api#revoke', 'url' => '/api/v1.0/share/revoke/{token}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'share_api#re_revoke', 'url' => '/api/v1.0/share/rerevoke/{token}', 'verb' => 'PUT'],
|
||||||
|
|
||||||
['name' => 'subscription_api#get', 'url' => '/api/v1.0/poll/{pollId}/subscription', 'verb' => 'GET'],
|
['name' => 'subscription_api#get', 'url' => '/api/v1.0/poll/{pollId}/subscription', 'verb' => 'GET'],
|
||||||
['name' => 'subscription_api#subscribe', 'url' => '/api/v1.0/poll/{pollId}/subscription', 'verb' => 'PUT'],
|
['name' => 'subscription_api#subscribe', 'url' => '/api/v1.0/poll/{pollId}/subscription', 'verb' => 'PUT'],
|
||||||
|
|
|
@ -113,6 +113,24 @@ class ShareController extends BaseController {
|
||||||
return $this->responseDeleteTolerant(fn () => ['share' => $this->shareService->delete(token: $token)]);
|
return $this->responseDeleteTolerant(fn () => ['share' => $this->shareService->delete(token: $token)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete share
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function revoke(string $token): JSONResponse {
|
||||||
|
return $this->responseDeleteTolerant(fn () => ['share' => $this->shareService->revoke(token: $token)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete share
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function reRevoke(string $token): JSONResponse {
|
||||||
|
return $this->responseDeleteTolerant(fn () => ['share' => $this->shareService->reRevoke(token: $token)]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send invitation mails for a share
|
* Send invitation mails for a share
|
||||||
* Additionally send notification via notifications
|
* Additionally send notification via notifications
|
||||||
|
|
|
@ -47,10 +47,14 @@ use OCP\IURLGenerator;
|
||||||
* @method void setInvitationSent(integer $value)
|
* @method void setInvitationSent(integer $value)
|
||||||
* @method int getReminderSent()
|
* @method int getReminderSent()
|
||||||
* @method void setReminderSent(integer $value)
|
* @method void setReminderSent(integer $value)
|
||||||
|
* @method int getRevoked()
|
||||||
|
* @method void setRevoked(integer $value)
|
||||||
* @method string getDisplayName()
|
* @method string getDisplayName()
|
||||||
* @method void setDisplayName(string $value)
|
* @method void setDisplayName(string $value)
|
||||||
* @method string getMiscSettings()
|
* @method string getMiscSettings()
|
||||||
* @method void setMiscSettings(string $value)
|
* @method void setMiscSettings(string $value)
|
||||||
|
* @method int getVoted()
|
||||||
|
* @method void setVoted(int $value)
|
||||||
*/
|
*/
|
||||||
class Share extends Entity implements JsonSerializable {
|
class Share extends Entity implements JsonSerializable {
|
||||||
public const TABLE = 'polls_share';
|
public const TABLE = 'polls_share';
|
||||||
|
@ -76,6 +80,23 @@ class Share extends Entity implements JsonSerializable {
|
||||||
public const TYPE_CIRCLE = 'circle';
|
public const TYPE_CIRCLE = 'circle';
|
||||||
public const TYPE_CONTACTGROUP = 'contactGroup';
|
public const TYPE_CONTACTGROUP = 'contactGroup';
|
||||||
|
|
||||||
|
|
||||||
|
// Share types, that are allowed for public access (without login)
|
||||||
|
public const SHARE_PUBLIC_ACCESS_ALLOWED = [
|
||||||
|
self::TYPE_PUBLIC,
|
||||||
|
self::TYPE_CONTACT,
|
||||||
|
self::TYPE_EMAIL,
|
||||||
|
self::TYPE_EXTERNAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Share types, that are allowed for authenticated access (with login)
|
||||||
|
public const SHARE_AUTH_ACCESS_ALLOWED = [
|
||||||
|
self::TYPE_PUBLIC,
|
||||||
|
self::TYPE_ADMIN,
|
||||||
|
self::TYPE_GROUP,
|
||||||
|
self::TYPE_USER,
|
||||||
|
];
|
||||||
|
|
||||||
public const TYPE_SORT_ARRAY = [
|
public const TYPE_SORT_ARRAY = [
|
||||||
self::TYPE_PUBLIC,
|
self::TYPE_PUBLIC,
|
||||||
self::TYPE_ADMIN,
|
self::TYPE_ADMIN,
|
||||||
|
@ -102,12 +123,15 @@ class Share extends Entity implements JsonSerializable {
|
||||||
protected ?string $emailAddress = null;
|
protected ?string $emailAddress = null;
|
||||||
protected int $invitationSent = 0;
|
protected int $invitationSent = 0;
|
||||||
protected int $reminderSent = 0;
|
protected int $reminderSent = 0;
|
||||||
|
protected int $revoked = 0;
|
||||||
protected ?string $displayName = null;
|
protected ?string $displayName = null;
|
||||||
protected ?string $miscSettings = '';
|
protected ?string $miscSettings = '';
|
||||||
|
protected int $voted = 0;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->addType('pollId', 'int');
|
$this->addType('pollId', 'int');
|
||||||
$this->addType('invitationSent', 'int');
|
$this->addType('invitationSent', 'int');
|
||||||
|
$this->addType('Revoked', 'int');
|
||||||
$this->addType('reminderSent', 'int');
|
$this->addType('reminderSent', 'int');
|
||||||
$this->urlGenerator = Container::queryClass(IURLGenerator::class);
|
$this->urlGenerator = Container::queryClass(IURLGenerator::class);
|
||||||
$this->appSettings = new AppSettings;
|
$this->appSettings = new AppSettings;
|
||||||
|
@ -126,11 +150,13 @@ class Share extends Entity implements JsonSerializable {
|
||||||
'emailAddress' => $this->getEmailAddress(),
|
'emailAddress' => $this->getEmailAddress(),
|
||||||
'invitationSent' => $this->getInvitationSent(),
|
'invitationSent' => $this->getInvitationSent(),
|
||||||
'reminderSent' => $this->getReminderSent(),
|
'reminderSent' => $this->getReminderSent(),
|
||||||
|
'revoked' => $this->getRevoked(),
|
||||||
'displayName' => $this->getDisplayName(),
|
'displayName' => $this->getDisplayName(),
|
||||||
'isNoUser' => !(in_array($this->getType(), [self::TYPE_USER, self::TYPE_ADMIN], true)),
|
'isNoUser' => !(in_array($this->getType(), [self::TYPE_USER, self::TYPE_ADMIN], true)),
|
||||||
'URL' => $this->getURL(),
|
'URL' => $this->getURL(),
|
||||||
'showLogin' => $this->appSettings->getBooleanSetting(AppSettings::SETTING_SHOW_LOGIN),
|
'showLogin' => $this->appSettings->getBooleanSetting(AppSettings::SETTING_SHOW_LOGIN),
|
||||||
'publicPollEmail' => $this->getPublicPollEmail(),
|
'publicPollEmail' => $this->getPublicPollEmail(),
|
||||||
|
'voted' => $this->getVoted(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +228,10 @@ class Share extends Entity implements JsonSerializable {
|
||||||
$this->setMiscSettings(json_encode($value));
|
$this->setMiscSettings(json_encode($value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getVoteCount(): int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private function getMiscSettingsArray(): array {
|
private function getMiscSettingsArray(): array {
|
||||||
if ($this->getMiscSettings()) {
|
if ($this->getMiscSettings()) {
|
||||||
return json_decode($this->getMiscSettings(), true);
|
return json_decode($this->getMiscSettings(), true);
|
||||||
|
|
|
@ -25,10 +25,12 @@
|
||||||
namespace OCA\Polls\Db;
|
namespace OCA\Polls\Db;
|
||||||
|
|
||||||
use OCA\Polls\Exceptions\ShareNotFoundException;
|
use OCA\Polls\Exceptions\ShareNotFoundException;
|
||||||
|
use OCA\Polls\Migration\TableSchema;
|
||||||
use OCA\Polls\Model\UserBase;
|
use OCA\Polls\Model\UserBase;
|
||||||
use OCP\AppFramework\Db\DoesNotExistException;
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
use OCP\AppFramework\Db\QBMapper;
|
use OCP\AppFramework\Db\QBMapper;
|
||||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IConfig;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,7 +39,10 @@ use OCP\IDBConnection;
|
||||||
class ShareMapper extends QBMapper {
|
class ShareMapper extends QBMapper {
|
||||||
public const TABLE = Share::TABLE;
|
public const TABLE = Share::TABLE;
|
||||||
|
|
||||||
public function __construct(IDBConnection $db) {
|
public function __construct(
|
||||||
|
IDBConnection $db,
|
||||||
|
private IConfig $config,
|
||||||
|
) {
|
||||||
parent::__construct($db, Share::TABLE, Share::class);
|
parent::__construct($db, Share::TABLE, Share::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,16 +52,41 @@ class ShareMapper extends QBMapper {
|
||||||
* @psalm-return array<array-key, Share>
|
* @psalm-return array<array-key, Share>
|
||||||
*/
|
*/
|
||||||
public function findByPoll(int $pollId): array {
|
public function findByPoll(int $pollId): array {
|
||||||
|
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
|
||||||
$qb->select('*')
|
// get all column names from TableSchema
|
||||||
->from($this->getTableName())
|
foreach (TableSchema::TABLES[Share::TABLE] as $column => $values) {
|
||||||
|
$selectColumns[] = 'p.' . $column;
|
||||||
|
}
|
||||||
|
// add vote counter
|
||||||
|
$selectColumns[] = $qb->func()->count('c1.id', 'voted');
|
||||||
|
|
||||||
|
// Build the following select (MySQL)
|
||||||
|
//
|
||||||
|
// SELECT p.*, COUNT(c1.user_id) as voted
|
||||||
|
// FROM oc_polls_share p
|
||||||
|
// LEFT JOIN oc_polls_votes c1
|
||||||
|
// ON p.poll_id = c1.poll_id AND
|
||||||
|
// p.user_id = c1.user_id
|
||||||
|
// GROUP BY p.poll_id, p.user_id
|
||||||
|
//
|
||||||
|
$qb->select($selectColumns)
|
||||||
|
->from($this->getTableName(), 'p')
|
||||||
->where(
|
->where(
|
||||||
$qb->expr()->eq('poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))
|
$qb->expr()->eq('p.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))
|
||||||
);
|
)
|
||||||
|
->leftJoin('p', Vote::TABLE, 'c1',
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('p.poll_id', 'c1.poll_id'),
|
||||||
|
$qb->expr()->eq('p.user_id', 'c1.user_id'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->groupBy('poll_id', 'user_id');
|
||||||
|
|
||||||
return $this->findEntities($qb);
|
return $this->findEntities($qb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
|
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
|
||||||
* @return Share[]
|
* @return Share[]
|
||||||
|
@ -74,6 +104,18 @@ class ShareMapper extends QBMapper {
|
||||||
|
|
||||||
return $this->findEntities($qb);
|
return $this->findEntities($qb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countVotesByShare(Share $share): int {
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
|
||||||
|
$qb->select('*')
|
||||||
|
->from($this->config->getSystemValue('dbtableprefix', 'oc_') . Vote::TABLE)
|
||||||
|
->where($qb->expr()->eq('poll_id', $qb->createNamedParameter($share->getPollId(), IQueryBuilder::PARAM_INT)))
|
||||||
|
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($share->getUserId(), IQueryBuilder::PARAM_STR)));
|
||||||
|
|
||||||
|
return count($this->findEntities($qb));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
|
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
|
||||||
* @return Share[]
|
* @return Share[]
|
||||||
|
|
|
@ -35,6 +35,7 @@ abstract class ShareEvent extends BaseEvent {
|
||||||
public const CHANGE_REG_CONSTR = 'share_change_reg_const';
|
public const CHANGE_REG_CONSTR = 'share_change_reg_const';
|
||||||
public const REGISTRATION = 'share_registration';
|
public const REGISTRATION = 'share_registration';
|
||||||
public const DELETE = 'share_delete';
|
public const DELETE = 'share_delete';
|
||||||
|
public const REVOKED = 'share_revoked';
|
||||||
|
|
||||||
private Share $share;
|
private Share $share;
|
||||||
// protected UserBase $sharee = null;
|
// protected UserBase $sharee = null;
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* @copyright Copyright (c) 2021 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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Polls\Event;
|
||||||
|
|
||||||
|
use OCA\Polls\Db\Share;
|
||||||
|
|
||||||
|
class ShareRevokedEvent extends ShareEvent {
|
||||||
|
public function __construct(Share $share) {
|
||||||
|
parent::__construct($share);
|
||||||
|
$this->eventId = self::REVOKED;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2020 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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Polls\Exceptions;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
|
||||||
|
class InvalidMethodCallException extends Exception {
|
||||||
|
public function __construct(
|
||||||
|
string $e = 'Method is used illegally'
|
||||||
|
) {
|
||||||
|
parent::__construct($e, Http::STATUS_CONFLICT);
|
||||||
|
}
|
||||||
|
}
|
|
@ -151,6 +151,9 @@ abstract class TableSchema {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* define table structure
|
* define table structure
|
||||||
|
*
|
||||||
|
* IMPORTANT: After adding or deletion check queries in ShareMapper
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public const TABLES = [
|
public const TABLES = [
|
||||||
Poll::TABLE => [
|
Poll::TABLE => [
|
||||||
|
@ -215,6 +218,7 @@ abstract class TableSchema {
|
||||||
'display_name' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => null, 'length' => 256]],
|
'display_name' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => null, 'length' => 256]],
|
||||||
'email_address' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => null, 'length' => 256]],
|
'email_address' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => null, 'length' => 256]],
|
||||||
'invitation_sent' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
|
'invitation_sent' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
|
||||||
|
'revoked' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
|
||||||
'reminder_sent' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
|
'reminder_sent' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
|
||||||
'misc_settings' => ['type' => Types::TEXT, 'options' => ['notnull' => false, 'default' => null, 'length' => 65535]],
|
'misc_settings' => ['type' => Types::TEXT, 'options' => ['notnull' => false, 'default' => null, 'length' => 65535]],
|
||||||
],
|
],
|
||||||
|
|
|
@ -32,6 +32,7 @@ use OCA\Polls\Db\Share;
|
||||||
use OCA\Polls\Db\ShareMapper;
|
use OCA\Polls\Db\ShareMapper;
|
||||||
use OCA\Polls\Db\VoteMapper;
|
use OCA\Polls\Db\VoteMapper;
|
||||||
use OCA\Polls\Exceptions\ForbiddenException;
|
use OCA\Polls\Exceptions\ForbiddenException;
|
||||||
|
use OCA\Polls\Exceptions\InvalidMethodCallException;
|
||||||
use OCA\Polls\Exceptions\NotFoundException;
|
use OCA\Polls\Exceptions\NotFoundException;
|
||||||
use OCA\Polls\Exceptions\ShareNotFoundException;
|
use OCA\Polls\Exceptions\ShareNotFoundException;
|
||||||
use OCA\Polls\Model\Settings\AppSettings;
|
use OCA\Polls\Model\Settings\AppSettings;
|
||||||
|
@ -64,10 +65,7 @@ class Acl implements JsonSerializable {
|
||||||
public const PERMISSION_PUBLIC_SHARES = 'publicShares';
|
public const PERMISSION_PUBLIC_SHARES = 'publicShares';
|
||||||
public const PERMISSION_ALL_ACCESS = 'allAccess';
|
public const PERMISSION_ALL_ACCESS = 'allAccess';
|
||||||
|
|
||||||
private AppSettings $appSettings;
|
|
||||||
private Poll $poll;
|
|
||||||
private Share $share;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private IUserManager $userManager,
|
private IUserManager $userManager,
|
||||||
private IUserSession $userSession,
|
private IUserSession $userSession,
|
||||||
|
@ -75,53 +73,120 @@ class Acl implements JsonSerializable {
|
||||||
private OptionMapper $optionMapper,
|
private OptionMapper $optionMapper,
|
||||||
private PollMapper $pollMapper,
|
private PollMapper $pollMapper,
|
||||||
private VoteMapper $voteMapper,
|
private VoteMapper $voteMapper,
|
||||||
private ShareMapper $shareMapper
|
private ShareMapper $shareMapper,
|
||||||
|
private AppSettings $appSettings,
|
||||||
|
private ?Poll $poll,
|
||||||
|
private ?Share $share,
|
||||||
) {
|
) {
|
||||||
$this->poll = new Poll;
|
$this->poll = null;
|
||||||
$this->share = new Share;
|
$this->share = null;
|
||||||
$this->appSettings = new AppSettings;
|
$this->appSettings = new AppSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function jsonSerialize(): array {
|
||||||
|
return [
|
||||||
|
'allowAddOptions' => $this->getIsAllowed(self::PERMISSION_OPTIONS_ADD),
|
||||||
|
'allowAllAccess' => $this->getIsAllowed(self::PERMISSION_ALL_ACCESS),
|
||||||
|
'allowArchive' => $this->getIsAllowed(self::PERMISSION_POLL_ARCHIVE),
|
||||||
|
'allowComment' => $this->getIsAllowed(self::PERMISSION_COMMENT_ADD),
|
||||||
|
'allowDelete' => $this->getIsAllowed(self::PERMISSION_POLL_DELETE),
|
||||||
|
'allowEdit' => $this->getIsAllowed(self::PERMISSION_POLL_EDIT),
|
||||||
|
'allowPollCreation' => $this->getIsAllowed(self::PERMISSION_POLL_CREATE),
|
||||||
|
'allowPollDownload' => $this->getIsAllowed(self::PERMISSION_POLL_DOWNLOAD),
|
||||||
|
'allowPublicShares' => $this->getIsAllowed(self::PERMISSION_PUBLIC_SHARES),
|
||||||
|
'allowSeeResults' => $this->getIsAllowed(self::PERMISSION_POLL_RESULTS_VIEW),
|
||||||
|
'allowSeeUsernames' => $this->getIsAllowed(self::PERMISSION_POLL_USERNAMES_VIEW),
|
||||||
|
'allowSeeMailAddresses' => $this->getIsAllowed(self::PERMISSION_POLL_MAILADDRESSES_VIEW),
|
||||||
|
'allowSubscribe' => $this->getIsAllowed(self::PERMISSION_POLL_SUBSCRIBE),
|
||||||
|
'allowView' => $this->getIsAllowed(self::PERMISSION_POLL_VIEW),
|
||||||
|
'allowVote' => $this->getIsAllowed(self::PERMISSION_VOTE_EDIT),
|
||||||
|
'displayName' => $this->getDisplayName(),
|
||||||
|
'isOwner' => $this->getIsOwner(),
|
||||||
|
'isVoteLimitExceeded' => $this->getIsVoteLimitExceeded(),
|
||||||
|
'loggedIn' => $this->getIsLoggedIn(),
|
||||||
|
'isNoUser' => !$this->getIsLoggedIn(),
|
||||||
|
'isGuest' => !$this->getIsLoggedIn(),
|
||||||
|
'pollId' => $this->getPollId(),
|
||||||
|
'token' => $this->getToken(),
|
||||||
|
'userHasVoted' => $this->getIsParticipant(),
|
||||||
|
'userId' => $this->getUserId(),
|
||||||
|
'userIsInvolved' => $this->getIsInvolved(),
|
||||||
|
'pollExpired' => $this->poll->getExpired(),
|
||||||
|
'pollExpire' => $this->poll->getExpire(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load share via token and than call setShare
|
* Setters
|
||||||
*/
|
*/
|
||||||
public function setToken(string $token = '',
|
|
||||||
string $permission = self::PERMISSION_POLL_VIEW,
|
/**
|
||||||
?int $pollIdToValidate = null
|
* Set share token and load share if neccessary
|
||||||
): Acl {
|
* All ends with self::setpoll(), where the permission is checked
|
||||||
|
*/
|
||||||
|
public function setToken(string $token = '', string $permission = self::PERMISSION_POLL_VIEW): Acl {
|
||||||
try {
|
try {
|
||||||
$this->share = $this->shareMapper->findByToken($token);
|
if ($this->share?->$token === $token) { // share matching the requested token is already loaded
|
||||||
|
$this->setPollId($this->share->getPollId(), $permission); // Set the poll Id to verify the correct poll gets loaded and permissions get checked
|
||||||
if ($pollIdToValidate && $this->share->getPollId() !== $pollIdToValidate) {
|
} else {
|
||||||
throw new ForbiddenException('Share is not allowed accessing this poll');
|
$this->setShare($this->shareMapper->findByToken($token), $permission); // load the share mathing the requested token
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->poll = $this->pollMapper->find($this->share->getPollId());
|
|
||||||
$this->validateShareAccess();
|
|
||||||
$this->request($permission);
|
|
||||||
} catch (ShareNotFoundException $e) {
|
} catch (ShareNotFoundException $e) {
|
||||||
throw new NotFoundException('Error loading share ' . $token);
|
throw new NotFoundException('Error loading share ' . $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set share and load poll
|
||||||
|
* All ends with self::setPoll(), where the permission is checked
|
||||||
|
*/
|
||||||
|
public function setShare(Share $share, string $permission = self::PERMISSION_POLL_VIEW): Acl {
|
||||||
|
$this->share = $share;
|
||||||
|
|
||||||
|
if ($this->share->getRevoked()) {
|
||||||
|
throw new ShareNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validateShareAccess(); // check, if share is allowed for the user type
|
||||||
|
$this->setPollId($this->share->getPollId(), $permission); // set the poll id to laod the poll corresponding to the share and check permissions
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set poll id and load poll
|
||||||
|
*/
|
||||||
public function setPollId(int $pollId = 0, string $permission = self::PERMISSION_POLL_VIEW): Acl {
|
public function setPollId(int $pollId = 0, string $permission = self::PERMISSION_POLL_VIEW): Acl {
|
||||||
try {
|
try {
|
||||||
$this->poll = $this->pollMapper->find($pollId);
|
if ($this->poll?->getPollId() !== $pollId) {
|
||||||
$this->request($permission);
|
$this->setPoll($this->pollMapper->find($pollId), $permission); // load requested poll
|
||||||
|
} else {
|
||||||
|
$this->request($permission); // just check the permissions in all cases
|
||||||
|
}
|
||||||
|
|
||||||
} catch (DoesNotExistException $e) {
|
} catch (DoesNotExistException $e) {
|
||||||
throw new NotFoundException('Error loading poll with id ' . $pollId);
|
throw new NotFoundException('Error loading poll with id ' . $pollId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPoll(Poll $poll): void {
|
/**
|
||||||
|
* Set poll
|
||||||
|
*/
|
||||||
|
public function setPoll(Poll $poll, string $permission = self::PERMISSION_POLL_VIEW): Acl {
|
||||||
$this->poll = $poll;
|
$this->poll = $poll;
|
||||||
|
$this->loadShare();
|
||||||
|
$this->request($permission);
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPoll(): Poll {
|
/**
|
||||||
|
* Property getters
|
||||||
|
*/
|
||||||
|
public function getPoll(): Poll|null {
|
||||||
return $this->poll;
|
return $this->poll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,13 +195,29 @@ class Acl implements JsonSerializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getToken(): string {
|
public function getToken(): string {
|
||||||
return strval($this->share->getToken());
|
return strval($this->share?->getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTokenIsValid(): bool {
|
||||||
|
return ($this->share?->getToken() && !$this->share->getRevoked());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUserId(): string {
|
public function getUserId(): string {
|
||||||
return $this->userSession->getUser()?->getUID() ?? $this->share->getUserId();
|
return $this->userSession->getUser()?->getUID() ?? $this->share->getUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getDisplayName(): string {
|
||||||
|
return ($this->getIsLoggedIn() ? $this->userManager->get($this->getUserId())?->getDisplayName() : $this->share->getDisplayName()) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validations
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function getIsOwner(): bool {
|
||||||
|
return ($this->getIsLoggedIn() && $this->poll->getOwner() === $this->getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
public function validateUserId(string $userId): void {
|
public function validateUserId(string $userId): void {
|
||||||
if ($this->getUserId() !== $userId) {
|
if ($this->getUserId() !== $userId) {
|
||||||
throw new ForbiddenException('User id does not match.');
|
throw new ForbiddenException('User id does not match.');
|
||||||
|
@ -149,12 +230,13 @@ class Acl implements JsonSerializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIsOwner(): bool {
|
private function validateShareAccess(): void {
|
||||||
return ($this->getIsLoggedIn() && $this->poll->getOwner() === $this->getUserId());
|
if ($this->getIsLoggedIn() && !$this->getIsShareValidForUsers()) {
|
||||||
}
|
throw new ForbiddenException('Share type "' . $this->share->getType() . '" is only valid for guests');
|
||||||
|
}
|
||||||
private function getDisplayName(): string {
|
if (!$this->getIsShareValidForGuests()) {
|
||||||
return ($this->getIsLoggedIn() ? $this->userManager->get($this->getUserId())?->getDisplayName() : $this->share->getDisplayName()) ?? '';
|
throw new ForbiddenException('Share type "' . $this->share->getType() . '" is only valid for registered users');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getIsAllowed(string $permission): bool {
|
public function getIsAllowed(string $permission): bool {
|
||||||
|
@ -212,39 +294,6 @@ class Acl implements JsonSerializable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): array {
|
|
||||||
return [
|
|
||||||
'allowAddOptions' => $this->getIsAllowed(self::PERMISSION_OPTIONS_ADD),
|
|
||||||
'allowAllAccess' => $this->getIsAllowed(self::PERMISSION_ALL_ACCESS),
|
|
||||||
'allowArchive' => $this->getIsAllowed(self::PERMISSION_POLL_ARCHIVE),
|
|
||||||
'allowComment' => $this->getIsAllowed(self::PERMISSION_COMMENT_ADD),
|
|
||||||
'allowDelete' => $this->getIsAllowed(self::PERMISSION_POLL_DELETE),
|
|
||||||
'allowEdit' => $this->getIsAllowed(self::PERMISSION_POLL_EDIT),
|
|
||||||
'allowPollCreation' => $this->getIsAllowed(self::PERMISSION_POLL_CREATE),
|
|
||||||
'allowPollDownload' => $this->getIsAllowed(self::PERMISSION_POLL_DOWNLOAD),
|
|
||||||
'allowPublicShares' => $this->getIsAllowed(self::PERMISSION_PUBLIC_SHARES),
|
|
||||||
'allowSeeResults' => $this->getIsAllowed(self::PERMISSION_POLL_RESULTS_VIEW),
|
|
||||||
'allowSeeUsernames' => $this->getIsAllowed(self::PERMISSION_POLL_USERNAMES_VIEW),
|
|
||||||
'allowSeeMailAddresses' => $this->getIsAllowed(self::PERMISSION_POLL_MAILADDRESSES_VIEW),
|
|
||||||
'allowSubscribe' => $this->getIsAllowed(self::PERMISSION_POLL_SUBSCRIBE),
|
|
||||||
'allowView' => $this->getIsAllowed(self::PERMISSION_POLL_VIEW),
|
|
||||||
'allowVote' => $this->getIsAllowed(self::PERMISSION_VOTE_EDIT),
|
|
||||||
'displayName' => $this->getDisplayName(),
|
|
||||||
'isOwner' => $this->getIsOwner(),
|
|
||||||
'isVoteLimitExceeded' => $this->getIsVoteLimitExceeded(),
|
|
||||||
'loggedIn' => $this->getIsLoggedIn(),
|
|
||||||
'isNoUser' => !$this->getIsLoggedIn(),
|
|
||||||
'isGuest' => !$this->getIsLoggedIn(),
|
|
||||||
'pollId' => $this->getPollId(),
|
|
||||||
'token' => $this->getToken(),
|
|
||||||
'userHasVoted' => $this->getIsParticipant(),
|
|
||||||
'userId' => $this->getUserId(),
|
|
||||||
'userIsInvolved' => $this->getIsInvolved(),
|
|
||||||
'pollExpired' => $this->poll->getExpired(),
|
|
||||||
'pollExpire' => $this->poll->getExpire(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getIsLogged - Is user logged in to nextcloud?
|
* getIsLogged - Is user logged in to nextcloud?
|
||||||
*/
|
*/
|
||||||
|
@ -306,23 +355,16 @@ class Acl implements JsonSerializable {
|
||||||
* This only affects logged in users.
|
* This only affects logged in users.
|
||||||
*/
|
*/
|
||||||
private function getIsPersonallyInvited(): bool {
|
private function getIsPersonallyInvited(): bool {
|
||||||
if (!$this->getIsLoggedIn()) {
|
if ($this->getIsLoggedIn() && $this->share) {
|
||||||
return false;
|
return in_array($this->share->getType(), [
|
||||||
|
Share::TYPE_ADMIN,
|
||||||
|
Share::TYPE_USER,
|
||||||
|
Share::TYPE_EXTERNAL,
|
||||||
|
Share::TYPE_EMAIL,
|
||||||
|
Share::TYPE_CONTACT
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return 0 < count(
|
|
||||||
array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
|
|
||||||
return ($item->getUserId() === $this->getUserId()
|
|
||||||
&& in_array($item->getType(), [
|
|
||||||
Share::TYPE_ADMIN,
|
|
||||||
Share::TYPE_USER,
|
|
||||||
Share::TYPE_EXTERNAL,
|
|
||||||
Share::TYPE_EMAIL,
|
|
||||||
Share::TYPE_CONTACT
|
|
||||||
])
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getIsDelegatedAdmin(): bool {
|
private function getIsDelegatedAdmin(): bool {
|
||||||
|
@ -330,100 +372,84 @@ class Acl implements JsonSerializable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filteredList = array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
|
if ($this->loadShare()) { // load share, if not loaded
|
||||||
return ($item->getUserId() === $this->getUserId()
|
return $this->share->getType() === Share::TYPE_ADMIN && !$this->share->getRevoked();
|
||||||
&& in_array($item->getType(), [
|
|
||||||
Share::TYPE_ADMIN,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return 0 < count($filteredList);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function validateShareAccess(): void {
|
|
||||||
if ($this->getIsLoggedIn() && !$this->getIsShareValidForUsers()) {
|
|
||||||
throw new ForbiddenException('Share type "' . $this->share->getType() . '" is only valid for guests');
|
|
||||||
}
|
|
||||||
if (!$this->getIsShareValidForGuests()) {
|
|
||||||
throw new ForbiddenException('Share type "' . $this->share->getType() . '" is only valid for registered users');
|
|
||||||
};
|
};
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getIsShareValidForGuests(): bool {
|
private function getIsShareValidForGuests(): bool {
|
||||||
return in_array($this->share->getType(), [
|
return in_array($this->share->getType(), Share::SHARE_PUBLIC_ACCESS_ALLOWED);
|
||||||
Share::TYPE_PUBLIC,
|
|
||||||
Share::TYPE_EMAIL,
|
|
||||||
Share::TYPE_CONTACT,
|
|
||||||
Share::TYPE_EXTERNAL
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getIsShareValidForUsers(): bool {
|
private function getIsShareValidForUsers(): bool {
|
||||||
return in_array($this->share->getType(), [
|
return in_array($this->share->getType(), Share::SHARE_AUTH_ACCESS_ALLOWED);
|
||||||
Share::TYPE_PUBLIC,
|
|
||||||
Share::TYPE_ADMIN,
|
|
||||||
Share::TYPE_USER,
|
|
||||||
Share::TYPE_GROUP
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getHasEmail(): bool {
|
private function getHasEmail(): bool {
|
||||||
return $this->share->getToken() ? strlen($this->share->getEmailAddress()) > 0 : $this->getIsLoggedIn();
|
return $this->share?->getToken() ? strlen($this->share->getEmailAddress()) > 0 : $this->getIsLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load share for access checks, if it is not already loaded
|
||||||
|
**/
|
||||||
|
private function loadShare(): bool {
|
||||||
|
if (!$this->poll) {
|
||||||
|
throw new InvalidMethodCallException('Loading share only possible with loaded poll');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->share?->getUserId() !== $this->getUserId() || $this->share?->getPollId() !== $this->poll->getId()) {
|
||||||
|
$this->share = $this->shareMapper->findByPollAndUser($this->poll->getId(), $this->getUserId());
|
||||||
|
}
|
||||||
|
} catch (\Throwable $th) {
|
||||||
|
$this->share = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->share !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks, if user is allowed to edit the poll configuration
|
* Checks, if user is allowed to edit the poll configuration
|
||||||
**/
|
**/
|
||||||
private function getAllowEditPoll(): bool {
|
private function getAllowEditPoll(): bool {
|
||||||
// Console god mode
|
|
||||||
if (defined('OC_CONSOLE')) {
|
if (defined('OC_CONSOLE')) {
|
||||||
return true;
|
return true; // Console god mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// owner is always allowed to edit the poll configuration
|
|
||||||
if ($this->getIsOwner()) {
|
if ($this->getIsOwner()) {
|
||||||
return true;
|
return true; // owner is always allowed to edit the poll configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// user has delegated owner rights
|
|
||||||
if ($this->getIsDelegatedAdmin()) {
|
if ($this->getIsDelegatedAdmin()) {
|
||||||
return true;
|
return true; // user has delegated owner rights
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false; // deny edit rights in all other cases
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks, if user is allowed to access poll
|
* Checks, if user is allowed to access poll
|
||||||
**/
|
**/
|
||||||
private function getAllowAccessPoll(): bool {
|
private function getAllowAccessPoll(): bool {
|
||||||
// edit rights include access to poll
|
|
||||||
if ($this->getAllowEditPoll()) {
|
if ($this->getAllowEditPoll()) {
|
||||||
return true;
|
return true; // edit rights include access to poll
|
||||||
}
|
}
|
||||||
|
|
||||||
// No further access to poll, if it is deleted
|
|
||||||
if ($this->poll->getDeleted()) {
|
if ($this->poll->getDeleted()) {
|
||||||
return false;
|
return false; // No further access to poll, if it is deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
// grant access if user is involved in poll in any way
|
|
||||||
if ($this->getIsInvolved()) {
|
if ($this->getIsInvolved()) {
|
||||||
return true;
|
return true; // grant access if user is involved in poll in any way
|
||||||
}
|
}
|
||||||
|
|
||||||
// grant access if poll poll is an open poll (for logged in users)
|
|
||||||
if ($this->poll->getAccess() === Poll::ACCESS_OPEN && $this->getIsLoggedIn()) {
|
if ($this->poll->getAccess() === Poll::ACCESS_OPEN && $this->getIsLoggedIn()) {
|
||||||
return true;
|
return true; // grant access if poll poll is an open poll (for logged in users)
|
||||||
}
|
}
|
||||||
|
|
||||||
// user has valid token of this poll
|
return $this->getTokenIsValid(); // return check result of an existing valid share for this user
|
||||||
if ($this->getToken()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -431,62 +457,54 @@ class Acl implements JsonSerializable {
|
||||||
* includes the right to archive and take over
|
* includes the right to archive and take over
|
||||||
**/
|
**/
|
||||||
private function getAllowDeletePoll(): bool {
|
private function getAllowDeletePoll(): bool {
|
||||||
// users with edit rights are allowed to delete the poll
|
|
||||||
if ($this->getAllowEditPoll()) {
|
if ($this->getAllowEditPoll()) {
|
||||||
return true;
|
return true; // users with edit rights are allowed to delete the poll
|
||||||
}
|
}
|
||||||
|
|
||||||
// admins are allowed to delete the poll
|
|
||||||
if ($this->getIsAdmin()) {
|
if ($this->getIsAdmin()) {
|
||||||
return true;
|
return true; // admins are allowed to delete polls
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false; // in all other cases deny poll deletion right
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* User rights inside poll
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks, if user is allowed to add add vote options
|
* Checks, if user is allowed to add add vote options
|
||||||
**/
|
**/
|
||||||
private function getAllowAddOptions(): bool {
|
private function getAllowAddOptions(): bool {
|
||||||
// Edit right includes adding new options
|
|
||||||
if ($this->getAllowEditPoll()) {
|
if ($this->getAllowEditPoll()) {
|
||||||
return true;
|
return true; // Edit right includes adding new options
|
||||||
}
|
}
|
||||||
|
|
||||||
// deny, if user has no access right to this poll
|
|
||||||
if (!$this->getAllowAccessPoll()) {
|
if (!$this->getAllowAccessPoll()) {
|
||||||
return false;
|
return false; // deny, if user has no access right to this poll
|
||||||
}
|
}
|
||||||
|
|
||||||
// public shares are not allowed to add options
|
if ($this->share?->getType() === Share::TYPE_PUBLIC) {
|
||||||
if ($this->share->getType() === Share::TYPE_PUBLIC) {
|
return false; // public shares are not allowed to add options
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request for option proposals is expired, deny
|
|
||||||
if ($this->poll->getProposalsExpired()) {
|
if ($this->poll->getProposalsExpired()) {
|
||||||
return false;
|
return false; // Request for option proposals is expired, deny
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->poll->getAllowProposals() === Poll::PROPOSAL_ALLOW;
|
return $this->poll->getAllowProposals() === Poll::PROPOSAL_ALLOW; // Allow, if poll requests proposals
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks, if user is allowed to comment
|
* Checks, if user is allowed to see and write comments
|
||||||
**/
|
**/
|
||||||
private function getAllowComment(): bool {
|
private function getAllowComment(): bool {
|
||||||
// user has no access right to this poll
|
|
||||||
if (!$this->getAllowAccessPoll()) {
|
if (!$this->getAllowAccessPoll()) {
|
||||||
return false;
|
return false; // user has no access right to this poll
|
||||||
}
|
}
|
||||||
|
|
||||||
// public shares are not allowed to comment
|
if ($this->share?->getType() === Share::TYPE_PUBLIC) {
|
||||||
if ($this->share->getType() === Share::TYPE_PUBLIC) {
|
return false; // public shares are not allowed to comment
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
if ($this->share?->getRevoked()) {
|
||||||
|
return false; // public shares are not allowed to comment
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool) $this->poll->getAllowComment();
|
return (bool) $this->poll->getAllowComment();
|
||||||
|
@ -496,43 +514,40 @@ class Acl implements JsonSerializable {
|
||||||
* Checks, if user is allowed to comment
|
* Checks, if user is allowed to comment
|
||||||
**/
|
**/
|
||||||
private function getAllowVote(): bool {
|
private function getAllowVote(): bool {
|
||||||
// user has no access right to this poll
|
|
||||||
if (!$this->getAllowAccessPoll()) {
|
if (!$this->getAllowAccessPoll()) {
|
||||||
return false;
|
return false; // user has no access right to this poll
|
||||||
}
|
}
|
||||||
|
|
||||||
// public shares are not allowed to vote
|
if ($this->share?->getType() === Share::TYPE_PUBLIC) {
|
||||||
if ($this->share->getType() === Share::TYPE_PUBLIC) {
|
return false; // public shares are not allowed to vote
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// deny votes, if poll is expired
|
if ($this->share?->getRevoked()) {
|
||||||
return !$this->poll->getExpired();
|
return false; // public shares are not allowed to vote
|
||||||
|
}
|
||||||
|
|
||||||
|
return !$this->poll->getExpired(); // deny votes, if poll is expired
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getAllowSubscribeToPoll(): bool {
|
private function getAllowSubscribeToPoll(): bool {
|
||||||
// user has no access right to this poll
|
|
||||||
if (!$this->getAllowAccessPoll()) {
|
if (!$this->getAllowAccessPoll()) {
|
||||||
return false;
|
return false; // user has no access right to this poll
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getHasEmail();
|
return $this->getHasEmail();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getShowResults(): bool {
|
private function getShowResults(): bool {
|
||||||
// edit rights include access to results
|
|
||||||
if ($this->getAllowEditPoll()) {
|
if ($this->getAllowEditPoll()) {
|
||||||
return true;
|
return true; // edit rights include access to results
|
||||||
}
|
}
|
||||||
|
|
||||||
// no access to poll, deny
|
|
||||||
if (!$this->getAllowAccessPoll()) {
|
if (!$this->getAllowAccessPoll()) {
|
||||||
return false;
|
return false; // no access to poll, deny
|
||||||
}
|
}
|
||||||
|
|
||||||
// show results, when poll is cloed
|
|
||||||
if ($this->poll->getShowResults() === Poll::SHOW_RESULTS_CLOSED && $this->poll->getExpired()) {
|
if ($this->poll->getShowResults() === Poll::SHOW_RESULTS_CLOSED && $this->poll->getExpired()) {
|
||||||
return true;
|
return true; // show results, when poll is cloed
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->poll->getShowResults() === Poll::SHOW_RESULTS_ALWAYS;
|
return $this->poll->getShowResults() === Poll::SHOW_RESULTS_ALWAYS;
|
||||||
|
|
|
@ -83,7 +83,7 @@ class PollService {
|
||||||
|
|
||||||
foreach ($polls as $poll) {
|
foreach ($polls as $poll) {
|
||||||
try {
|
try {
|
||||||
$this->acl->setPollId($poll->getId());
|
$this->acl->setPoll($poll);
|
||||||
// TODO: Not the elegant way. Improvement neccessary
|
// TODO: Not the elegant way. Improvement neccessary
|
||||||
$relevantThreshold = max(
|
$relevantThreshold = max(
|
||||||
$poll->getCreated(),
|
$poll->getCreated(),
|
||||||
|
|
|
@ -32,6 +32,7 @@ use OCA\Polls\Event\ShareChangedRegistrationConstraintEvent;
|
||||||
use OCA\Polls\Event\ShareCreateEvent;
|
use OCA\Polls\Event\ShareCreateEvent;
|
||||||
use OCA\Polls\Event\ShareDeletedEvent;
|
use OCA\Polls\Event\ShareDeletedEvent;
|
||||||
use OCA\Polls\Event\ShareRegistrationEvent;
|
use OCA\Polls\Event\ShareRegistrationEvent;
|
||||||
|
use OCA\Polls\Event\ShareRevokedEvent;
|
||||||
use OCA\Polls\Event\ShareTypeChangedEvent;
|
use OCA\Polls\Event\ShareTypeChangedEvent;
|
||||||
use OCA\Polls\Exceptions\ForbiddenException;
|
use OCA\Polls\Exceptions\ForbiddenException;
|
||||||
use OCA\Polls\Exceptions\InvalidShareTypeException;
|
use OCA\Polls\Exceptions\InvalidShareTypeException;
|
||||||
|
@ -325,6 +326,38 @@ class ShareService {
|
||||||
return $share->getToken();
|
return $share->getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke share
|
||||||
|
*/
|
||||||
|
public function revoke(Share $share = null, string $token = null): string {
|
||||||
|
if ($token) {
|
||||||
|
$share = $this->shareMapper->findByToken($token);
|
||||||
|
}
|
||||||
|
$this->acl->setPollId($share->getPollId(), Acl::PERMISSION_POLL_EDIT);
|
||||||
|
|
||||||
|
$share->setRevoked(time());
|
||||||
|
$this->shareMapper->update($share);
|
||||||
|
$this->eventDispatcher->dispatchTyped(new ShareRevokedEvent($share));
|
||||||
|
|
||||||
|
return $share->getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-revoke share
|
||||||
|
*/
|
||||||
|
public function reRevoke(Share $share = null, string $token = null): string {
|
||||||
|
if ($token) {
|
||||||
|
$share = $this->shareMapper->findByToken($token);
|
||||||
|
}
|
||||||
|
$this->acl->setPollId($share->getPollId(), Acl::PERMISSION_POLL_EDIT);
|
||||||
|
|
||||||
|
$share->setRevoked(0);
|
||||||
|
$this->shareMapper->update($share);
|
||||||
|
$this->eventDispatcher->dispatchTyped(new ShareCreateEvent($share));
|
||||||
|
|
||||||
|
return $share->getToken();
|
||||||
|
}
|
||||||
|
|
||||||
public function sendAllInvitations(int $pollId): SentResult|null {
|
public function sendAllInvitations(int $pollId): SentResult|null {
|
||||||
$sentResult = new SentResult();
|
$sentResult = new SentResult();
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,22 @@ const shares = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
revokeShare(shareToken) {
|
||||||
|
return httpInstance.request({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `share/${shareToken}/revoke`,
|
||||||
|
cancelToken: cancelTokenHandlerObject[this.revokeShare.name].handleRequestCancellation().token,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
reRevokeShare(shareToken) {
|
||||||
|
return httpInstance.request({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `share/${shareToken}/rerevoke`,
|
||||||
|
cancelToken: cancelTokenHandlerObject[this.reRevokeShare.name].handleRequestCancellation().token,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
inviteAll(pollId) {
|
inviteAll(pollId) {
|
||||||
return httpInstance.request({
|
return httpInstance.request({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<NcContent app-name="polls" :class="appClass">
|
<NcContent app-name="polls" :class="appClass">
|
||||||
<router-view v-if="getCurrentUser()" name="navigation" />
|
<router-view v-if="getCurrentUser()" name="navigation" />
|
||||||
<router-view />
|
<router-view />
|
||||||
<router-view v-show="sideBar.open" name="sidebar" />
|
<router-view v-if="poll.acl.allowEdit || poll.acl.allowComment" name="sidebar" />
|
||||||
<LoadingOverlay v-if="loading" />
|
<LoadingOverlay v-if="loading" />
|
||||||
<UserSettingsDlg />
|
<UserSettingsDlg />
|
||||||
</NcContent>
|
</NcContent>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
import UserSettingsDlg from './components/Settings/UserSettingsDlg.vue'
|
import UserSettingsDlg from './components/Settings/UserSettingsDlg.vue'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
import { NcContent } from '@nextcloud/vue'
|
import { NcContent } from '@nextcloud/vue'
|
||||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||||
import { mapState, mapActions } from 'vuex'
|
import { mapState, mapActions } from 'vuex'
|
||||||
import '@nextcloud/dialogs/dist/index.css'
|
import '@nextcloud/dialogs/dist/index.css'
|
||||||
import './assets/scss/colors.scss'
|
import './assets/scss/colors.scss'
|
||||||
|
@ -57,9 +57,6 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sideBar: {
|
|
||||||
open: (window.innerWidth > 920),
|
|
||||||
},
|
|
||||||
transitionClass: 'transitions-active',
|
transitionClass: 'transitions-active',
|
||||||
loading: false,
|
loading: false,
|
||||||
isLoggedin: !!getCurrentUser(),
|
isLoggedin: !!getCurrentUser(),
|
||||||
|
@ -119,16 +116,11 @@ export default {
|
||||||
this.loadPoll(silent)
|
this.loadPoll(silent)
|
||||||
})
|
})
|
||||||
|
|
||||||
subscribe('polls:sidebar:toggle', (payload) => {
|
|
||||||
emit('polls:sidebar:changeTab', { activeTab: payload.activeTab })
|
|
||||||
this.sideBar.open = payload?.open ?? !this.sideBar.open
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.cancelToken.cancel()
|
this.cancelToken.cancel()
|
||||||
unsubscribe('polls:poll:load')
|
unsubscribe('polls:poll:load')
|
||||||
unsubscribe('polls:sidebar:toggle')
|
|
||||||
unsubscribe('polls:transitions:on')
|
unsubscribe('polls:transitions:on')
|
||||||
unsubscribe('polls:transitions:off')
|
unsubscribe('polls:transitions:off')
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
<UndoIcon v-if="deleteTimeout"
|
<UndoIcon v-if="deleteTimeout"
|
||||||
:size="iconSize"
|
:size="iconSize"
|
||||||
@click="cancelDelete()" />
|
@click="cancelDelete()" />
|
||||||
|
<RevokeIcon v-else-if="revoke"
|
||||||
|
:size="iconSize"
|
||||||
|
@click="deleteItem()" />
|
||||||
<DeleteIcon v-else
|
<DeleteIcon v-else
|
||||||
:size="iconSize"
|
:size="iconSize"
|
||||||
@click="deleteItem()" />
|
@click="deleteItem()" />
|
||||||
|
@ -40,12 +43,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { NcButton } from '@nextcloud/vue'
|
import { NcButton } from '@nextcloud/vue'
|
||||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
||||||
|
import RevokeIcon from 'vue-material-design-icons/Close.vue'
|
||||||
import UndoIcon from 'vue-material-design-icons/ArrowULeftTop.vue'
|
import UndoIcon from 'vue-material-design-icons/ArrowULeftTop.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ActionDelete',
|
name: 'ActionDelete',
|
||||||
components: {
|
components: {
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
|
RevokeIcon,
|
||||||
UndoIcon,
|
UndoIcon,
|
||||||
NcButton,
|
NcButton,
|
||||||
},
|
},
|
||||||
|
@ -64,6 +69,10 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 20,
|
default: 20,
|
||||||
},
|
},
|
||||||
|
revoke: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -85,6 +94,12 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
deleteItem() {
|
deleteItem() {
|
||||||
|
// delete immediately
|
||||||
|
if (this.timeout === 0) {
|
||||||
|
this.$emit('delete')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.countDown = this.timeout
|
this.countDown = this.timeout
|
||||||
this.deleteInterval = setInterval(() => {
|
this.deleteInterval = setInterval(() => {
|
||||||
this.countdown -= 1
|
this.countdown -= 1
|
||||||
|
|
|
@ -66,7 +66,7 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
allowComment: (state) => state.poll.allowComment,
|
allowComment: (state) => state.poll.acl.allowComment,
|
||||||
allowEdit: (state) => state.poll.acl.allowEdit,
|
allowEdit: (state) => state.poll.acl.allowEdit,
|
||||||
allowVote: (state) => state.poll.acl.allowVote,
|
allowVote: (state) => state.poll.acl.allowVote,
|
||||||
allowPollDownload: (state) => state.poll.acl.allowPollDownload,
|
allowPollDownload: (state) => state.poll.acl.allowPollDownload,
|
||||||
|
|
|
@ -24,9 +24,10 @@
|
||||||
<UserItem v-bind="share"
|
<UserItem v-bind="share"
|
||||||
show-email
|
show-email
|
||||||
resolve-info
|
resolve-info
|
||||||
|
:forced-description="share.revoked ? t('polls', 'User is only able to see the votes.') : null"
|
||||||
:icon="true">
|
:icon="true">
|
||||||
<template #status>
|
<template #status>
|
||||||
<div v-if="hasVoted(share.userId)">
|
<div v-if="share.voted">
|
||||||
<VotedIcon class="vote-status voted" :title="t('polls', 'Has voted')" />
|
<VotedIcon class="vote-status voted" :title="t('polls', 'Has voted')" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="['public', 'group'].includes(share.type)">
|
<div v-else-if="['public', 'group'].includes(share.type)">
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<NcActions>
|
<NcActions v-if="!share.revoked">
|
||||||
<NcActionInput v-if="share.type === 'public'"
|
<NcActionInput v-if="share.type === 'public'"
|
||||||
:show-trailing-button="false"
|
:show-trailing-button="false"
|
||||||
:value.sync="label"
|
:value.sync="label"
|
||||||
|
@ -112,13 +113,24 @@
|
||||||
</NcActionRadio>
|
</NcActionRadio>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
|
|
||||||
<ActionDelete :title="t('polls', 'Remove share')"
|
<NcActions v-if="share.revoked">
|
||||||
@delete="removeShare({ share })" />
|
<NcActionButton @click="reRevokeShare({ share })">
|
||||||
|
<template #icon>
|
||||||
|
<ReRevokeIcon />
|
||||||
|
</template>
|
||||||
|
{{ t('polls', 'Re-Revoke share') }}
|
||||||
|
</NcActionButton>
|
||||||
|
</NcActions>
|
||||||
|
|
||||||
|
<ActionDelete :timeout="share.revoked ? 4 : 0"
|
||||||
|
:revoke="!!share.voted && !share.revoked"
|
||||||
|
:title="deleteButtonCaption"
|
||||||
|
@delete="clickDeleted(share)" />
|
||||||
</UserItem>
|
</UserItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
import { showSuccess, showError } from '@nextcloud/dialogs'
|
import { showSuccess, showError } from '@nextcloud/dialogs'
|
||||||
import { NcActions, NcActionButton, NcActionCaption, NcActionInput, NcActionRadio } from '@nextcloud/vue'
|
import { NcActions, NcActionButton, NcActionCaption, NcActionInput, NcActionRadio } from '@nextcloud/vue'
|
||||||
import { ActionDelete } from '../Actions/index.js'
|
import { ActionDelete } from '../Actions/index.js'
|
||||||
|
@ -131,6 +143,7 @@ import EditIcon from 'vue-material-design-icons/Pencil.vue'
|
||||||
import WithdrawAdminIcon from 'vue-material-design-icons/ShieldCrownOutline.vue'
|
import WithdrawAdminIcon from 'vue-material-design-icons/ShieldCrownOutline.vue'
|
||||||
import ClippyIcon from 'vue-material-design-icons/ClipboardArrowLeftOutline.vue'
|
import ClippyIcon from 'vue-material-design-icons/ClipboardArrowLeftOutline.vue'
|
||||||
import QrIcon from 'vue-material-design-icons/Qrcode.vue'
|
import QrIcon from 'vue-material-design-icons/Qrcode.vue'
|
||||||
|
import ReRevokeIcon from 'vue-material-design-icons/Recycle.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ShareItem',
|
name: 'ShareItem',
|
||||||
|
@ -151,6 +164,7 @@ export default {
|
||||||
NcActionRadio,
|
NcActionRadio,
|
||||||
ActionDelete,
|
ActionDelete,
|
||||||
ResolveGroupIcon,
|
ResolveGroupIcon,
|
||||||
|
ReRevokeIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -161,10 +175,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
|
||||||
hasVoted: 'votes/hasVoted',
|
|
||||||
}),
|
|
||||||
|
|
||||||
label: {
|
label: {
|
||||||
get() {
|
get() {
|
||||||
return this.share.displayName
|
return this.share.displayName
|
||||||
|
@ -173,17 +183,50 @@ export default {
|
||||||
this.$store.commit('shares/setShareProperty', { id: this.share.id, displayName: value })
|
this.$store.commit('shares/setShareProperty', { id: this.share.id, displayName: value })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deleteButtonCaption() {
|
||||||
|
if (this.share.voted && this.share.revoked) {
|
||||||
|
return t('polls', 'Delete share and remove user from poll')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.share.voted && !this.share.revoked) {
|
||||||
|
return t('polls', 'Revoke share')
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('polls', 'Delete share')
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
removeShare: 'shares/delete',
|
deleteShare: 'shares/delete',
|
||||||
|
revokeShare: 'shares/revoke',
|
||||||
|
reRevokeShare: 'shares/reRevoke',
|
||||||
switchAdmin: 'shares/switchAdmin',
|
switchAdmin: 'shares/switchAdmin',
|
||||||
setPublicPollEmail: 'shares/setPublicPollEmail',
|
setPublicPollEmail: 'shares/setPublicPollEmail',
|
||||||
setLabel: 'shares/writeLabel',
|
setLabel: 'shares/writeLabel',
|
||||||
|
deleteUser: 'votes/deleteUser',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
async clickDeleted(share) {
|
||||||
|
try {
|
||||||
|
if (share.voted && share.revoked) {
|
||||||
|
this.deleteShare({ share })
|
||||||
|
this.deleteUser({ userId: share.userId })
|
||||||
|
showSuccess(t('polls', 'Deleted share and votes for {displayName}', { displayName: share.displayName }))
|
||||||
|
} else if (share.voted && !share.revoked) {
|
||||||
|
this.revokeShare({ share })
|
||||||
|
showSuccess(t('polls', 'Share for user {displayName} revoked', { displayName: share.displayName }))
|
||||||
|
} else {
|
||||||
|
this.deleteShare({ share })
|
||||||
|
showSuccess(t('polls', 'Deleted share for user {displayName}', { displayName: share.displayName }))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showError(t('polls', 'Error deleting or revoking share for user {displayName}', { displayName: share.displayName }))
|
||||||
|
console.error('Error deleting or revoking share', { share }, e.response)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async writeLabel() {
|
async writeLabel() {
|
||||||
this.setLabel({ token: this.share.token, displayName: this.share.displayName })
|
this.setLabel({ token: this.share.token, displayName: this.share.displayName })
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,9 +30,9 @@
|
||||||
<ShareItemAllUsers v-if="allowAllAccess" />
|
<ShareItemAllUsers v-if="allowAllAccess" />
|
||||||
<SharePublicAdd v-if="allowPublicShares" />
|
<SharePublicAdd v-if="allowPublicShares" />
|
||||||
|
|
||||||
<div v-if="invitationShares.length" class="shares-list shared">
|
<div v-if="activeShares.length" class="shares-list shared">
|
||||||
<TransitionGroup :css="false" tag="div">
|
<TransitionGroup :css="false" tag="div">
|
||||||
<ShareItem v-for="(share) in invitationShares"
|
<ShareItem v-for="(share) in activeShares"
|
||||||
:key="share.id"
|
:key="share.id"
|
||||||
:share="share"
|
:share="share"
|
||||||
@show-qr-code="openQrModal(share)" />
|
@show-qr-code="openQrModal(share)" />
|
||||||
|
@ -94,7 +94,7 @@ export default {
|
||||||
pollDescription: (state) => state.poll.description,
|
pollDescription: (state) => state.poll.description,
|
||||||
}),
|
}),
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
invitationShares: 'shares/invitation',
|
activeShares: 'shares/active',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!--
|
||||||
|
- @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>
|
||||||
|
<ConfigBox v-if="revokedShares.length" :title="t('polls', 'Revoked shares')">
|
||||||
|
<template #icon>
|
||||||
|
<DeletedIcon />
|
||||||
|
</template>
|
||||||
|
<TransitionGroup :css="false" tag="div" class="shares-list">
|
||||||
|
<ShareItem v-for="(share) in revokedShares"
|
||||||
|
:key="share.id"
|
||||||
|
:share="share" />
|
||||||
|
</TransitionGroup>
|
||||||
|
</ConfigBox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions, mapState } from 'vuex'
|
||||||
|
import { ConfigBox } from '../Base/index.js'
|
||||||
|
import DeletedIcon from 'vue-material-design-icons/Delete.vue'
|
||||||
|
import ShareItem from './ShareItem.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SharesListRevoked',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
DeletedIcon,
|
||||||
|
ConfigBox,
|
||||||
|
ShareItem,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
pollId: (state) => state.poll.id,
|
||||||
|
}),
|
||||||
|
|
||||||
|
...mapGetters({
|
||||||
|
revokedShares: 'shares/revoked',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
removeShare: 'shares/delete',
|
||||||
|
inviteAll: 'shares/inviteAll',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -24,6 +24,7 @@
|
||||||
<div class="sidebar-share">
|
<div class="sidebar-share">
|
||||||
<SharesListUnsent class="shares unsent" />
|
<SharesListUnsent class="shares unsent" />
|
||||||
<SharesList class="shares effective" />
|
<SharesList class="shares effective" />
|
||||||
|
<SharesListRevoked class="shares" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import SharesList from '../Shares/SharesList.vue'
|
import SharesList from '../Shares/SharesList.vue'
|
||||||
import SharesListUnsent from '../Shares/SharesListUnsent.vue'
|
import SharesListUnsent from '../Shares/SharesListUnsent.vue'
|
||||||
|
import SharesListRevoked from '../Shares/SharesListRevoked.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SideBarTabShare',
|
name: 'SideBarTabShare',
|
||||||
|
@ -38,6 +40,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
SharesList,
|
SharesList,
|
||||||
SharesListUnsent,
|
SharesListUnsent,
|
||||||
|
SharesListRevoked,
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -31,7 +31,8 @@
|
||||||
:show-user-status="showUserStatus"
|
:show-user-status="showUserStatus"
|
||||||
:user="avatarUserId"
|
:user="avatarUserId"
|
||||||
:display-name="name"
|
:display-name="name"
|
||||||
:is-no-user="isNoUser">
|
:is-no-user="isNoUser"
|
||||||
|
@click="showMenu()">
|
||||||
<template v-if="useIconSlot" #icon>
|
<template v-if="useIconSlot" #icon>
|
||||||
<LinkIcon v-if="type==='public'" :size="mdIconSize" />
|
<LinkIcon v-if="type==='public'" :size="mdIconSize" />
|
||||||
<InternalLinkIcon v-if="type==='internalAccess'" :size="mdIconSize" />
|
<InternalLinkIcon v-if="type==='internalAccess'" :size="mdIconSize" />
|
||||||
|
@ -124,6 +125,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
forcedDescription: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'user',
|
default: 'user',
|
||||||
|
@ -178,6 +183,7 @@ export default {
|
||||||
return !['user', 'admin'].includes(this.type)
|
return !['user', 'admin'].includes(this.type)
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
|
if (this.forcedDescription) return this.forcedDescription
|
||||||
if (this.type === 'admin') return t('polls', 'Is granted admin rights for this poll')
|
if (this.type === 'admin') return t('polls', 'Is granted admin rights for this poll')
|
||||||
if (this.displayEmailAddress) return this.displayEmailAddress
|
if (this.displayEmailAddress) return this.displayEmailAddress
|
||||||
return ''
|
return ''
|
||||||
|
|
|
@ -27,6 +27,7 @@ const defaultShares = () => ({
|
||||||
displayName: '',
|
displayName: '',
|
||||||
id: null,
|
id: null,
|
||||||
invitationSent: 0,
|
invitationSent: 0,
|
||||||
|
revoked: 0,
|
||||||
pollId: null,
|
pollId: null,
|
||||||
token: '',
|
token: '',
|
||||||
type: '',
|
type: '',
|
||||||
|
|
|
@ -60,17 +60,25 @@ const mutations = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
invitation: (state) => {
|
active: (state) => {
|
||||||
// share types, which will be active, after the user gets his invitation
|
// share types, which will be active, after the user gets his invitation
|
||||||
const invitationTypes = ['email', 'external', 'contact']
|
const invitationTypes = ['email', 'external', 'contact']
|
||||||
// sharetype which are active without sending an invitation
|
// sharetype which are active without sending an invitation
|
||||||
const directShareTypes = ['user', 'group', 'admin', 'public']
|
const directShareTypes = ['user', 'group', 'admin', 'public']
|
||||||
return state.list.filter((share) => (invitationTypes.includes(share.type) && (share.type === 'external' || share.invitationSent)) || directShareTypes.includes(share.type))
|
return state.list.filter((share) => (!share.revoked
|
||||||
|
&& (directShareTypes.includes(share.type)
|
||||||
|
|| (invitationTypes.includes(share.type) && (share.type === 'external' || share.invitationSent || share.voted))
|
||||||
|
)
|
||||||
|
))
|
||||||
},
|
},
|
||||||
|
|
||||||
unsentInvitations: (state) => state.list.filter((share) => (share.emailAddress || share.type === 'group' || share.type === 'contactGroup' || share.type === 'circle') && !share.invitationSent),
|
revoked: (state) => state.list.filter((share) => (!!share.revoked)),
|
||||||
|
unsentInvitations: (state) => state.list.filter((share) =>
|
||||||
|
(share.emailAddress || share.type === 'group' || share.type === 'contactGroup' || share.type === 'circle')
|
||||||
|
&& !share.invitationSent && !share.revoked && !share.voted),
|
||||||
public: (state) => state.list.filter((share) => ['public'].includes(share.type)),
|
public: (state) => state.list.filter((share) => ['public'].includes(share.type)),
|
||||||
hasShares: (state) => state.list.length > 0,
|
hasShares: (state) => state.list.length > 0,
|
||||||
|
hasRevoked: (state, getters) => getters.revoked.length > 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
@ -173,6 +181,32 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async revoke(context, payload) {
|
||||||
|
// context.commit('delete', { share: payload.share })
|
||||||
|
try {
|
||||||
|
await SharesAPI.revokeShare(payload.share.token)
|
||||||
|
context.dispatch('list')
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.code === 'ERR_CANCELED') return
|
||||||
|
console.error('Error revoking share', { error: e.response }, { payload })
|
||||||
|
context.dispatch('list')
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async reRevoke(context, payload) {
|
||||||
|
// context.commit('delete', { share: payload.share })
|
||||||
|
try {
|
||||||
|
await SharesAPI.reRevokeShare(payload.share.token)
|
||||||
|
context.dispatch('list')
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.code === 'ERR_CANCELED') return
|
||||||
|
console.error('Error re-revoking share', { error: e.response }, { payload })
|
||||||
|
context.dispatch('list')
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async delete(context, payload) {
|
async delete(context, payload) {
|
||||||
context.commit('delete', { share: payload.share })
|
context.commit('delete', { share: payload.share })
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -60,7 +60,6 @@ const getters = {
|
||||||
relevant: (state, getters, rootState) => state.list.filter((vote) => rootState.options.list.some((option) => option.pollId === vote.pollId && option.text === vote.optionText)),
|
relevant: (state, getters, rootState) => state.list.filter((vote) => rootState.options.list.some((option) => option.pollId === vote.pollId && option.text === vote.optionText)),
|
||||||
countVotes: (state, getters, rootState) => (answer) => getters.relevant.filter((vote) => vote.user.userId === rootState.poll.acl.userId && vote.answer === answer).length,
|
countVotes: (state, getters, rootState) => (answer) => getters.relevant.filter((vote) => vote.user.userId === rootState.poll.acl.userId && vote.answer === answer).length,
|
||||||
countAllVotes: (state, getters) => (answer) => getters.relevant.filter((vote) => vote.answer === answer).length,
|
countAllVotes: (state, getters) => (answer) => getters.relevant.filter((vote) => vote.answer === answer).length,
|
||||||
hasVoted: (state) => (userId) => state.list.findIndex((vote) => vote.user.userId === userId) > -1,
|
|
||||||
hasVotes: (state) => state.list.length > 0,
|
hasVotes: (state) => state.list.length > 0,
|
||||||
|
|
||||||
getVote: (state) => (payload) => {
|
getVote: (state) => (payload) => {
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NcAppSidebar :active.sync="activeTab"
|
<NcAppSidebar v-show="showSidebar"
|
||||||
|
:active.sync="activeTab"
|
||||||
:title="t('polls', 'Details')"
|
:title="t('polls', 'Details')"
|
||||||
@close="closeSideBar()">
|
@close="closeSideBar()">
|
||||||
<NcAppSidebarTab v-if="acl.allowEdit"
|
<NcAppSidebarTab v-if="acl.allowEdit"
|
||||||
|
@ -112,6 +113,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeTab: t('polls', 'Comments').toLowerCase(),
|
activeTab: t('polls', 'Comments').toLowerCase(),
|
||||||
|
showSidebar: (window.innerWidth > 920),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -129,10 +131,16 @@ export default {
|
||||||
subscribe('polls:sidebar:changeTab', (payload) => {
|
subscribe('polls:sidebar:changeTab', (payload) => {
|
||||||
this.activeTab = payload?.activeTab ?? this.activeTab
|
this.activeTab = payload?.activeTab ?? this.activeTab
|
||||||
})
|
})
|
||||||
|
|
||||||
|
subscribe('polls:sidebar:toggle', (payload) => {
|
||||||
|
emit('polls:sidebar:changeTab', { activeTab: payload?.activeTab })
|
||||||
|
this.showSidebar = payload?.open ?? !this.showSidebar
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
unsubscribe('polls:sidebar:changeTab')
|
unsubscribe('polls:sidebar:changeTab')
|
||||||
|
unsubscribe('polls:sidebar:toggle')
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче