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

LibreSign / libresign / 27835001251

19 Jun 2026 03:38PM UTC coverage: 59.155%. First build
27835001251

Pull #7070

github

web-flow
Merge 79c567339 into e7d9ccf99
Pull Request #7070: feat: setup check implementation

331 of 352 new or added lines in 11 files covered. (94.03%)

11356 of 19197 relevant lines covered (59.16%)

7.08 hits per line

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

13.18
/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\InstallService;
26
use OCA\Libresign\Service\ReminderService;
27
use OCA\Libresign\Service\SetupCheckResultService;
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\NoAdminRequired;
34
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
35
use OCP\AppFramework\Http\ContentSecurityPolicy;
36
use OCP\AppFramework\Http\DataDownloadResponse;
37
use OCP\AppFramework\Http\DataResponse;
38
use OCP\AppFramework\Http\FileDisplayResponse;
39
use OCP\Files\SimpleFS\InMemoryFile;
40
use OCP\IAppConfig;
41
use OCP\IEventSource;
42
use OCP\IEventSourceFactory;
43
use OCP\IL10N;
44
use OCP\IRequest;
45
use OCP\ISession;
46
use UnexpectedValueException;
47

48
/**
49
 * @psalm-import-type LibresignCertificateDataGenerated from \OCA\Libresign\ResponseDefinitions
50
 * @psalm-import-type LibresignCertificateEngineConfigResponse from \OCA\Libresign\ResponseDefinitions
51
 * @psalm-import-type LibresignCertificatePolicyResponse from \OCA\Libresign\ResponseDefinitions
52
 * @psalm-import-type LibresignConfigureCheck from \OCA\Libresign\ResponseDefinitions
53
 * @psalm-import-type LibresignConfigureChecksResponse from \OCA\Libresign\ResponseDefinitions
54
 * @psalm-import-type LibresignEngineHandlerResponse from \OCA\Libresign\ResponseDefinitions
55
 * @psalm-import-type LibresignErrorResponse from \OCA\Libresign\ResponseDefinitions
56
 * @psalm-import-type LibresignErrorStatusResponse from \OCA\Libresign\ResponseDefinitions
57
 * @psalm-import-type LibresignEngineHandler from \OCA\Libresign\ResponseDefinitions
58
 * @psalm-import-type LibresignIdentifyMethodSetting from \OCA\Libresign\ResponseDefinitions
59
 * @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
60
 * @psalm-import-type LibresignSignatureTextSettingsResponse from \OCA\Libresign\ResponseDefinitions
61
 * @psalm-import-type LibresignSignatureTemplateSettingsResponse from \OCA\Libresign\ResponseDefinitions
62
 * @psalm-import-type LibresignSuccessStatusResponse from \OCA\Libresign\ResponseDefinitions
63
 * @psalm-import-type LibresignFailureStatusResponse from \OCA\Libresign\ResponseDefinitions
64
 * @psalm-import-type LibresignActiveSigningsResponse from \OCA\Libresign\ResponseDefinitions
65
 * @psalm-import-type LibresignReminderSettings from \OCA\Libresign\ResponseDefinitions
66
 * @psalm-import-type LibresignRootCertificate from \OCA\Libresign\ResponseDefinitions
67
 * @psalm-import-type LibresignFooterTemplateResponse from \OCA\Libresign\ResponseDefinitions
68
 */
69
class AdminController extends AEnvironmentAwareController {
70
        private IEventSource $eventSource;
71
        public function __construct(
72
                IRequest $request,
73
                private IAppConfig $appConfig,
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
                private SetupCheckResultService $setupCheckResultService,
89
        ) {
90
                parent::__construct(Application::APP_ID, $request);
7✔
91
                $this->eventSource = $this->eventSourceFactory->create();
7✔
92
        }
93

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

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

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

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

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

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

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

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

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

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

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

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

282
                return new DataResponse();
×
283
        }
284

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

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

NEW
325
                $this->eventSource->send('configure_check', $this->setupCheckResultService->getLegacyFormattedChecks());
×
326
                $this->eventSource->send('done', '');
×
327
                $this->eventSource->close();
×
328
                // Nextcloud inject a lot of headers that is incompatible with SSE
329
                exit();
×
330
        }
331

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

380
                return new DataResponse(
×
381
                        [
×
382
                                'status' => 'success',
×
383
                        ]
×
384
                );
×
385
        }
386

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

778
                $authType = $tsa_auth_type ?? 'none';
1✔
779
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
780

781
                if ($authType === 'basic') {
1✔
782
                        $hasUsername = !empty($tsa_username);
1✔
783
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
784

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

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

814
                return new DataResponse(['status' => 'success']);
1✔
815
        }
816

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

831
                foreach ($fields as $field) {
2✔
832
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
833
                }
834

835
                return new DataResponse(['status' => 'success']);
2✔
836
        }
837

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

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

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

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

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

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

937
                        $this->saveOrDeleteConfig('signing_mode', $mode, 'sync');
×
938
                        $this->saveOrDeleteConfig('worker_type', $workerType, 'local');
×
939

