• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 25703625108

11 May 2026 11:33PM UTC coverage: 56.791%. First build
25703625108

Pull #7675

github

web-flow
Merge d0c6d6056 into 2f1329aa4
Pull Request #7675: fix: keep groups_request_sign JSON unicode serialization consistent

0 of 14 new or added lines in 1 file covered. (0.0%)

10729 of 18892 relevant lines covered (56.79%)

6.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

11.8
/lib/Controller/AdminController.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Controller;
10

11
use DateTimeInterface;
12
use OCA\Libresign\AppInfo\Application;
13
use OCA\Libresign\Db\FileMapper;
14
use OCA\Libresign\Enum\DocMdpLevel;
15
use OCA\Libresign\Enum\FileStatus;
16
use OCA\Libresign\Exception\LibresignException;
17
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
18
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
19
use OCA\Libresign\Helper\ConfigureCheckHelper;
20
use OCA\Libresign\Service\Certificate\ValidateService;
21
use OCA\Libresign\Service\CertificatePolicyService;
22
use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService;
23
use OCA\Libresign\Service\FooterService;
24
use OCA\Libresign\Service\IdentifyMethodService;
25
use OCA\Libresign\Service\Install\ConfigureCheckService;
26
use OCA\Libresign\Service\Install\InstallService;
27
use OCA\Libresign\Service\ReminderService;
28
use OCA\Libresign\Service\SignatureBackgroundService;
29
use OCA\Libresign\Service\SignatureTextService;
30
use OCA\Libresign\Settings\Admin;
31
use OCP\AppFramework\Http;
32
use OCP\AppFramework\Http\Attribute\ApiRoute;
33
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
34
use OCP\AppFramework\Http\ContentSecurityPolicy;
35
use OCP\AppFramework\Http\DataDownloadResponse;
36
use OCP\AppFramework\Http\DataResponse;
37
use OCP\AppFramework\Http\FileDisplayResponse;
38
use OCP\Files\SimpleFS\InMemoryFile;
39
use OCP\IAppConfig;
40
use OCP\IEventSource;
41
use OCP\IEventSourceFactory;
42
use OCP\IL10N;
43
use OCP\IRequest;
44
use OCP\ISession;
45
use UnexpectedValueException;
46

47
/**
48
 * @psalm-import-type LibresignCertificateDataGenerated from \OCA\Libresign\ResponseDefinitions
49
 * @psalm-import-type LibresignCertificateEngineConfigResponse from \OCA\Libresign\ResponseDefinitions
50
 * @psalm-import-type LibresignCertificatePolicyResponse from \OCA\Libresign\ResponseDefinitions
51
 * @psalm-import-type LibresignConfigureCheck from \OCA\Libresign\ResponseDefinitions
52
 * @psalm-import-type LibresignConfigureChecksResponse from \OCA\Libresign\ResponseDefinitions
53
 * @psalm-import-type LibresignEngineHandlerResponse from \OCA\Libresign\ResponseDefinitions
54
 * @psalm-import-type LibresignErrorResponse from \OCA\Libresign\ResponseDefinitions
55
 * @psalm-import-type LibresignErrorStatusResponse from \OCA\Libresign\ResponseDefinitions
56
 * @psalm-import-type LibresignEngineHandler from \OCA\Libresign\ResponseDefinitions
57
 * @psalm-import-type LibresignIdentifyMethodSetting from \OCA\Libresign\ResponseDefinitions
58
 * @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
59
 * @psalm-import-type LibresignSignatureTextSettingsResponse from \OCA\Libresign\ResponseDefinitions
60
 * @psalm-import-type LibresignSignatureTemplateSettingsResponse from \OCA\Libresign\ResponseDefinitions
61
 * @psalm-import-type LibresignSuccessStatusResponse from \OCA\Libresign\ResponseDefinitions
62
 * @psalm-import-type LibresignFailureStatusResponse from \OCA\Libresign\ResponseDefinitions
63
 * @psalm-import-type LibresignActiveSigningsResponse from \OCA\Libresign\ResponseDefinitions
64
 * @psalm-import-type LibresignReminderSettings from \OCA\Libresign\ResponseDefinitions
65
 * @psalm-import-type LibresignRootCertificate from \OCA\Libresign\ResponseDefinitions
66
 * @psalm-import-type LibresignFooterTemplateResponse from \OCA\Libresign\ResponseDefinitions
67
 */
