Skip to content
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
80e9a7f
chore: stop search early if haven't values to search for
vitormattos Sep 22, 2025
5598b8c
feat: Search by signers
vitormattos Sep 22, 2025
721e7e3
fix: add pendign param
vitormattos Sep 22, 2025
49325a8
chore: update openapi
vitormattos Sep 22, 2025
b6a1fc3
fix: remove todo and fix method to use twofactor_gateway
vitormattos Sep 22, 2025
9f31207
chore: remove direct access to property and implement getter
vitormattos Sep 22, 2025
9dee22c
fix: don't return not allowed settings
vitormattos Sep 22, 2025
618feda
chore: add setter and getter to name
vitormattos Sep 22, 2025
6ed2ae4
fix: make more generic
vitormattos Sep 22, 2025
c46471c
feat: change way to add a signer, use a modal to be possible split
vitormattos Sep 22, 2025
3da5031
chore: add signal svg
vitormattos Sep 22, 2025
5096129
fix: cs
vitormattos Sep 22, 2025
6e4ae67
feat: send twofactor_gateway notification
vitormattos Sep 22, 2025
939c639
feat: implement gateways
vitormattos Sep 22, 2025
ace3a36
fix: psalm
vitormattos Sep 22, 2025
72a4acb
fix: copy paste code, added real code
vitormattos Sep 22, 2025
91d6dd7
fix: psalm update baseline
vitormattos Sep 22, 2025
20ba69d
fix: send code to sign
vitormattos Sep 22, 2025
75cd9ad
fix: linter
vitormattos Sep 22, 2025
9ed9790
chore: add docblock
vitormattos Sep 22, 2025
fe0865f
fix: cs
vitormattos Sep 22, 2025
37ead7f
fix: psalm
vitormattos Sep 22, 2025
8cb8f94
fix: typo
vitormattos Sep 22, 2025
e671cf5
fix: typo
vitormattos Sep 22, 2025
560bf36
fix: error handler
vitormattos Sep 22, 2025
70680c5
fix: psalm update baseline
vitormattos Sep 22, 2025
fd9dc8b
fix: prevent error when class doesn't exists
vitormattos Sep 23, 2025
7c12c0a
fix: revert default method.
vitormattos Sep 23, 2025
49e3bb2
fix: remove unused interface
vitormattos Sep 23, 2025
b41bb04
chore: resolve code review comment
vitormattos Sep 23, 2025
501a139
fix: only consider the search item when have a method
vitormattos Sep 23, 2025
dcca860
fix: validate the email address when is searching by account
vitormattos Sep 23, 2025
aba92b9
chore: indent code
vitormattos Sep 23, 2025
73814be
fix: typo
vitormattos Sep 23, 2025
c8c593f
chore: replace shareType by method
vitormattos Sep 23, 2025
08f6dda
fix: filter valid condition
vitormattos Sep 23, 2025
b244d4f
fix: code review
vitormattos Sep 23, 2025
548a39a
fix: code review
vitormattos Sep 23, 2025
62e81b9
fix: code review
vitormattos Sep 23, 2025
5a22bd0
fix: previous change was wrong
vitormattos Sep 23, 2025
889003c
fix: openapi
vitormattos Sep 23, 2025
610eb24
fix: prevent warning when haven't an email
vitormattos Sep 23, 2025
4833fbb
fix: the method key was removed and I added back
vitormattos Sep 23, 2025
5fdcf43
fix: the match syntax was incorrect
vitormattos Sep 23, 2025
1160db3
fix: enlarge the dialog size
vitormattos Sep 23, 2025
3e4fcd8
chore: use a more readdable code
vitormattos Sep 23, 2025
b7412c0
chore: replace string svg by a real file
vitormattos Sep 23, 2025
81c4ad1
fix: psalm update baseline
vitormattos Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions img/logo-signal-app.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use OCA\Libresign\Listener\MailNotifyListener;
use OCA\Libresign\Listener\NotificationListener;
use OCA\Libresign\Listener\SignedCallbackListener;
use OCA\Libresign\Listener\TwofactorGatewayListener;
use OCA\Libresign\Listener\UserDeletedListener;
use OCA\Libresign\Middleware\GlobalInjectionMiddleware;
use OCA\Libresign\Middleware\InjectionMiddleware;
Expand Down Expand Up @@ -80,6 +81,10 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(SendSignNotificationEvent::class, MailNotifyListener::class);
$context->registerEventListener(SignedEvent::class, MailNotifyListener::class);