940
                        return new DataResponse([
×
941
                                'message' => $this->l10n->t('Settings saved'),
×
942
                        ]);
×
943
                } catch (\Exception $e) {
×
944
                        return new DataResponse([
×
945
                                'error' => $e->getMessage(),
×
946
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
947
                }
948
        }
949

950
        private function saveOrDeleteConfig(string $key, ?string $value, string $default): void {
951
                if ($value === $default) {
×
952
                        $this->appConfig->deleteKey(Application::APP_ID, $key);
×
953
                } else {
954
                        $this->appConfig->setValueString(Application::APP_ID, $key, $value);
×
955
                }
956
        }
957

958
        /**
959
         * Persist groups allowed to request signatures as typed app config array
960
         *
961
         * @param list<string> $groups List of group IDs allowed to request signatures
962
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
963
         *
964
         * 200: Settings saved
965
         * 500: Internal server error
966
         */
967
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/groups-request-sign/config', requirements: ['apiVersion' => '(v1)'])]
968
        public function setGroupsRequestSignConfig(array $groups = []): DataResponse {
969
                try {
970
                        $normalizedGroups = array_values(array_map(static fn (mixed $group): string => (string)$group, $groups));
2✔
971
                        $this->appConfig->setValueArray(Application::APP_ID, 'groups_request_sign', $normalizedGroups);
2✔
972

973
                        return new DataResponse([
2✔
974
                                'message' => $this->l10n->t('Settings saved'),
2✔
975
                        ]);
2✔
976
                } catch (\Exception $e) {
×
977
                        return new DataResponse([
×
978
                                'error' => $e->getMessage(),
×
979
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
980
                }
981
        }
982

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

1004
                        if ($mode === null) {
×
1005
                                return new DataResponse([
×
1006
                                        'error' => $this->l10n->t('Mode is required when signature flow is enabled.'),
×
1007
                                ], Http::STATUS_BAD_REQUEST);
×
1008
                        }
1009

1010
                        try {
1011
                                $signatureFlow = \OCA\Libresign\Enum\SignatureFlow::from($mode);
×
1012
                        } catch (\ValueError) {
×
1013
                                return new DataResponse([
×
1014
                                        'error' => $this->l10n->t('Invalid signature flow mode. Use "parallel" or "ordered_numeric".'),
×
1015
                                ], Http::STATUS_BAD_REQUEST);
×
1016
                        }
1017

1018
                        $this->appConfig->setValueString(
×
1019
                                Application::APP_ID,
×
1020
                                'signature_flow',
×
1021
                                $signatureFlow->value
×
1022
                        );
×
1023

1024
                        return new DataResponse([
×
1025
                                'message' => $this->l10n->t('Settings saved'),
×
1026
                        ]);
×
1027
                } catch (\Exception $e) {
×
1028
                        return new DataResponse([
×
1029
                                'error' => $e->getMessage(),
×
1030
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1031
                }
1032
        }
1033

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

1050
                        if ($enabled) {
×
1051
                                $level = DocMdpLevel::tryFrom($defaultLevel);
×
1052
                                if ($level === null) {
×
1053
                                        return new DataResponse([
×
1054
                                                'error' => $this->l10n->t('Invalid DocMDP level'),
×
1055
                                        ], Http::STATUS_BAD_REQUEST);
×
1056
                                }
1057

1058
                                $this->docMdpConfigService->setLevel($level);
×
1059
                        }
1060

1061
                        return new DataResponse([
×
1062
                                'message' => $this->l10n->t('Settings saved'),
×
1063
                        ]);
×
1064
                } catch (\Exception $e) {
×
1065
                        return new DataResponse([
×
1066
                                'error' => $e->getMessage(),
×
1067
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1068
                }
1069
        }
1070

1071
        /**
1072
         * Get list of files currently being signed (status = SIGNING_IN_PROGRESS)
1073
         *
1074
         * @return DataResponse<Http::STATUS_OK, LibresignActiveSigningsResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
1075
         *
1076
         * 200: List of active signings
1077
         */
1078
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/active-signings', requirements: ['apiVersion' => '(v1)'])]
1079
        public function getActiveSignings(): DataResponse {
1080
                try {
1081
                        $activeSignings = $this->fileMapper->findByStatus(FileStatus::SIGNING_IN_PROGRESS->value);
×
1082

1083
                        $result = [];
×
1084
                        foreach ($activeSignings as $file) {
×
1085
                                $result[] = [
×
1086
                                        'id' => $file->getId(),
×
1087
                                        'uuid' => $file->getUuid(),
×
1088
                                        'name' => $file->getName(),
×
1089
                                        'signerEmail' => $file->getSignerEmail() ?? '',
×
1090
                                        'signerDisplayName' => $file->getSignerName() ?? '',
×
1091
                                        'updatedAt' => $file->getUpdatedAt(),
×
1092
                                ];
×
1093
                        }
1094

1095
                        return new DataResponse([
×
1096
                                'data' => $result,
×
1097
                        ]);
×
1098
                } catch (\Exception $e) {
×
1099
                        return new DataResponse([
×
1100
                                'error' => $e->getMessage(),
×
1101
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1102
                }
1103
        }
1104
}
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