68
class AdminController extends AEnvironmentAwareController {
69
        private IEventSource $eventSource;
70
        public function __construct(
71
                IRequest $request,
72
                private IAppConfig $appConfig,
73
                private ConfigureCheckService $configureCheckService,
74
                private InstallService $installService,
75
                private CertificateEngineFactory $certificateEngineFactory,
76
                private IEventSourceFactory $eventSourceFactory,
77
                private SignatureTextService $signatureTextService,
78
                private IL10N $l10n,
79
                protected ISession $session,
80
                private SignatureBackgroundService $signatureBackgroundService,
81
                private CertificatePolicyService $certificatePolicyService,
82
                private ValidateService $validateService,
83
                private ReminderService $reminderService,
84
                private FooterService $footerService,
85
                private DocMdpConfigService $docMdpConfigService,
86
                private IdentifyMethodService $identifyMethodService,
87
                private FileMapper $fileMapper,
88
        ) {
89
                parent::__construct(Application::APP_ID, $request);
5✔
90
                $this->eventSource = $this->eventSourceFactory->create();
5✔
91
        }
92

93
        /**
94
         * Generate certificate using CFSSL engine
95
         *
96
         * @param array{commonName: string, names: array<string, array{value:string|array<string>}>} $rootCert fields of root certificate
97
         * @param string $cfsslUri URI of CFSSL API
98
         * @param string $configPath Path of config files of CFSSL
99
         * @return DataResponse<Http::STATUS_OK, LibresignEngineHandlerResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>
100
         *
101
         * 200: OK
102
         * 401: Account not found
103
         */
104
        #[NoCSRFRequired]
105
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate/cfssl', requirements: ['apiVersion' => '(v1)'])]
106
        public function generateCertificateCfssl(
107
                array $rootCert,
108
                string $cfsslUri = '',
109
                string $configPath = '',
110
        ): DataResponse {
111
                try {
112
                        $engineHandler = $this->generateCertificate($rootCert, [
×
113
                                'engine' => 'cfssl',
×
114
                                'configPath' => trim($configPath),
×
115
                                'cfsslUri' => trim($cfsslUri),
×
116
                        ])->toArray();
×
117
                        return new DataResponse([
×
118
                                'data' => $engineHandler,
×
119
                        ]);
×
120
                } catch (\Exception $exception) {
×
121
                        return new DataResponse(
×
122
                                [
×
123
                                        'message' => $exception->getMessage()
×
124
                                ],
×
125
                                Http::STATUS_UNAUTHORIZED
×
126
                        );
×
127
                }
128
        }
129

130
        /**
131
         * Generate certificate using OpenSSL engine
132
         *
133
         * @param array{commonName: string, names: array<string, array{value:string|array<string>}>} $rootCert fields of root certificate
134
         * @param string $configPath Path of config files of CFSSL
135
         * @return DataResponse<Http::STATUS_OK, LibresignEngineHandlerResponse, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>
136
         *
137
         * 200: OK
138
         * 401: Account not found
139
         */
140
        #[NoCSRFRequired]
141
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate/openssl', requirements: ['apiVersion' => '(v1)'])]
142
        public function generateCertificateOpenSsl(
143
                array $rootCert,
144
                string $configPath = '',
145
        ): DataResponse {
146
                try {
147
                        $engineHandler = $this->generateCertificate($rootCert, [
1✔
148
                                'engine' => 'openssl',
1✔
149
                                'configPath' => trim($configPath),
1✔
150
                        ])->toArray();
1✔
151
                        return new DataResponse([
×
152
                                'data' => $engineHandler,
×
153
                        ]);
×
154
                } catch (\Exception $exception) {
1✔
155
                        return new DataResponse(
1✔
156
                                [
1✔
157
                                        'message' => $exception->getMessage()
1✔
158
                                ],
1✔
159
                                Http::STATUS_UNAUTHORIZED
1✔
160
                        );
1✔
161
                }
162
        }
163

164
        /**
165
         * Set certificate engine
166
         *
167
         * Sets the certificate engine (openssl, cfssl, or none) and automatically configures identify_methods when needed
168
         *
169
         * @param string $engine The certificate engine to use (openssl, cfssl, or none)
170
         * @return DataResponse<Http::STATUS_OK, LibresignCertificateEngineConfigResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignMessageResponse, array{}>
171
         *
172
         * 200: OK
173
         * 400: Invalid engine
174
         */
175
        #[NoCSRFRequired]
176
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate/engine', requirements: ['apiVersion' => '(v1)'])]
177
        public function setCertificateEngine(string $engine): DataResponse {
178
                $validEngines = ['openssl', 'cfssl', 'none'];
×
179
                if (!in_array($engine, $validEngines, true)) {
×
180
                        return new DataResponse(
×
181
                                ['message' => 'Invalid engine. Must be one of: ' . implode(', ', $validEngines)],
×
182
                                Http::STATUS_BAD_REQUEST
×
183
                        );
×
184
                }
185

186
                $handler = $this->certificateEngineFactory->getEngine();
×
187
                $handler->setEngine($engine);
×
188
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings();
×
189

190
                return new DataResponse([
×
191
                        'engine' => $engine,
×
192
                        'identify_methods' => $identifyMethods,
×
193
                ]);
×
194
        }
195

196
        private function generateCertificate(
197
                array $rootCert,
198
                array $properties = [],
199
        ): IEngineHandler {
200
                $names = [];
1✔
201
                if (isset($rootCert['names'])) {
1✔
202
                        $this->validateService->validateNames($rootCert['names']);
1✔
203
                        foreach ($rootCert['names'] as $item) {
×
204
                                if (is_array($item['value'])) {
×
205
                                        $trimmedValues = array_map('trim', $item['value']);
×
206
                                        $names[$item['id']]['value'] = array_filter($trimmedValues, fn ($val) => $val !== '');
×
207
                                } else {
208
                                        $names[$item['id']]['value'] = trim((string)$item['value']);
×
209
                                }
210
                        }
211
                }
212
                $this->validateService->validate('CN', $rootCert['commonName']);
×
213
                $this->installService->generate(
×
214
                        trim((string)$rootCert['commonName']),
×
215
                        $properties['engine'],
×
216
                        $names,
×
217
                        $properties,
×
218
                );
×
219

220
                return $this->certificateEngineFactory->getEngine();
×
221
        }
222

223
        /**
224
         * Load certificate data
225
         *
226
         * Return all data of root certificate and a field called `generated` with a boolean value.
227
         *
228
         * @return DataResponse<Http::STATUS_OK, LibresignCertificateDataGenerated, array{}>
229
         *
230
         * 200: OK
231
         */
232
        #[NoCSRFRequired]
233
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/certificate', requirements: ['apiVersion' => '(v1)'])]
234
        public function loadCertificate(): DataResponse {
235
                $engine = $this->certificateEngineFactory->getEngine();
1✔
236
                /** @var LibresignEngineHandler */
237
                $certificate = $engine->toArray();
1✔
238
                $configureResult = $engine->configureCheck();
1✔
239
                $success = array_filter(
1✔
240
                        $configureResult,
1✔
241
                        fn (ConfigureCheckHelper $config) => $config->getStatus() === 'success'
1✔
242
                );
1✔
243
                $certificate['generated'] = count($success) === count($configureResult);
1✔
244

245
                return new DataResponse($certificate);
1✔
246
        }
247

248
        /**
249
         * Check the configuration of LibreSign
250
         *
251
         * Return the status of necessary configuration and tips to fix the problems.
252
         *
253
         * @return DataResponse<Http::STATUS_OK, LibresignConfigureChecksResponse, array{}>
254
         *
255
         * 200: OK
256
         */
257
        #[NoCSRFRequired]
258
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/configure-check', requirements: ['apiVersion' => '(v1)'])]
259
        public function configureCheck(): DataResponse {
260
                /** @var LibresignConfigureChecksResponse $configureCheckList */
261
                $configureCheckList = array_values($this->configureCheckService->checkAll());
×
262
                return new DataResponse(
×
263
                        $configureCheckList
×
264
                );
×
265
        }
266

267
        /**
268
         * Disable hate limit to current session
269
         *
270
         * This will disable hate limit to current session.
271
         *
272
         * @return DataResponse<Http::STATUS_OK, array{}, array{}>
273
         *
274
         * 200: OK
275
         */
276
        #[NoCSRFRequired]
277
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/disable-hate-limit', requirements: ['apiVersion' => '(v1)'])]
278
        public function disableHateLimit(): DataResponse {
279
                $this->session->set('app_api', true);
×
280

281
                // TODO: Remove after drop support NC29
282
                // deprecated since AppAPI 2.8.0
283
                $this->session->set('app_api_system', true);
×
284

285
                return new DataResponse();
×
286
        }
287

288
        /**
289
         * @IgnoreOpenAPI
290
         */
291
        #[NoCSRFRequired]
292
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/install-and-validate', requirements: ['apiVersion' => '(v1)'])]
293
        public function installAndValidate(): void {
294
                try {
295
                        $async = \function_exists('proc_open');
×
296
                        $this->installService->installJava($async);
×
297
                        $this->installService->installJSignPdf($async);
×
298
                        $this->installService->installPdftk($async);
×
299
                        if ($this->appConfig->getValueString(Application::APP_ID, 'certificate_engine') === 'cfssl') {
×
300
                                $this->installService->installCfssl($async);
×
301
                        }
302

303
                        $this->configureCheckService->disableCache();
×
304
                        $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
305
                        $seconds = 0;
×
306
                        while ($this->installService->isDownloadWip()) {
×
307
                                $totalSize = $this->installService->getTotalSize();
×
308
                                $this->eventSource->send('total_size', json_encode($totalSize));
×
309
                                if ($errors = $this->installService->getErrorMessages()) {
×
310
                                        $this->eventSource->send('errors', json_encode($errors));
×
311
                                }
312
                                usleep(200000); // 0.2 seconds
×
313
                                $seconds += 0.2;
×
314
                                if ($seconds === 5.0) {
×
315
                                        $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
316
                                        $seconds = 0;
×
317
                                }
318
                        }
319
                        if ($errors = $this->installService->getErrorMessages()) {
×
320
                                $this->eventSource->send('errors', json_encode($errors));
×
321
                        }
322
                } catch (\Exception $exception) {
×
323
                        $this->eventSource->send('errors', json_encode([
×
324
                                $this->l10n->t('Could not download binaries.'),
×
325
                                $exception->getMessage(),
×
326
                        ]));
×
327
                }
328

329
                $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
330
                $this->eventSource->send('done', '');
×
331
                $this->eventSource->close();
×
332
                // Nextcloud inject a lot of headers that is incompatible with SSE
333
                exit();
×
334
        }
335

336
        /**
337
         * Add custom background image
338
         *
339
         * @return DataResponse<Http::STATUS_OK, LibresignSuccessStatusResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignFailureStatusResponse, array{}>
340
         *
341
         * 200: OK
342
         * 422: Error
343
         */
344
        #[NoCSRFRequired]
345
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
346
        public function signatureBackgroundSave(): DataResponse {
347
                $image = $this->request->getUploadedFile('image');
×
348
                $phpFileUploadErrors = [
×
349
                        UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
×
350
                        UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
×
351
                        UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
×
352
                        UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
×
353
                        UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
×
354
                        UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
×
355
                        UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
×
356
                        UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
×
357
                ];
×
358
                if (empty($image)) {
×
359
                        $error = $this->l10n->t('No file uploaded');
×
360
                } elseif (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
×
361
                        $error = $phpFileUploadErrors[$image['error']];
×
362
                }
363
                if ($error !== null) {
×
364
                        return new DataResponse(
×
365
                                [
×
366
                                        'message' => $error,
×
367
                                        'status' => 'failure',
×
368
                                ],
×
369
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
370
                        );
×
371
                }
372
                try {
373
                        $this->signatureBackgroundService->updateImage($image['tmp_name']);
×
374
                } catch (\Exception $e) {
×
375
                        return new DataResponse(
×
376
                                [
×
377
                                        'message' => $e->getMessage(),
×
378
                                        'status' => 'failure',
×
379
                                ],
×
380
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
381
                        );
×
382
                }
383

384
                return new DataResponse(
×
385
                        [
×
386
                                'status' => 'success',
×
387
                        ]
×
388
                );
×
389
        }
390

391
        /**
392
         * Get custom background image
393
         *
394
         * @return FileDisplayResponse<Http::STATUS_OK, array{}>
395
         *
396
         * 200: Image returned
397
         */
398
        #[NoCSRFRequired]
399
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
400
        public function signatureBackgroundGet(): FileDisplayResponse {
401
                $file = $this->signatureBackgroundService->getImage();
×
402

403
                $response = new FileDisplayResponse($file);
×
404
                $csp = new ContentSecurityPolicy();
×
405
                $csp->allowInlineStyle();
×
406
                $response->setContentSecurityPolicy($csp);
×
407
                $response->cacheFor(3600);
×
408
                $response->addHeader('Content-Type', 'image/png');
×
409
                $response->addHeader('Content-Disposition', 'attachment; filename="background.png"');
×
410
                $response->addHeader('Content-Type', 'image/png');
×
411
                return $response;
×
412
        }
413

414
        /**
415
         * Reset the background image to be the default of LibreSign
416
         *
417
         * @return DataResponse<Http::STATUS_OK, LibresignSuccessStatusResponse, array{}>
418
         *
419
         * 200: Image reseted to default
420
         */
421
        #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
422
        public function signatureBackgroundReset(): DataResponse {
423
                $this->signatureBackgroundService->reset();
×
424
                return new DataResponse(
×
425
                        [
×
426
                                'status' => 'success',
×
427
                        ]
×
428
                );
×
429
        }
430

431
        /**
432
         * Delete background image
433
         *
434
         * @return DataResponse<Http::STATUS_OK, LibresignSuccessStatusResponse, array{}>
435
         *
436
         * 200: Deleted with success
437
         */
438
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
439
        public function signatureBackgroundDelete(): DataResponse {
440
                $this->signatureBackgroundService->delete();
×
441
                return new DataResponse(
×
442
                        [
×
443
                                'status' => 'success',
×
444
                        ]
×
445
                );
×
446
        }
447

448
        /**
449
         * Save signature text service
450
         *
451
         * @param string $template Template to signature text
452
         * @param float $templateFontSize Font size used when print the parsed text of this template at PDF file
453
         * @param float $signatureFontSize Font size used when the signature mode is SIGNAME_AND_DESCRIPTION
454
         * @param float $signatureWidth Signature box width, minimum 1
455
         * @param float $signatureHeight Signature box height, minimum 1
456
         * @param string $renderMode Signature render mode
457
         * @return DataResponse<Http::STATUS_OK, LibresignSignatureTextSettingsResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
458
         *
459
         * 200: OK
460
         * 400: Bad request
461
         */
462
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-text', requirements: ['apiVersion' => '(v1)'])]
463
        public function signatureTextSave(
464
                string $template,
465
                /** @todo openapi package don't evaluate SignatureTextService::TEMPLATE_DEFAULT_FONT_SIZE */
466
                float $templateFontSize = 10,
467
                /** @todo openapi package don't evaluate SignatureTextService::SIGNATURE_DEFAULT_FONT_SIZE */
468
                float $signatureFontSize = 20,
469
                /** @todo openapi package don't evaluate SignatureTextService::DEFAULT_SIGNATURE_WIDTH */
470
                float $signatureWidth = 350,
471
                /** @todo openapi package don't evaluate SignatureTextService::DEFAULT_SIGNATURE_HEIGHT */
472
                float $signatureHeight = 100,
473
                string $renderMode = 'GRAPHIC_AND_DESCRIPTION',
474
        ): DataResponse {
475
                try {
476
                        $return = $this->signatureTextService->save(
×
477
                                $template,
×
478
                                $templateFontSize,
×
479
                                $signatureFontSize,
×
480
                                $signatureWidth,
×
481
                                $signatureHeight,
×
482
                                $renderMode,
×
483
                        );
×
484
                        return new DataResponse(
×
485
                                $return,
×
486
                                Http::STATUS_OK
×
487
                        );
×
488
                } catch (LibresignException $th) {
×
489
                        return new DataResponse(
×
490
                                [
×
491
                                        'error' => $th->getMessage(),
×
492
                                ],
×
493
                                Http::STATUS_BAD_REQUEST
×
494
                        );
×
495
                }
496
        }
497

498
        /**
499
         * Get parsed signature text service
500
         *
501
         * @param string $template Template to signature text
502
         * @param string $context Context for parsing the template
503
         * @return DataResponse<Http::STATUS_OK, LibresignSignatureTextSettingsResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
504
         *
505
         * 200: OK
506
         * 400: Bad request
507
         */
508
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-text', requirements: ['apiVersion' => '(v1)'])]
509
        public function signatureTextGet(string $template = '', string $context = ''): DataResponse {
510
                $context = json_decode($context, true) ?? [];
×
511
                try {
512
                        $return = $this->signatureTextService->parse($template, $context);
×
513
                        return new DataResponse(
×
514
                                $return,
×
515
                                Http::STATUS_OK
×
516
                        );
×
517
                } catch (LibresignException $th) {
×
518
                        return new DataResponse(
×
519
                                [
×
520
                                        'error' => $th->getMessage(),
×
521
                                ],
×
522
                                Http::STATUS_BAD_REQUEST
×
523
                        );
×
524
                }
525
        }
526

527
        /**
528
         * Get signature settings
529
         *
530
         * @return DataResponse<Http::STATUS_OK, LibresignSignatureTemplateSettingsResponse, array{}>
531
         *
532
         * 200: OK
533
         */
534
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-settings', requirements: ['apiVersion' => '(v1)'])]
535
        public function getSignatureSettings(): DataResponse {
536
                $response = [
×
537
                        'signature_available_variables' => $this->signatureTextService->getAvailableVariables(),
×
538
                        'default_signature_text_template' => $this->signatureTextService->getDefaultTemplate(),
×
539
                ];
×
540
                return new DataResponse($response);
×
541
        }
542

543
        /**
544
         * Convert signer name as image
545
         *
546
         * @param int $width Image width,
547
         * @param int $height Image height
548
         * @param string $text Text to be added to image
549
         * @param float $fontSize Font size of text
550
         * @param bool $isDarkTheme Color of text, white if is tark theme and black if not
551
         * @param string $align Align of text: left, center or right
552
         * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Disposition: 'inline; filename="signer-name.png"', Content-Type: 'image/png'}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
553
         *
554
         * 200: OK
555
         * 400: Bad request
556
         */
557
        #[NoCSRFRequired]
558
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signer-name', requirements: ['apiVersion' => '(v1)'])]
559
        public function signerName(
560
                int $width,
561
                int $height,
562
                string $text,
563
                float $fontSize,
564
                bool $isDarkTheme,
565
                string $align,
566
        ):  FileDisplayResponse|DataResponse {
567
                try {
568
                        $blob = $this->signatureTextService->signerNameImage(
×
569
                                width: $width,
×
570
                                height: $height,
×
571
                                text: $text,
×
572
                                fontSize: $fontSize,
×
573
                                isDarkTheme: $isDarkTheme,
×
574
                                align: $align,
×
575
                        );
×
576
                        $file = new InMemoryFile('signer-name.png', $blob);
×
577
                        return new FileDisplayResponse($file, Http::STATUS_OK, [
×
578
                                'Content-Disposition' => 'inline; filename="signer-name.png"',
×
579
                                'Content-Type' => 'image/png',
×
580
                        ]);
×
581
                } catch (LibresignException $th) {
×
582
                        return new DataResponse(
×
583
                                [
×
584
                                        'error' => $th->getMessage(),
×
585
                                ],
×
586
                                Http::STATUS_BAD_REQUEST
×
587
                        );
×
588
                }
589
        }
590

591
        /**
592
         * Update certificate policy of this instance
593
         *
594
         * @return DataResponse<Http::STATUS_OK, LibresignCertificatePolicyResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignFailureStatusResponse, array{}>
595
         *
596
         * 200: OK
597
         * 422: Not found
598
         */
599
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
600
        public function saveCertificatePolicy(): DataResponse {
601
                $pdf = $this->request->getUploadedFile('pdf');
×
602
                $phpFileUploadErrors = [
×
603
                        UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
×
604
                        UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
×
605
                        UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
×
606
                        UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
×
607
                        UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
×
608
                        UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
×
609
                        UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
×
610
                        UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
×
611
                ];
×
612
                if (empty($pdf)) {
×
613
                        $error = $this->l10n->t('No file uploaded');
×
614
                } elseif (!empty($pdf) && array_key_exists('error', $pdf) && $pdf['error'] !== UPLOAD_ERR_OK) {
×
615
                        $error = $phpFileUploadErrors[$pdf['error']];
×
616
                }
617
                if ($error !== null) {
×
618
                        return new DataResponse(
×
619
                                [
×
620
                                        'message' => $error,
×
621
                                        'status' => 'failure',
×
622
                                ],
×
623
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
624
                        );
×
625
                }
626
                try {
627
                        $cps = $this->certificatePolicyService->updateFile($pdf['tmp_name']);
×
628
                } catch (UnexpectedValueException $e) {
×
629
                        return new DataResponse(
×
630
                                [
×
631
                                        'message' => $e->getMessage(),
×
632
                                        'status' => 'failure',
×
633
                                ],
×
634
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
635
                        );
×
636
                }
637
                return new DataResponse(
×
638
                        [
×
639
                                'CPS' => $cps,
×
640
                                'status' => 'success',
×
641
                        ]
×
642
                );
×
643
        }
644

645
        /**
646
         * Delete certificate policy of this instance
647
         *
648
         * @return DataResponse<Http::STATUS_OK, array{}, array{}>
649
         *
650
         * 200: OK
651
         * 404: Not found
652
         */
653
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
654
        public function deleteCertificatePolicy(): DataResponse {
655
                $this->certificatePolicyService->deleteFile();
×
656
                return new DataResponse();
×
657
        }
658

659
        /**
660
         * Update OID
661
         *
662
         * @param string $oid OID is a unique numeric identifier for certificate policies in digital certificates.
663
         * @return DataResponse<Http::STATUS_OK, LibresignSuccessStatusResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignFailureStatusResponse, array{}>
664
         *
665
         * 200: OK
666
         * 422: Validation error
667
         */
668
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate-policy/oid', requirements: ['apiVersion' => '(v1)'])]
669
        public function updateOid(string $oid): DataResponse {
670
                try {
671
                        $this->certificatePolicyService->updateOid($oid);
×
672
                        return new DataResponse(
×
673
                                [
×
674
                                        'status' => 'success',
×
675
                                ]
×
676
                        );
×
677
                } catch (\Exception $e) {
×
678
                        return new DataResponse(
×
679
                                [
×
680
                                        'message' => $e->getMessage(),
×
681
                                        'status' => 'failure',
×
682
                                ],
×
683
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
684
                        );
×
685
                }
686
        }
687

688
        /**
689
         * Get reminder settings
690
         *
691
         * @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
692
         *
693
         * 200: OK
694
         */
695
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
696
        public function reminderFetch(): DataResponse {
697
                $response = $this->reminderService->getSettings();
×
698
                if ($response['next_run'] instanceof \DateTime) {
×
699
                        $response['next_run'] = $response['next_run']->format(DateTimeInterface::ATOM);
×
700
                }
701
                return new DataResponse($response);
×
702
        }
703

704
        /**
705
         * Save reminder
706
         *
707
         * @param int $daysBefore First reminder after (days)
708
         * @param int $daysBetween Days between reminders
709
         * @param int $max Max reminders per signer
710
         * @param string $sendTimer Send time (HH:mm)
711
         * @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
712
         *
713
         * 200: OK
714
         */
715
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
716
        public function reminderSave(
717
                int $daysBefore,
718
                int $daysBetween,
719
                int $max,
720
                string $sendTimer,
721
        ): DataResponse {
722
                $response = $this->reminderService->save($daysBefore, $daysBetween, $max, $sendTimer);
×
723
                if ($response['next_run'] instanceof \DateTime) {
×
724
                        $response['next_run'] = $response['next_run']->format(DateTimeInterface::ATOM);
×
725
                }
726
                return new DataResponse($response);
×
727
        }
728

729
        /**
730
         * Set TSA configuration values with proper sensitive data handling
731
         *
732
         * Only saves configuration if tsa_url is provided. Automatically manages
733
         * username/password fields based on authentication type.
734
         *
735
         * @param string|null $tsa_url TSA server URL (required for saving)
736
         * @param string|null $tsa_policy_oid TSA policy OID
737
         * @param string|null $tsa_auth_type Authentication type (none|basic), defaults to 'none'
738
         * @param string|null $tsa_username Username for basic authentication
739
         * @param string|null $tsa_password Password for basic authentication (stored as sensitive data)
740
         * @return DataResponse<Http::STATUS_OK, LibresignSuccessStatusResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorStatusResponse, array{}>
741
         *
742
         * 200: OK
743
         * 400: Validation error
744
         */
745
        #[NoCSRFRequired]
746
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
747
        public function setTsaConfig(
748
                ?string $tsa_url = null,
749
                ?string $tsa_policy_oid = null,
750
                ?string $tsa_auth_type = null,
751
                ?string $tsa_username = null,
752
                ?string $tsa_password = null,
753
        ): DataResponse {
754
                if (empty($tsa_url)) {
2✔
755
                        return $this->deleteTsaConfig();
1✔
756
                }
757

758
                $trimmedUrl = trim($tsa_url);
1✔
759
                if (!filter_var($trimmedUrl, FILTER_VALIDATE_URL)
1✔
760
                        || !in_array(parse_url($trimmedUrl, PHP_URL_SCHEME), ['http', 'https'])) {
1✔
761
                        return new DataResponse([
×
762
                                'status' => 'error',
×
763
                                'message' => 'Invalid URL format'
×
764
                        ], Http::STATUS_BAD_REQUEST);
×
765
                }
766

767
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_url', $trimmedUrl);
1✔
768

769
                if (empty($tsa_policy_oid)) {
1✔
770
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_policy_oid');
1✔
771
                } else {
772
                        $trimmedOid = trim($tsa_policy_oid);
×
773
                        if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $trimmedOid)) {
×
774
                                return new DataResponse([
×
775
                                        'status' => 'error',
×
776
                                        'message' => 'Invalid OID format'
×
777
                                ], Http::STATUS_BAD_REQUEST);
×
778
                        }
779
                        $this->appConfig->setValueString(Application::APP_ID, 'tsa_policy_oid', $trimmedOid);
×
780
                }
