Skip to content

Commit 1207bdb

Browse files
authored
Merge pull request #5459 from LibreSign/feat/sign-using-twofactor_gateway
feat: sign usign twofactor_gateway
2 parents 7dd13a5 + 31970b7 commit 1207bdb

43 files changed

Lines changed: 1158 additions & 234 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

img/logo-signal-app.svg

Lines changed: 1 addition & 0 deletions
Loading

img/logo-telegram-app.svg

Lines changed: 3 additions & 0 deletions
Loading

lib/AppInfo/Application.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use OCA\Libresign\Listener\MailNotifyListener;
2222
use OCA\Libresign\Listener\NotificationListener;
2323
use OCA\Libresign\Listener\SignedCallbackListener;
24+
use OCA\Libresign\Listener\TwofactorGatewayListener;
2425
use OCA\Libresign\Listener\UserDeletedListener;
2526
use OCA\Libresign\Middleware\GlobalInjectionMiddleware;
2627
use OCA\Libresign\Middleware\InjectionMiddleware;
@@ -80,6 +81,10 @@ public function register(IRegistrationContext $context): void {
8081
$context->registerEventListener(SendSignNotificationEvent::class, MailNotifyListener::class);
8182
$context->registerEventListener(SignedEvent::class, MailNotifyListener::class);
8283

84+
// TwofactorGateway listener
85+
$context->registerEventListener(SendSignNotificationEvent::class, TwofactorGatewayListener::class);
86+
$context->registerEventListener(SignedEvent::class, TwofactorGatewayListener::class);
87+
8388
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
8489
}
8590
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Libresign\Collaboration\Collaborators;
10+
11+
use OCA\Libresign\Db\IdentifyMethodMapper;
12+
use OCP\Collaboration\Collaborators\ISearchPlugin;
13+
use OCP\Collaboration\Collaborators\ISearchResult;
14+
use OCP\Collaboration\Collaborators\SearchResultType;
15+
use OCP\IUserSession;
16+
17+
class SignerPlugin implements ISearchPlugin {
18+
public const TYPE_SIGNER = 50; // IShare::TYPE_SIGNER = 50; It's a custom share type. Not defined in OCP\Share\IShare
19+
public static string $method = '';
20+
21+
public function __construct(
22+
protected IdentifyMethodMapper $identifyMethodMapper,
23+
private IUserSession $userSession,
24+
) {
25+
}
26+
27+
public static function setMethod(string $method): void {
28+
self::$method = $method;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
35+
$user = $this->userSession->getUser()->getUID();
36+
37+
$limit++;
38+
$identifiers = $this->identifyMethodMapper->searchByIdentifierValue(
39+
$search,
40+
$user,
41+
self::$method,
42+
$limit,
43+
$offset,
44+
);
45+
46+
$result = ['wide' => [], 'exact' => []];
47+
48+
$hasMore = false;
49+
if (count($identifiers) > $limit) {
50+
$hasMore = true;
51+
array_pop($identifiers);
52+
}
53+
54+
foreach ($identifiers as $row) {
55+
$item = $this->rowToSearchResultItem($row);
56+
if (strtolower($row['identifier_value']) === strtolower($search)
57+
|| strtolower($row['display_name']) === strtolower($search)
58+
) {
59+
$result['exact'][] = $item;
60+
} else {
61+
$result['wide'][] = $item;
62+
}
63+
}
64+
65+
if (empty($identifiers) && self::$method && !$this->canValidateMethod()) {
66+
$result['exact'][] = [
67+
'label' => $search,
68+
'shareWithDisplayNameUnique' => $search,
69+
'method' => self::$method,
70+
'value' => [
71+
'shareWith' => $search,
72+
'shareType' => self::TYPE_SIGNER,
73+
],
74+
];
75+
}
76+
77+
$type = new SearchResultType('signer');
78+
$searchResult->addResultSet($type, $result['wide'], $result['exact']);
79+
80+
return $hasMore;
81+
}
82+
83+
private function canValidateMethod(): bool {
84+
return in_array(self::$method, ['email', 'account'], true);
85+
}
86+
87+
private function rowToSearchResultItem(array $row): array {
88+
$item = [
89+
'label' => $row['display_name'],
90+
'shareWithDisplayNameUnique' => $row['identifier_value'],
91+
'method' => $row['identifier_key'],
92+
'value' => [
93+
'shareWith' => $row['identifier_value'],
94+
'shareType' => self::TYPE_SIGNER,
95+
]
96+
];
97+
98+
return $item;
99+
}
100+
}

