Skip to content

Commit 0b14fe3

Browse files
committed
feat: sign usign twofactor_gateway
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 11335ab commit 0b14fe3

33 files changed

Lines changed: 877 additions & 197 deletions

img/logo-signal-app.svg

Lines changed: 1 addition & 0 deletions
Loading
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 + 1;
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 (!count($identifiers) && !$this->canValidateMethod()) {
66+
$result['exact'][] = [
67+
'label' => $search,
68+
'shareWithDisplayNameUnique' => $search,
69+
'key' => 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+
'key' => $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/IdentifyAccountController.php

Lines changed: 40 additions & 12 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;
@@ -55,17 +56,17 @@ public function __construct(
5556
#[NoAdminRequired]
5657
#[RequireManager]
5758
#[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) {
59+
public function search(string $search = '', string $method, int $page = 1, int $limit = 25): DataResponse {
60+
// only search for string larger than a minimum length
61+
if (strlen($search) < 1) {
6562
return new DataResponse();
6663
}
6764

65+
$shareTypes = $this->getShareTypes();
66+
$lookup = false;
67+
6868
$offset = $limit * ($page - 1);
69+
$this->registerPlugin($method);
6970
[$result] = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $limit, $offset);
7071
$result['exact'] = $this->unifyResult($result['exact']);
7172
$result = $this->unifyResult($result);
@@ -78,6 +79,19 @@ public function search(string $search = '', int $page = 1, int $limit = 25): Dat
7879
return new DataResponse($return);
7980
}
8081

82+
private function registerPlugin(string $method): void {
83+
SignerPlugin::setMethod($method);
84+
85+
$refObject = new \ReflectionObject($this->collaboratorSearch);
86+
$refProperty = $refObject->getProperty('pluginList');
87+
$refProperty->setAccessible(true);
88+
89+
$plugins = $refProperty->getValue($this->collaboratorSearch);
90+
$plugins[SignerPlugin::TYPE_SIGNER] = [SignerPlugin::class];
91+
92+
$refProperty->setValue($this->collaboratorSearch, $plugins);
93+
}
94+
8195
private function getShareTypes(): array {
8296
if (count($this->shareTypes) > 0) {
8397
return $this->shareTypes;
@@ -90,6 +104,8 @@ private function getShareTypes(): array {
90104
if ($settings['enabled']) {
91105
$this->shareTypes[] = IShare::TYPE_USER;
92106
}
107+
108+
$this->shareTypes[] = SignerPlugin::TYPE_SIGNER;
93109
return $this->shareTypes;
94110
}
95111

@@ -109,21 +125,33 @@ private function unifyResult(array $list): array {
109125
}
110126

111127
private function formatForNcSelect(array $list): array {
128+
$return = [];
112129
foreach ($list as $key => $item) {
113-
$list[$key] = [
130+
$return[$key] = [
114131
'id' => $item['value']['shareWith'],
115132
'isNoUser' => $item['value']['shareType'] !== IShare::TYPE_USER,
116133
'displayName' => $item['label'],
117134
'subname' => $item['shareWithDisplayNameUnique'] ?? '',
118-
'shareType' => $item['value']['shareType'],
119135
];
120136
if ($item['value']['shareType'] === IShare::TYPE_EMAIL) {
121-
$list[$key]['icon'] = 'icon-mail';
137+
$return[$key]['method'] = 'email';
138+
$return[$key]['icon'] = 'icon-mail';
122139
} elseif ($item['value']['shareType'] === IShare::TYPE_USER) {
123-
$list[$key]['icon'] = 'icon-user';
140+
$return[$key]['method'] = 'accoung';
141+
$return[$key]['icon'] = 'icon-user';
142+
} elseif ($item['value']['shareType'] === SignerPlugin::TYPE_SIGNER) {
143+
$return[$key]['method'] = $item['key'];
144+
if ($item['key'] === 'email') {
145+
$return[$key]['icon'] = 'icon-mail';
146+
} elseif ($item['key'] === 'account') {
147+
$return[$key]['icon'] = 'icon-user';
148+
} else {
149+
$return[$key]['iconSvg'] = 'svg' . ucfirst($item['key']);
150+
$return[$key]['iconName'] = $item['key'];
151+
}
124152
}
125153
}
126-
return $list;
154+
return $return;
127155
}
128156

129157
private function addHerselfAccount(array $return, string $search): array {

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, twofactorGateway
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, twofactorGateway
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
*

lib/Db/IdentifyMethodMapper.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,61 @@ public function neutralizeDeletedUser(string $userId, string $displayName): void
7373
$update->executeStatement();
7474
}
7575
}
76+
77+
public function searchByIdentifierValue(string $search, string $userId, string $method, int $limit = 20, int $offset = 0): array {
78+
$qb = $this->db->getQueryBuilder();
79+
80+
$latestQb = $this->db->getQueryBuilder();
81+
$latestQb->select('im2.identifier_key')
82+
->addSelect('im2.identifier_value')
83+
->addSelect($latestQb->func()->max('sr2.created_at', 'created_at'))
84+
->from('libresign_identify_method', 'im2')
85+
->join('im2', 'libresign_sign_request', 'sr2',
86+
$latestQb->expr()->eq('sr2.id', 'im2.sign_request_id')
87+
)
88+
->join('im2', 'libresign_file', 'f2',
89+
$latestQb->expr()->eq('f2.id', 'sr2.file_id')
90+
)
91+
->where($latestQb->expr()->eq('f2.user_id', $latestQb->createNamedParameter($userId)))
92+
->andWhere($latestQb->expr()->eq('im2.identifier_key', $latestQb->createNamedParameter($method)))
93+
->andWhere(
94+
$latestQb->expr()->orX(
95+
$latestQb->expr()->iLike(
96+
'im2.identifier_value',
97+
$latestQb->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%')
98+
),
99+
$latestQb->expr()->iLike(
100+
'sr2.display_name',
101+
$latestQb->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%')
102+
)
103+
)
104+
)
105+
->groupBy('im2.identifier_key')
106+
->addGroupBy('im2.identifier_value');
107+
108+
foreach ($latestQb->getParameters() as $name => $value) {
109+
$qb->setParameter($name, $value);
110+
}
111+
112+
$qb->select('im.identifier_key', 'im.identifier_value', 'sr.display_name')
113+
->from('libresign_identify_method', 'im')
114+
->join('im', $qb->createFunction('(' . $latestQb->getSQL() . ')'), 'latest',
115+
$qb->expr()->andX(
116+
$qb->expr()->eq('latest.identifier_key', 'im.identifier_key'),
117+
$qb->expr()->eq('latest.identifier_value', 'im.identifier_value')
118+
)
119+
)
120+
->join('im', 'libresign_sign_request', 'sr',
121+
$qb->expr()->eq('sr.id', 'im.sign_request_id'),
122+
)
123+
->setMaxResults($limit)
124+
->setFirstResult($offset);
125+
126+
$cursor = $qb->executeQuery();
127+
$return = [];
128+
while ($row = $cursor->fetch()) {
129+
$return[] = $row;
130+
}
131+
return $return;
132+
}
76133
}

lib/ResponseDefinitions.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@
138138
* blurredEmail: string,
139139
* hashOfEmail: string,
140140
* }
141+
* @psalm-type LibresignSignatureMethodTwofactorGatewayToken = array{
142+
* label: string,
143+
* identifyMethod: "twofactorGateway",
144+
* needCode: bool,
145+
* hasConfirmCode: bool,
146+
* blurredIdentifier: string,
147+
* hashOfIdentifier: string,
148+
* }
141149
* @psalm-type LibresignSignatureMethodPassword = array{
142150
* label: string,
143151
* name: string,
@@ -146,6 +154,7 @@
146154
* @psalm-type LibresignSignatureMethods = array{
147155
* clickToSign?: LibresignSignatureMethod,
148156
* emailToken?: LibresignSignatureMethodEmailToken,
157+
* twofactorGatewayToken?: LibresignSignatureMethodTwofactorGatewayToken,
149158
* password?: LibresignSignatureMethodPassword,
150159
* }
151160
* @psalm-type LibresignNotify = array{

lib/Service/IdentifyMethod/AbstractIdentifyMethod.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use OCA\Libresign\Exception\LibresignException;
1919
use OCA\Libresign\Helper\JSActions;
2020
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\AbstractSignatureMethod;
21+
use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\ISignatureMethod;
2122
use OCA\Libresign\Service\SessionService;
2223
use OCP\IUser;
2324
use Wobeto\EmailBlur\Blur;
@@ -34,7 +35,7 @@ abstract class AbstractIdentifyMethod implements IIdentifyMethod {
3435
* @var string[]
3536
*/
3637
public array $availableSignatureMethods = [];
37-
protected string $defaultSignatureMethod = '';
38+
protected string $defaultSignatureMethod = ISignatureMethod::SIGNATURE_METHOD_CLICK_TO_SIGN;
3839
/**
3940
* @var AbstractSignatureMethod[]
4041
*/
@@ -60,6 +61,10 @@ public function getFriendlyName(): string {
6061
return $this->friendlyName;
6162
}
6263

64+
public function setFriendlyName(string $friendlyName): void {
65+
$this->friendlyName = $friendlyName;
66+
}
67+
6368
public function setCodeSentByUser(string $code): void {
6469
$this->codeSentByUser = $code;
6570
}
@@ -79,14 +84,18 @@ public function getEntity(): IdentifyMethod {
7984

8085
public function signatureMethodsToArray(): array {
8186
return array_map(fn (AbstractSignatureMethod $method) => [
82-
'label' => $method->friendlyName,
87+
'label' => $method->getFriendlyName(),
8388
'name' => $method->getName(),
8489
'enabled' => $method->isEnabled(),
8590
], $this->signatureMethods);
8691
}
8792

93+
public function getAvailableSignatureMethods(): array {
94+
return $this->availableSignatureMethods;
95+
}
96+
8897
public function getEmptyInstanceOfSignatureMethodByName(string $name): AbstractSignatureMethod {
89-
if (!in_array($name, $this->availableSignatureMethods)) {
98+
if (!in_array($name, $this->getAvailableSignatureMethods())) {
9099
throw new InvalidArgumentException(sprintf('%s is not a valid signature method of identify method %s', $name, $this->getName()));
91100
}
92101
$className = 'OCA\Libresign\Service\IdentifyMethod\\SignatureMethod\\' . ucfirst($name);
@@ -284,7 +293,7 @@ protected function getSettingsFromDatabase(array $default = [], array $immutable
284293
$default = array_merge(
285294
[
286295
'name' => $this->name,
287-
'friendly_name' => $this->friendlyName,
296+
'friendly_name' => $this->getFriendlyName(),
288297
'enabled' => true,
289298
'mandatory' => true,
290299
'signatureMethods' => $this->signatureMethodsToArray(),
@@ -310,7 +319,8 @@ private function loadSavedSettings(): void {
310319
return $carry;
311320
}, []);
312321
$enabled = false;
313-
foreach ($this->availableSignatureMethods as $signatureMethodName) {
322+
$availableSignatureMethods = $this->getAvailableSignatureMethods();
323+
foreach ($availableSignatureMethods as $signatureMethodName) {
314324
$this->signatureMethods[$signatureMethodName]
315325
= $this->getEmptyInstanceOfSignatureMethodByName($signatureMethodName);
316326
if (isset($this->settings['signatureMethods'][$signatureMethodName]['enabled'])
@@ -320,6 +330,13 @@ private function loadSavedSettings(): void {
320330
$enabled = true;
321331
}
322332
}
333+
if (isset($this->settings['signatureMethods'])) {
334+
foreach (array_keys($this->settings['signatureMethods']) as $signatureMethodName) {
335+
if (!in_array($signatureMethodName, $availableSignatureMethods, true)) {
336+
unset($this->settings['signatureMethods'][$signatureMethodName]);
337+
}
338+
}
339+
}
323340
if (!$enabled && $this->defaultSignatureMethod) {
324341
$this->signatureMethods[$this->defaultSignatureMethod]->enable();
325342
}

lib/Service/IdentifyMethod/Account.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function __construct(
4747
private MailService $mail,
4848
) {
4949
// TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by Nextcloud acccount
50-
$this->friendlyName = $this->identifyService->getL10n()->t('Account');
50+
$this->setFriendlyName($this->identifyService->getL10n()->t('Account'));
5151
parent::__construct(
5252
$identifyService,
5353
);

0 commit comments

Comments
 (0)