781

782
                $authType = $tsa_auth_type ?? 'none';
1✔
783
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
784

785
                if ($authType === 'basic') {
1✔
786
                        $hasUsername = !empty($tsa_username);
1✔
787
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
788

789
                        if (!$hasUsername && !$hasPassword) {
1✔
790
                                return new DataResponse([
×
791
                                        'status' => 'error',
×
792
                                        'message' => 'Username and password are required for basic authentication'
×
793
                                ], Http::STATUS_BAD_REQUEST);
×
794
                        } elseif (!$hasUsername) {
1✔
795
                                return new DataResponse([
×
796
                                        'status' => 'error',
×
797
                                        'message' => 'Username is required'
×
798
                                ], Http::STATUS_BAD_REQUEST);
×
799
                        } elseif (!$hasPassword) {
1✔
800
                                return new DataResponse([
×
801
                                        'status' => 'error',
×
802
                                        'message' => 'Password is required'
×
803
                                ], Http::STATUS_BAD_REQUEST);
×
804
                        }
805

806
                        $this->appConfig->setValueString(Application::APP_ID, 'tsa_username', trim($tsa_username));
1✔
807
                        $this->appConfig->setValueString(
1✔
808
                                Application::APP_ID,
1✔
809
                                key: 'tsa_password',
1✔
810
                                value: $tsa_password,
1✔
811
                                sensitive: true,
1✔
812
                        );
1✔
813
                } else {
814
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_username');
×
815
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_password');
×
816
                }