lib/Controller/FileController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ private function fetchPreview(
378378
/**
379379
* Send a file
380380
*
381-
* Send a new file to Nextcloud and return the fileId to request to sign usign fileId
381+
* Send a new file to Nextcloud and return the fileId to request signature
382382
*
383383
* @param LibresignNewFile $file File to save
384384
* @param string $name The name of file to sign

lib/Controller/IdentifyAccountController.php

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace OCA\Libresign\Controller;
1010

1111
use OCA\Libresign\AppInfo\Application;
12+
use OCA\Libresign\Collaboration\Collaborators\SignerPlugin;
1213
use OCA\Libresign\Middleware\Attribute\RequireManager;
1314
use OCA\Libresign\ResponseDefinitions;
1415
use OCA\Libresign\Service\IdentifyMethod\Account;
@@ -45,6 +46,7 @@ public function __construct(
4546
* Used to identify who can sign the document. The return of this endpoint is related with Administration Settiongs > LibreSign > Identify method.
4647
*
4748
* @param string $search search params
49+
* @param string $method filter by method (email, account, sms, signal, telegram, whatsapp, xmpp)
4850
* @param int $page the number of page to return. Default: 1
4951
* @param int $limit Total of elements to return. Default: 25
5052
* @return DataResponse<Http::STATUS_OK, LibresignIdentifyAccount[], array{}>
@@ -55,29 +57,43 @@ public function __construct(
5557
#[NoAdminRequired]
5658
#[RequireManager]
5759
#[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/identify-account/search', requirements: ['apiVersion' => '(v1)'])]
58-
public function search(string $search = '', int $page = 1, int $limit = 25): DataResponse {
59-
$shareTypes = $this->getShareTypes();
60-
$lookup = false;
61-
62-
// only search for string larger than a given threshold
63-
$threshold = 1;
64-
if (strlen($search) < $threshold) {
60+
public function search(string $search = '', string $method = '', int $page = 1, int $limit = 25): DataResponse {
61+
// only search for string larger than a minimum length
62+
if (strlen($search) < 1) {
6563
return new DataResponse();
6664
}
6765

66+
$shareTypes = $this->getShareTypes();
67+
$lookup = false;
68+
6869
$offset = $limit * ($page - 1);
70+
$this->registerPlugin($method);
6971
[$result] = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $limit, $offset);
7072
$result['exact'] = $this->unifyResult($result['exact']);
7173
$result = $this->unifyResult($result);
7274
$result = $this->excludeEmptyShareWith($result);
7375
$return = $this->formatForNcSelect($result);
7476
$return = $this->addHerselfAccount($return, $search);
7577
$return = $this->addHerselfEmail($return, $search);
78+
$return = $this->replaceShareTypeByMethod($return);
7679
$return = $this->excludeNotAllowed($return);
7780

7881
return new DataResponse($return);
7982
}
8083

84+
private function registerPlugin(string $method): void {
85+
SignerPlugin::setMethod($method);
86+
87+
$refObject = new \ReflectionObject($this->collaboratorSearch);
88+
$refProperty = $refObject->getProperty('pluginList');
89+
$refProperty->setAccessible(true);
90+
91+
$plugins = $refProperty->getValue($this->collaboratorSearch);
92+
$plugins[SignerPlugin::TYPE_SIGNER] = [SignerPlugin::class];
93+
94+
$refProperty->setValue($this->collaboratorSearch, $plugins);
95+
}
96+
8197
private function getShareTypes(): array {
8298
if (count($this->shareTypes) > 0) {
8399
return $this->shareTypes;
@@ -90,6 +106,8 @@ private function getShareTypes(): array {
90106
if ($settings['enabled']) {
91107
$this->shareTypes[] = IShare::TYPE_USER;
92108
}
109+
110+
$this->shareTypes[] = SignerPlugin::TYPE_SIGNER;
93111
return $this->shareTypes;
94112
}
95113

@@ -109,21 +127,35 @@ private function unifyResult(array $list): array {
109127
}
110128

111129
private function formatForNcSelect(array $list): array {
130+
$formattedList = [];
112131
foreach ($list as $key => $item) {
113-
$list[$key] = [
132+
$formattedList[$key] = [
114133
'id' => $item['value']['shareWith'],
115-
'isNoUser' => $item['value']['shareType'] !== IShare::TYPE_USER,
134+
'isNoUser' => $item['value']['shareType'] !== IShare::TYPE_USER
135+
&& isset($item['method'])
136+
&& $item['method'] !== 'account',
116137
'displayName' => $item['label'],
117138
'subname' => $item['shareWithDisplayNameUnique'] ?? '',
118-
'shareType' => $item['value']['shareType'],
119139
];
120140
if ($item['value']['shareType'] === IShare::TYPE_EMAIL) {
121-
$list[$key]['icon'] = 'icon-mail';
141+
$formattedList[$key]['method'] = 'email';
142+
$formattedList[$key]['icon'] = 'icon-mail';
122143
} elseif ($item['value']['shareType'] === IShare::TYPE_USER) {
123-
$list[$key]['icon'] = 'icon-user';
144+
$formattedList[$key]['method'] = 'account';
145+
$formattedList[$key]['icon'] = 'icon-user';
146+
} elseif ($item['value']['shareType'] === SignerPlugin::TYPE_SIGNER) {
147+
$formattedList[$key]['method'] = $item['method'] ?? '';
148+
if ($item['method'] === 'email') {
149+
$formattedList[$key]['icon'] = 'icon-mail';
150+
} elseif ($item['method'] === 'account') {
151+
$formattedList[$key]['icon'] = 'icon-user';
152+
} else {
153+
$formattedList[$key]['iconSvg'] = 'svg' . ucfirst($item['method']);
154+
$formattedList[$key]['iconName'] = $item['method'];
155+
}
124156
}
125157
}
126-
return $list;
158+
return $formattedList;
127159
}
128160

129161
private function addHerselfAccount(array $return, string $search): array {
@@ -132,7 +164,14 @@ private function addHerselfAccount(array $return, string $search): array {
132164
return $return;
133165
}
134166
$user = $this->userSession->getUser();
135-
if (!str_contains($user->getUID(), $search) && !str_contains(strtolower($user->getDisplayName()), $search)) {
167+
$search = strtolower($search);
168+
if (!str_contains($user->getUID(), $search)
169+
&& !str_contains(strtolower($user->getDisplayName()), $search)
170+
&& (
171+
$user->getEMailAddress() === null
172+
|| ($user->getEMailAddress() !== null && !str_contains($user->getEMailAddress(), $search))
173+
)
174+
) {
136175
return $return;
137176
}
138177
$filtered = array_filter($return, fn ($i) => $i['id'] === $user->getUID());
@@ -145,7 +184,7 @@ private function addHerselfAccount(array $return, string $search): array {
145184
'displayName' => $user->getDisplayName(),
146185
'subname' => $user->getEMailAddress(),
147186
'icon' => 'icon-user',
148-
'shareType' => IShare::TYPE_USER,
187+
'method' => 'account',
149188
];
150189
return $return;
151190
}
@@ -159,7 +198,9 @@ private function addHerselfEmail(array $return, string $search): array {
159198
if (empty($user->getEMailAddress())) {
160199
return $return;
161200
}
162-
if (!str_contains($user->getEMailAddress(), $search) && !str_contains($user->getDisplayName(), $search)) {
201+
if (!str_contains($user->getEMailAddress(), $search)
202+
&& !str_contains($user->getDisplayName(), $search)
203+
) {
163204
return $return;
164205
}
165206
$filtered = array_filter($return, fn ($i) => $i['id'] === $user->getUID());
@@ -172,7 +213,7 @@ private function addHerselfEmail(array $return, string $search): array {
172213
'displayName' => $user->getDisplayName(),
173214
'subname' => $user->getEMailAddress(),
174215
'icon' => 'icon-mail',
175-
'shareType' => IShare::TYPE_EMAIL,
216+
'method' => 'email',
176217
];
177218
return $return;
178219
}
@@ -182,7 +223,21 @@ private function excludeEmptyShareWith(array $list): array {
182223
}
183224

184225
private function excludeNotAllowed(array $list): array {
185-
$shareTypes = $this->getShareTypes();
186-
return array_filter($list, fn ($result) => in_array($result['shareType'], $shareTypes));
226+
return array_filter($list, fn ($result) => isset($result['method']) && !empty($result['method']));
227+
}
228+
229+
private function replaceShareTypeByMethod(array $list): array {
230+
foreach ($list as $key => $item) {
231+
if (isset($item['method']) && !empty($item['method'])) {
232+
continue;
233+
}
234+
$list[$key]['method'] = match ($item['shareType']) {
235+
IShare::TYPE_EMAIL => 'email',
236+
IShare::TYPE_USER => 'account',
237+
default => '',
238+
};
239+
unset($list[$key]['shareType']);
240+
}
241+
return $list;
187242
}
188243
}

lib/Controller/SignFileController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public function signRenew(string $method): DataResponse {
220220
*
221221
* @param string $uuid UUID of LibreSign file
222222
* @param 'account'|'email'|null $identifyMethod Identify signer method
223-
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign
223+
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, sms, signal, telegram, whatsapp, xmpp
224224
* @param string|null $identify Identify value, i.e. the signer email, account or phone number
225225
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
226226
*
@@ -246,7 +246,7 @@ public function getCodeUsingUuid(string $uuid, ?string $identifyMethod, ?string
246246
*
247247
* @param int $fileId Id of LibreSign file
248248
* @param 'account'|'email'|null $identifyMethod Identify signer method
249-
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign
249+
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, sms, signal, telegram, whatsapp, xmpp
250250
* @param string|null $identify Identify value, i.e. the signer email, account or phone number
251251
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{message: string}, array{}>
252252
*

0 commit comments

Comments
 (0)