// TwofactorGateway listener
$context->registerEventListener(SendSignNotificationEvent::class, TwofactorGatewayListener::class);
$context->registerEventListener(SignedEvent::class, TwofactorGatewayListener::class);

$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
}
}
100 changes: 100 additions & 0 deletions lib/Collaboration/Collaborators/SignerPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Libresign\Collaboration\Collaborators;

use OCA\Libresign\Db\IdentifyMethodMapper;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\IUserSession;

class SignerPlugin implements ISearchPlugin {
public const TYPE_SIGNER = 50; // IShare::TYPE_SIGNER = 50; It's a custom share type. Not defined in OCP\Share\IShare
public static string $method = '';

public function __construct(
protected IdentifyMethodMapper $identifyMethodMapper,
private IUserSession $userSession,
) {
}

public static function setMethod(string $method): void {
self::$method = $method;
}

/**
* {@inheritdoc}
*/
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
$user = $this->userSession->getUser()->getUID();

$limit++;
$identifiers = $this->identifyMethodMapper->searchByIdentifierValue(
$search,
$user,
self::$method,
$limit,
$offset,
);

$result = ['wide' => [], 'exact' => []];

$hasMore = false;
if (count($identifiers) > $limit) {
$hasMore = true;
array_pop($identifiers);
}

foreach ($identifiers as $row) {
$item = $this->rowToSearchResultItem($row);
if (strtolower($row['identifier_value']) === strtolower($search)
|| strtolower($row['display_name']) === strtolower($search)
) {
$result['exact'][] = $item;
} else {
$result['wide'][] = $item;
}
}

if (!count($identifiers) && self::$method && !$this->canValidateMethod()) {
$result['exact'][] = [
'label' => $search,
'shareWithDisplayNameUnique' => $search,
'method' => self::$method,
'value' => [
'shareWith' => $search,
'shareType' => self::TYPE_SIGNER,
],
];
}

$type = new SearchResultType('signer');
$searchResult->addResultSet($type, $result['wide'], $result['exact']);

return $hasMore;
}

private function canValidateMethod(): bool {
return in_array(self::$method, ['email', 'account'], true);
}