817

818
                return new DataResponse(['status' => 'success']);
1✔
819
        }
820

821
        /**
822
         * Delete TSA configuration
823
         *
824
         * Delete all TSA configuration fields from the application settings.
825
         *
826
         * @return DataResponse<Http::STATUS_OK, LibresignSuccessStatusResponse, array{}>
827
         *
828
         * 200: OK
829
         */
830
        #[NoCSRFRequired]
831
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
832
        public function deleteTsaConfig(): DataResponse {
833
                $fields = ['tsa_url', 'tsa_policy_oid', 'tsa_auth_type', 'tsa_username', 'tsa_password'];
2✔
834

835
                foreach ($fields as $field) {
2✔
836
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
837
                }
838

839
                return new DataResponse(['status' => 'success']);
2✔
840
        }
841

842
        /**
843
         * Get footer template
844
         *
845
         * Returns the current footer template if set, otherwise returns the default template.
846
         *
847
         * @return DataResponse<Http::STATUS_OK, LibresignFooterTemplateResponse, array{}>
848
         *
849
         * 200: OK
850
         */
851
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/footer-template', requirements: ['apiVersion' => '(v1)'])]
852
        public function getFooterTemplate(): DataResponse {
853
                return new DataResponse([
×
854
                        'template' => $this->footerService->getTemplate(),
×
855
                        'isDefault' => $this->footerService->isDefaultTemplate(),
×
856
                        'preview_width' => $this->appConfig->getValueInt(Application::APP_ID, 'footer_preview_width', 595),
×
857
                        'preview_height' => $this->appConfig->getValueInt(Application::APP_ID, 'footer_preview_height', 100),
×
858
                ]);
×
859
        }
