Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
- Configuration for using the Nextcloud default footer in public polls
- Delete polls without the need to archive them first
- Collapsible poll description
- Transfer polls to another owner by the current poll owner or the administration

## [7.4.1] - 2024-03-07
### New
Expand Down
2 changes: 0 additions & 2 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
use OCA\Polls\Event\PollOwnerChangeEvent;
use OCA\Polls\Event\PollReopenEvent;
use OCA\Polls\Event\PollRestoredEvent;
use OCA\Polls\Event\PollTakeoverEvent;
use OCA\Polls\Event\PollUpdatedEvent;
use OCA\Polls\Event\ShareChangedDisplayNameEvent;
use OCA\Polls\Event\ShareChangedEmailEvent;
Expand Down Expand Up @@ -106,7 +105,6 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(PollOptionReorderedEvent::class, PollListener::class);
$context->registerEventListener(PollOwnerChangeEvent::class, PollListener::class);
$context->registerEventListener(PollRestoredEvent::class, PollListener::class);
$context->registerEventListener(PollTakeoverEvent::class, PollListener::class);
$context->registerEventListener(PollUpdatedEvent::class, PollListener::class);
$context->registerEventListener(PollReopenEvent::class, PollListener::class);
$context->registerEventListener(PollCloseEvent::class, PollListener::class);
Expand Down
18 changes: 9 additions & 9 deletions lib/Controller/PollApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,26 +159,26 @@ public function clone(int $pollId): DataResponse {

/**
* Transfer all polls from one user to another (change owner of poll)
* @param string $sourceUser User to transfer polls from
* @param string $targetUser User to transfer polls to
* @param string $sourceUserId User id to transfer polls from
* @param string $targetUserId User id to transfer polls to
*/
#[CORS]
#[NoCSRFRequired]
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/transfer/{sourceUser}/{targetUser}', requirements: ['apiVersion' => '(v2)'])]
public function transferPolls(string $sourceUser, string $targetUser): DataResponse {
return $this->response(fn () => ['transferred' => $this->pollService->transferPolls($sourceUser, $targetUser)]);
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/transfer/{sourceUserId}/{targetUserId}', requirements: ['apiVersion' => '(v2)'])]
public function transferPolls(string $sourceUserId, string $targetUserId): DataResponse {
return $this->response(fn () => ['transferred' => $this->pollService->transferPolls($sourceUserId, $targetUserId)]);
}

/**
* Transfer single poll to another user (change owner of poll)
* @param int $pollId Poll to transfer
* @param string $targetUser User to transfer the poll to
* @param string $targetUserId User id to transfer the poll to
*/
#[CORS]
#[NoCSRFRequired]
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/{pollId}/transfer/{targetUser}', requirements: ['apiVersion' => '(v2)'])]
public function transferPoll(int $pollId, string $targetUser): DataResponse {
return $this->response(fn () => ['transferred' => $this->pollService->transferPoll($pollId, $targetUser)]);
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/{pollId}/transfer/{targetUserId}', requirements: ['apiVersion' => '(v2)'])]
public function transferPoll(int $pollId, string $targetUserId): DataResponse {
return $this->response(fn () => ['transferred' => $this->pollService->transferPoll($pollId, $targetUserId)]);
}

/**
Expand Down
21 changes: 16 additions & 5 deletions lib/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,23 @@ private function clonePoll(int $pollId): Poll {

/**
* Transfer polls between users
* @param string $sourceUser User to transfer polls from
* @param string $targetUser User to transfer polls to
* @param string $sourceUserId User id to transfer polls from
* @param string $targetUserId User id to transfer polls to
*/
#[FrontpageRoute(verb: 'PUT', url: '/poll/transfer/{sourceUser}/{targetUser}')]
public function transferPolls(string $sourceUser, string $targetUser): JSONResponse {
return $this->response(fn () => $this->pollService->transferPolls($sourceUser, $targetUser));
#[FrontpageRoute(verb: 'PUT', url: '/poll/transfer/{sourceUserId}/{targetUserId}')]
public function transferPolls(string $sourceUserId, string $targetUserId): JSONResponse {
return $this->response(fn () => $this->pollService->transferPolls($sourceUserId, $targetUserId));
}

/**
* Transfer ownership of one poll
* @param int $pollId poll to transfer ownership
* @param string $targetUserId User to transfer polls to
*/
#[NoAdminRequired]
#[FrontpageRoute(verb: 'PUT', url: '/poll/{pollId}/changeowner/{targetUserId}')]
public function changeOwner(int $pollId, string $targetUserId): JSONResponse {
return $this->response(fn () => $this->pollService->transferPoll($pollId, $targetUserId));
}

/**
Expand Down
9 changes: 6 additions & 3 deletions lib/Controller/SystemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ public function __construct(
#[NoAdminRequired]
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
#[FrontpageRoute(verb: 'GET', url: '/search/users/{query}')]
public function userSearch(string $query = ''): JSONResponse {
return new JSONResponse(['siteusers' => $this->systemService->getSiteUsersAndGroups(
$query)], Http::STATUS_OK);
public function userSearch(string $query, string $types): JSONResponse {
$types = explode(',', $types);
return new JSONResponse([
'siteusers' => $this->systemService->getSiteUsersAndGroups($query, $types),
'types' => $types
], Http::STATUS_OK);
}
/**
* Get a combined list of all NC groups
Expand Down
18 changes: 17 additions & 1 deletion lib/Db/Poll.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class Poll extends EntityWithUser implements JsonSerializable {
public const PERMISSION_OVERRIDE = 'override_permission';
public const PERMISSION_POLL_VIEW = 'view';
public const PERMISSION_POLL_EDIT = 'edit';
public const PERMISSION_POLL_CHANGE_OWNER = 'changeOwner';
public const PERMISSION_POLL_DELETE = 'delete';
public const PERMISSION_POLL_ARCHIVE = 'archive';
public const PERMISSION_POLL_RESULTS_VIEW = 'seeResults';
Expand Down Expand Up @@ -291,6 +292,7 @@ public function getPermissionsArray(): array {
'addSharesExternal' => $this->getIsAllowed(self::PERMISSION_SHARE_ADD_EXTERNAL),
'archive' => $this->getIsAllowed(self::PERMISSION_POLL_ARCHIVE),
'changeForeignVotes' => $this->getIsAllowed(self::PERMISSION_VOTE_FOREIGN_CHANGE),
'changeOwner' => $this->getIsAllowed(self::PERMISSION_POLL_CHANGE_OWNER),
'clone' => $this->getIsAllowed(self::PERMISSION_OPTION_CLONE),
'comment' => $this->getIsAllowed(self::PERMISSION_COMMENT_ADD),
'confirmOptions' => $this->getIsAllowed(self::PERMISSION_OPTION_CONFIRM),
Expand All @@ -302,6 +304,7 @@ public function getPermissionsArray(): array {
'seeUsernames' => $this->getIsAllowed(self::PERMISSION_POLL_USERNAMES_VIEW),
'shiftOptions' => $this->getIsAllowed(self::PERMISSION_OPTIONS_SHIFT),
'subscribe' => $this->getIsAllowed(self::PERMISSION_POLL_SUBSCRIBE),
'takeOver' => $this->getIsAllowed(self::PERMISSION_POLL_TAKEOVER),
'view' => $this->getIsAllowed(self::PERMISSION_POLL_VIEW),
'vote' => $this->getIsAllowed(self::PERMISSION_VOTE_EDIT),
];
Expand Down Expand Up @@ -560,7 +563,8 @@ public function getIsAllowed(string $permission): bool {
self::PERMISSION_POLL_EDIT => $this->getAllowEditPoll(),
self::PERMISSION_POLL_DELETE => $this->getAllowDeletePoll(),
self::PERMISSION_POLL_ARCHIVE => $this->getAllowEditPoll(),
self::PERMISSION_POLL_TAKEOVER => $this->getAllowEditPoll(),
self::PERMISSION_POLL_TAKEOVER => $this->getAllowTakeOver(),
self::PERMISSION_POLL_CHANGE_OWNER => $this->getAllowChangeOwner(),
self::PERMISSION_POLL_SUBSCRIBE => $this->getAllowSubscribeToPoll(),
self::PERMISSION_POLL_RESULTS_VIEW => $this->getAllowShowResults(),
self::PERMISSION_POLL_USERNAMES_VIEW => $this->getAllowEditPoll() || !$this->getAnonymous(),
Expand Down Expand Up @@ -669,6 +673,18 @@ private function getAllowEditPoll(): bool {
return false;
}

private function getAllowTakeOver(): bool {
return $this->userSession->getCurrentUser()->getIsAdmin();
}

/**
* Checks, if user is allowed to edit the poll configuration
**/
private function getAllowChangeOwner(): bool {
return $this->getAllowEditPoll()
|| $this->userSession->getCurrentUser()->getIsAdmin();
}

/**
* Checks, if user is allowed to access (view) poll
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/Event/PollOwnerChangeEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@
namespace OCA\Polls\Event;

use OCA\Polls\Db\Poll;
use OCA\Polls\Notification\Notifier;

class PollOwnerChangeEvent extends PollEvent {
public function __construct(
protected Poll $poll,
protected string $oldOwner,
protected string $newOwner,
) {
parent::__construct($poll);
$this->eventId = self::OWNER_CHANGE;
}
public function getNotification(): array {
return [
'msgId' => Notifier::NOTIFY_POLL_CHANGED_OWNER,
'objectType' => 'poll',
'objectValue' => $this->getPollId(),
'recipient' => $this->oldOwner,
'newOwner' => $this->newOwner,
'actor' => $this->getActor(),
'pollTitle' => $this->getPollTitle(),
];
}
}
30 changes: 0 additions & 30 deletions lib/Event/PollTakeoverEvent.php

This file was deleted.

4 changes: 1 addition & 3 deletions lib/Listener/BaseListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use OCA\Polls\Event\PollDeletedEvent;
use OCA\Polls\Event\PollExpiredEvent;
use OCA\Polls\Event\PollOwnerChangeEvent;
use OCA\Polls\Event\PollTakeoverEvent;
use OCA\Polls\Exceptions\InvalidClassException;
use OCA\Polls\Exceptions\OCPEventException;
use OCA\Polls\Model\Settings\AppSettings;
Expand Down Expand Up @@ -94,8 +93,7 @@ protected function checkClass() : void {

protected function updateLastInteraction(): void {
// Update last interaction, exept event is one of the of excluded events
if ($this->event instanceof PollTakeoverEvent
|| $this->event instanceof PollOwnerChangeEvent
if ($this->event instanceof PollOwnerChangeEvent
|| $this->event instanceof PollExpiredEvent
|| $this->event instanceof PollDeletedEvent
|| $this->event instanceof PollArchivedEvent
Expand Down
72 changes: 47 additions & 25 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
use Psr\Log\LoggerInterface;

class Notifier implements INotifier {
public const NOTIFY_POLL_DELETED_BY_OTHER = 'deletePollByOther';
public const NOTIFY_POLL_ARCHIVED_BY_OTHER = 'softDeletePollByOther';
public const NOTIFY_POLL_TAKEOVER = 'takeOverPoll';
public const NOTIFY_POLL_CHANGED_OWNER = 'PollChangedOwner';
public const NOTIFY_INVITATION = 'invitation';
private const SUBJECT_PARSED = 'parsedSubject';
private const SUBJECT_RICH = 'richSubject';
Expand All @@ -33,6 +35,7 @@ public function __construct(
protected PollMapper $pollMapper,
private UserMapper $userMapper,
private NotificationService $notificationService,
private LoggerInterface $logger,
) {
}

Expand All @@ -50,22 +53,6 @@ public function getName(): string {
return $this->l10nFactory->get(AppConstants::APP_ID)->t('Polls');
}

/**
* @return string[][]
*
* @psalm-return array{actor: array{type: 'user', id: string, name: string}}
*/
private function getActor(string $actorId): array {
$actor = $this->userMapper->getUserFromUserBase($actorId);
return [
'actor' => [
'type' => 'user',
'id' => $actor->getId(),
'name' => $actor->getDisplayName(),
]
];
}

public function prepare(INotification $notification, string $languageCode): INotification {
$l = $this->l10nFactory->get(AppConstants::APP_ID, $languageCode);
if ($notification->getApp() !== AppConstants::APP_ID) {
Expand All @@ -81,37 +68,72 @@ public function prepare(INotification $notification, string $languageCode): INot

try {
$poll = $this->pollMapper->get(intval($notification->getObjectId()));
$actor = $this->getActor($parameters['actor'] ?? $poll->getOwner());
$pollTitle = $poll->getTitle();
$notification->setLink($poll->getVoteUrl());
} catch (DoesNotExistException $e) {
$this->logger->info('Notification silently removed, poll not found', [
'notification' => $notification->getObjectId(),
'error' => $e->getMessage(),
]);
$this->notificationService->removeNotification(intval($notification->getObjectId()));
return $notification;
}

$actor = $this->userMapper->getUserFromUserBase($parameters['actor'] ?? $poll->getOwner());
$pollTitle = $parameters['pollTitle'] ?? $poll->getTitle();
$notification->setLink($poll->getVoteUrl());

// TODO: tidy subjects and parameters
$subjects = match ($notification->getSubject()) {
self::NOTIFY_INVITATION => [
self::SUBJECT_PARSED => $l->t('%s invited you to a poll', $actor['actor']['name']),
self::SUBJECT_PARSED => $l->t('%s invited you to a poll', $actor->getDisplayName()),
self::SUBJECT_RICH => $l->t('{actor} has invited you to the poll "%s".', $pollTitle),
],
self::NOTIFY_POLL_TAKEOVER => [
self::SUBJECT_PARSED => $l->t('%s took over your poll', $actor['actor']['name']),
self::SUBJECT_PARSED => $l->t('%s took over your poll', $actor->getDisplayName()),
self::SUBJECT_RICH => $l->t('{actor} took over your poll "%s" and is the new owner.', $pollTitle),
],
self::NOTIFY_POLL_CHANGED_OWNER => [
self::SUBJECT_PARSED => $l->t('%s is the new owner of your poll. ', $parameters['newOwner']),
self::SUBJECT_RICH => $l->t('{actor} transfered your poll "%s" to {newOwner}. You are no more the owner.', $pollTitle),
],
self::NOTIFY_POLL_DELETED_BY_OTHER => [
self::SUBJECT_PARSED => $l->t('%s deleted your poll', $actor['actor']['name']),
self::SUBJECT_PARSED => $l->t('%s deleted your poll', $actor->getDisplayName()),
self::SUBJECT_RICH => $l->t('{actor} deleted your poll "%s".', $pollTitle),
],
self::NOTIFY_POLL_ARCHIVED_BY_OTHER => [
self::SUBJECT_PARSED => $l->t('%s archived your poll', $actor['actor']['name']),
self::SUBJECT_PARSED => $l->t('%s archived your poll', $actor->getDisplayName()),
self::SUBJECT_RICH => $l->t('{actor} archived your poll "%s".', $pollTitle),
],
// Unknown subject => Unknown notification => throw
default => throw new \InvalidArgumentException(),
};

$notification->setParsedSubject($subjects[self::SUBJECT_PARSED]);
$notification->setRichSubject($subjects[self::SUBJECT_RICH], $actor);
switch ($notification->getSubject()) {
case self::NOTIFY_POLL_CHANGED_OWNER:
$newOwner = $this->userMapper->getUserFromUserBase($parameters['newOwner']);
// overwrite the subject with the new owner
$notification->setParsedSubject(
$l->t('%s is the new owner of your poll. ', $newOwner->getDisplayName())
);

$notification->setRichSubject(
$subjects[self::SUBJECT_RICH],
[
'actor' => $actor->getRichObjectString(),
'newOwner' => $newOwner->getRichObjectString(),
]
);
break;

default:
$notification->setParsedSubject($subjects[self::SUBJECT_PARSED]);
$notification->setRichSubject(
$subjects[self::SUBJECT_RICH],
[
'actor' => $actor->getRichObjectString(),
]
);
break;
}

return $notification;
}
Expand Down
Loading
Loading