private function rowToSearchResultItem(array $row): array {
$item = [
'label' => $row['display_name'],
'shareWithDisplayNameUnique' => $row['identifier_value'],
'method' => $row['identifier_key'],
'value' => [
'shareWith' => $row['identifier_value'],
'shareType' => self::TYPE_SIGNER,
]
];

return $item;
}
}
2 changes: 1 addition & 1 deletion lib/Controller/FileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ private function fetchPreview(
/**
* Send a file
*
* Send a new file to Nextcloud and return the fileId to request to sign usign fileId
* Send a new file to Nextcloud and return the fileId to request to signature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Send a new file to Nextcloud and return the fileId to request to signature
* Send a new file to Nextcloud and return the fileId to request signature

*
* @param LibresignNewFile $file File to save
* @param string $name The name of file to sign
Expand Down
99 changes: 80 additions & 19 deletions lib/Controller/IdentifyAccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\Libresign\Controller;

use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Collaboration\Collaborators\SignerPlugin;
use OCA\Libresign\Middleware\Attribute\RequireManager;
use OCA\Libresign\ResponseDefinitions;
use OCA\Libresign\Service\IdentifyMethod\Account;
Expand Down Expand Up @@ -45,6 +46,7 @@ public function __construct(
* Used to identify who can sign the document. The return of this endpoint is related with Administration Settiongs > LibreSign > Identify method.
*
* @param string $search search params
* @param string $method filter by method (email, account, sms, signal, telegram, whatsapp, xmpp)
* @param int $page the number of page to return. Default: 1
* @param int $limit Total of elements to return. Default: 25
* @return DataResponse<Http::STATUS_OK, LibresignIdentifyAccount[], array{}>
Expand All @@ -55,29 +57,43 @@ public function __construct(
#[NoAdminRequired]
#[RequireManager]
#[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/identify-account/search', requirements: ['apiVersion' => '(v1)'])]
public function search(string $search = '', int $page = 1, int $limit = 25): DataResponse {
$shareTypes = $this->getShareTypes();
$lookup = false;

// only search for string larger than a given threshold
$threshold = 1;
if (strlen($search) < $threshold) {
public function search(string $search = '', string $method = '', int $page = 1, int $limit = 25): DataResponse {
// only search for string larger than a minimum length
if (strlen($search) < 1) {
return new DataResponse();
}

$shareTypes = $this->getShareTypes();
$lookup = false;

$offset = $limit * ($page - 1);
$this->registerPlugin($method);
[$result] = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $limit, $offset);
$result['exact'] = $this->unifyResult($result['exact']);
$result = $this->unifyResult($result);
$result = $this->excludeEmptyShareWith($result);
$return = $this->formatForNcSelect($result);
$return = $this->addHerselfAccount($return, $search);
$return = $this->addHerselfEmail($return, $search);
$return = $this->replaceShareTypeByMethod($return);
$return = $this->excludeNotAllowed($return);

return new DataResponse($return);
}

private function registerPlugin(string $method): void {
SignerPlugin::setMethod($method);

$refObject = new \ReflectionObject($this->collaboratorSearch);
$refProperty = $refObject->getProperty('pluginList');
$refProperty->setAccessible(true);

$plugins = $refProperty->getValue($this->collaboratorSearch);
$plugins[SignerPlugin::TYPE_SIGNER] = [SignerPlugin::class];

$refProperty->setValue($this->collaboratorSearch, $plugins);
}

private function getShareTypes(): array {
if (count($this->shareTypes) > 0) {
return $this->shareTypes;
Expand All @@ -90,6 +106,8 @@ private function getShareTypes(): array {
if ($settings['enabled']) {
$this->shareTypes[] = IShare::TYPE_USER;
}

$this->shareTypes[] = SignerPlugin::TYPE_SIGNER;
return $this->shareTypes;
}

Expand All @@ -109,21 +127,41 @@ private function unifyResult(array $list): array {
}

private function formatForNcSelect(array $list): array {
$return = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$return doesn't sound very descriptive, maybe $formattedList would be better.

foreach ($list as $key => $item) {
$list[$key] = [
$return[$key] = [
'id' => $item['value']['shareWith'],
'isNoUser' => $item['value']['shareType'] !== IShare::TYPE_USER,
'isNoUser' => $item['value']['shareType'] !== IShare::TYPE_USER
&& isset($item['method'])
&& $item['method'] !== 'account',
'displayName' => $item['label'],
'subname' => $item['shareWithDisplayNameUnique'] ?? '',
'shareType' => $item['value']['shareType'],
];
if ($item['value']['shareType'] === IShare::TYPE_EMAIL) {
$list[$key]['icon'] = 'icon-mail';
$return[$key]['method'] = 'email';
$return[$key]['icon'] = 'icon-mail';
} elseif ($item['value']['shareType'] === IShare::TYPE_USER) {
$list[$key]['icon'] = 'icon-user';
$return[$key]['method'] = 'account';
$return[$key]['icon'] = 'icon-user';
} elseif ($item['value']['shareType'] === SignerPlugin::TYPE_SIGNER) {
if (
!isset($return[$key]['method'])
&& empty($return[$key]['method'])
&& !empty($item['key'])
) {
$return[$key]['method'] = $item['key'];
}
if ($item['key'] === 'email') {
$return[$key]['icon'] = 'icon-mail';
} elseif ($item['key'] === 'account') {
$return[$key]['icon'] = 'icon-user';
} else {
$return[$key]['iconSvg'] = 'svg' . ucfirst($item['key']);
$return[$key]['iconName'] = $item['key'];
}
}
}
return $list;
return $return;
}

private function addHerselfAccount(array $return, string $search): array {
Expand All @@ -132,7 +170,14 @@ private function addHerselfAccount(array $return, string $search): array {
return $return;
}
$user = $this->userSession->getUser();
if (!str_contains($user->getUID(), $search) && !str_contains(strtolower($user->getDisplayName()), $search)) {
$search = strtolower($search);
if (!str_contains($user->getUID(), $search)
&& !str_contains(strtolower($user->getDisplayName()), $search)
&& (
$user->getEMailAddress() === null
|| ($user->getEMailAddress() !== null && !str_contains($user->getEMailAddress(), $search))
)
) {
return $return;
}
$filtered = array_filter($return, fn ($i) => $i['id'] === $user->getUID());
Expand All @@ -145,7 +190,7 @@ private function addHerselfAccount(array $return, string $search): array {
'displayName' => $user->getDisplayName(),
'subname' => $user->getEMailAddress(),
'icon' => 'icon-user',
'shareType' => IShare::TYPE_USER,
'method' => 'account',
];
return $return;
}
Expand All @@ -159,7 +204,9 @@ private function addHerselfEmail(array $return, string $search): array {
if (empty($user->getEMailAddress())) {
return $return;
}
if (!str_contains($user->getEMailAddress(), $search) && !str_contains($user->getDisplayName(), $search)) {
if (!str_contains($user->getEMailAddress(), $search)
&& !str_contains($user->getDisplayName(), $search)
) {
return $return;
}
$filtered = array_filter($return, fn ($i) => $i['id'] === $user->getUID());
Expand All @@ -172,7 +219,7 @@ private function addHerselfEmail(array $return, string $search): array {
'displayName' => $user->getDisplayName(),
'subname' => $user->getEMailAddress(),
'icon' => 'icon-mail',
'shareType' => IShare::TYPE_EMAIL,
'method' => 'email',
];
return $return;
}
Expand All @@ -182,7 +229,21 @@ private function excludeEmptyShareWith(array $list): array {
}

private function excludeNotAllowed(array $list): array {
$shareTypes = $this->getShareTypes();
return array_filter($list, fn ($result) => in_array($result['shareType'], $shareTypes));
return array_filter($list, fn ($result) => isset($result['method']) && !empty($result['method']));
}

private function replaceShareTypeByMethod(array $list): array {
foreach ($list as $key => $item) {
if (isset($item['method']) && !empty($item['method'])) {
continue;
}
$list[$key]['method'] = match ($item['shareType']) {
IShare::TYPE_EMAIL => $item['method'] = 'email',
IShare::TYPE_USER => $item['method'] = 'account',
default => $item['method'] = '',
};
unset($list[$key]['shareType']);
}
return $list;
}
}
4 changes: 2 additions & 2 deletions lib/Controller/SignFileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ public function signRenew(string $method): DataResponse {
*
* @param string $uuid UUID of LibreSign file
* @param 'account'|'email'|null $identifyMethod Identify signer method
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, sms, signal, telegram, whatsapp, xmpp
* @param string|null $identify Identify value, i.e. the signer email, account or phone number
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
*
Expand All @@ -246,7 +246,7 @@ public function getCodeUsingUuid(string $uuid, ?string $identifyMethod, ?string
*
* @param int $fileId Id of LibreSign file
* @param 'account'|'email'|null $identifyMethod Identify signer method
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, sms, signal, telegram, whatsapp, xmpp
* @param string|null $identify Identify value, i.e. the signer email, account or phone number
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
*
Expand Down
Loading
Loading