860

861
        /**
862
         * Save footer template and render preview
863
         *
864
         * Saves the footer template and returns the rendered PDF preview.
865
         *
866
         * @param string $template The Twig template to save (empty to reset to default)
867
         * @param int $width Width of preview in points (default: 595 - A4 width)
868
         * @param int $height Height of preview in points (default: 50)
869
         * @return DataDownloadResponse<Http::STATUS_OK, 'application/pdf', array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
870
         *
871
         * 200: OK
872
         * 400: Bad request
873
         */
874
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/footer-template', requirements: ['apiVersion' => '(v1)'])]
875
        public function saveFooterTemplate(string $template = '', int $width = 595, int $height = 50) {
876
                try {
877
                        $this->footerService->saveTemplate($template);
×
878
                        $pdf = $this->footerService->renderPreviewPdf('', $width, $height);
×
879

880
                        return new DataDownloadResponse($pdf, 'footer-preview.pdf', 'application/pdf');
×
881
                } catch (\Exception $e) {
×
882
                        return new DataResponse([
×
883
                                'error' => $e->getMessage(),
×
884
                        ], Http::STATUS_BAD_REQUEST);
×
885
                }
886
        }
887

888
        /**
889
         * Preview footer template as PDF
890
         *
891
         * @NoAdminRequired
892
         * @NoCSRFRequired
893
         *
894
         * @param string $template Template to preview
895
         * @param int $width Width of preview in points (default: 595 - A4 width)
896
         * @param int $height Height of preview in points (default: 50)
897
         * @return DataDownloadResponse<Http::STATUS_OK, 'application/pdf', array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
898
         *
899
         * 200: OK
900
         * 400: Bad request
901
         */
902
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/footer-template/preview-pdf', requirements: ['apiVersion' => '(v1)'])]
903
        public function footerTemplatePreviewPdf(string $template = '', int $width = 595, int $height = 50) {
904
                try {
905
                        $pdf = $this->footerService->renderPreviewPdf($template ?: null, $width, $height);
×
906
                        return new DataDownloadResponse($pdf, 'footer-preview.pdf', 'application/pdf');
×
907
                } catch (\Exception $e) {
×
908
                        return new DataResponse([
×
909
                                'error' => $e->getMessage(),
×
910
                        ], Http::STATUS_BAD_REQUEST);
×
911
                }
912
        }
913

914
        /**
915
         * Set signing mode configuration
916
         *
917
         * Configure whether document signing should be synchronous or asynchronous
918
         *
919
         * @param string $mode Signing mode: "sync" or "async"
920
         * @param string|null $workerType Worker type when async: "local" or "external" (optional)
921
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
922
         *
923
         * 200: Settings saved
924
         * 400: Invalid parameters
925
         * 500: Internal server error
926
         */
927
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signing-mode/config', requirements: ['apiVersion' => '(v1)'])]
928
        public function setSigningModeConfig(string $mode, ?string $workerType = null): DataResponse {
929
                try {
930
                        if (!in_array($mode, ['sync', 'async'], true)) {
×
931
                                return new DataResponse([
×
932
                                        'error' => $this->l10n->t('Invalid signing mode. Use "sync" or "async".'),
×
933
                                ], Http::STATUS_BAD_REQUEST);
×
934
                        }
935

936
                        if ($workerType !== null && !in_array($workerType, ['local', 'external'], true)) {
×
937
                                return new DataResponse([
×
938
                                        'error' => $this->l10n->t('Invalid worker type. Use "local" or "external".'),
×
939
                                ], Http::STATUS_BAD_REQUEST);
×
940
                        }
941

942
                        $this->saveOrDeleteConfig('signing_mode', $mode, 'sync');
×
943
                        $this->saveOrDeleteConfig('worker_type', $workerType, 'local');
×
944

945
                        return new DataResponse([
×
946
                                'message' => $this->l10n->t('Settings saved'),
×
947
                        ]);
×
948
                } catch (\Exception $e) {
×
949
                        return new DataResponse([
×
950
                                'error' => $e->getMessage(),
×
951
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
952
                }
953
        }
954

955
        private function saveOrDeleteConfig(string $key, ?string $value, string $default): void {
956
                if ($value === $default) {
×
957
                        $this->appConfig->deleteKey(Application::APP_ID, $key);
×
958
                } else {
959
                        $this->appConfig->setValueString(Application::APP_ID, $key, $value);
×
960
                }
961
        }
962

963
        /**
964
         * Persist groups allowed to request signatures as typed app config array.
965
         *
966
         * @param string $groups JSON array string
967
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
968
         */
969
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/groups-request-sign/config', requirements: ['apiVersion' => '(v1)'])]
970
        public function setGroupsRequestSignConfig(string $groups = '[]'): DataResponse {
971
                try {
NEW
972
                        $decoded = json_decode($groups, true);
×
NEW
973
                        if (!is_array($decoded)) {
×
NEW
974
                                return new DataResponse([
×
NEW
975
                                        'error' => $this->l10n->t('Invalid groups payload.'),
×
NEW
976
                                ], Http::STATUS_BAD_REQUEST);
×
977
                        }
978

NEW
979
                        $normalizedGroups = array_values(array_map(static fn (mixed $group): string => (string)$group, $decoded));
×
NEW
980
                        $this->appConfig->setValueArray(Application::APP_ID, 'groups_request_sign', $normalizedGroups);
×
981

NEW
982
                        return new DataResponse([
×
NEW
983
                                'message' => $this->l10n->t('Settings saved'),
×
NEW
984
                        ]);
×
NEW
985
                } catch (\Exception $e) {
×
NEW
986
                        return new DataResponse([
×
NEW
987
                                'error' => $e->getMessage(),
×
NEW
988
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
989
                }
990
        }
991

992
        /**
993
         * Set signature flow configuration
994
         *
995
         * @param bool $enabled Whether to force a signature flow for all documents
996
         * @param string|null $mode Signature flow mode: 'parallel' or 'ordered_numeric' (only used when enabled is true)
997
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
998
         *
999
         * 200: Configuration saved successfully
1000
         * 400: Invalid signature flow mode provided
1001
         * 500: Internal server error
1002
         */
1003
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-flow/config', requirements: ['apiVersion' => '(v1)'])]
1004
        public function setSignatureFlowConfig(bool $enabled, ?string $mode = null): DataResponse {
1005
                try {
1006
                        if (!$enabled) {
×
1007
                                $this->appConfig->deleteKey(Application::APP_ID, 'signature_flow');
×
1008
                                return new DataResponse([
×
1009
                                        'message' => $this->l10n->t('Settings saved'),
×
1010
                                ]);
×
1011
                        }
1012

1013
                        if ($mode === null) {
×
1014
                                return new DataResponse([
×
1015
                                        'error' => $this->l10n->t('Mode is required when signature flow is enabled.'),
×
1016
                                ], Http::STATUS_BAD_REQUEST);
×
1017
                        }
1018

1019
                        try {
1020
                                $signatureFlow = \OCA\Libresign\Enum\SignatureFlow::from($mode);
×
1021
                        } catch (\ValueError) {
×
1022
                                return new DataResponse([
×
1023
                                        'error' => $this->l10n->t('Invalid signature flow mode. Use "parallel" or "ordered_numeric".'),
×
1024
                                ], Http::STATUS_BAD_REQUEST);
×
1025
                        }
1026

1027
                        $this->appConfig->setValueString(
×
1028
                                Application::APP_ID,
×
1029
                                'signature_flow',
×
1030
                                $signatureFlow->value
×
1031
                        );
×
1032

1033
                        return new DataResponse([
×
1034
                                'message' => $this->l10n->t('Settings saved'),
×
1035
                        ]);
×
1036
                } catch (\Exception $e) {
×
1037
                        return new DataResponse([
×
1038
                                'error' => $e->getMessage(),
×
1039
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1040
                }
1041
        }
1042

1043
        /**
1044
         * Configure DocMDP signature restrictions
1045
         *
1046
         * @param bool $enabled Whether to enable DocMDP restrictions
1047
         * @param int $defaultLevel DocMDP level: 1 (no changes), 2 (fill forms), 3 (add annotations)
1048
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
1049
         *
1050
         * 200: Configuration saved successfully
1051
         * 400: Invalid DocMDP level provided
1052
         * 500: Internal server error
1053
         */
1054
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/docmdp/config', requirements: ['apiVersion' => '(v1)'])]
1055
        public function setDocMdpConfig(bool $enabled, int $defaultLevel = 2): DataResponse {
1056
                try {
1057
                        $this->docMdpConfigService->setEnabled($enabled);
×
1058

1059
                        if ($enabled) {
×
1060
                                $level = DocMdpLevel::tryFrom($defaultLevel);
×
1061
                                if ($level === null) {
×
1062
                                        return new DataResponse([
×
1063
                                                'error' => $this->l10n->t('Invalid DocMDP level'),
×
1064
                                        ], Http::STATUS_BAD_REQUEST);
×
1065
                                }
1066

1067
                                $this->docMdpConfigService->setLevel($level);
×
1068
                        }
1069

1070
                        return new DataResponse([
×
1071
                                'message' => $this->l10n->t('Settings saved'),
×
1072
                        ]);
×
1073
                } catch (\Exception $e) {
×
1074
                        return new DataResponse([
×
1075
                                'error' => $e->getMessage(),
×
1076
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1077
                }
1078
        }
1079

1080
        /**
1081
         * Get list of files currently being signed (status = SIGNING_IN_PROGRESS)
1082
         *
1083
         * @return DataResponse<Http::STATUS_OK, LibresignActiveSigningsResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
1084
         *
1085
         * 200: List of active signings
1086
         */
1087
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/active-signings', requirements: ['apiVersion' => '(v1)'])]
1088
        public function getActiveSignings(): DataResponse {
1089
                try {
1090
                        $activeSignings = $this->fileMapper->findByStatus(FileStatus::SIGNING_IN_PROGRESS->value);
×
1091

1092
                        $result = [];
×
1093
                        foreach ($activeSignings as $file) {
×
1094
                                $result[] = [
×
1095
                                        'id' => $file->getId(),
×
1096
                                        'uuid' => $file->getUuid(),
×
1097
                                        'name' => $file->getName(),
×
1098
                                        'signerEmail' => $file->getSignerEmail() ?? '',
×
1099
                                        'signerDisplayName' => $file->getSignerName() ?? '',
×
1100
                                        'updatedAt' => $file->getUpdatedAt(),
×
1101
                                ];
×
1102
                        }
1103

1104
                        return new DataResponse([
×
1105
                                'data' => $result,
×
1106
                        ]);
×
1107
                } catch (\Exception $e) {
×
1108
                        return new DataResponse([
×
1109
                                'error' => $e->getMessage(),
×
1110
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1111
                }
1112
        }
1113
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc