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

LibreSign / libresign / 27454170133

13 Jun 2026 02:40AM UTC coverage: 56.884%. First build
27454170133

Pull #7740

github

web-flow
Merge 26d3ba702 into 55aab64af
Pull Request #7740: chore: migrate to PHP 8.3

23 of 35 new or added lines in 20 files covered. (65.71%)

10751 of 18900 relevant lines covered (56.88%)

7.01 hits per line

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

13.06
/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\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 ConfigureCheckService $configureCheckService,
75
                private InstallService $installService,
76
                private CertificateEngineFactory $certificateEngineFactory,
77
                private IEventSourceFactory $eventSourceFactory,
78
                private SignatureTextService $signatureTextService,
79
                private IL10N $l10n,
80
                protected ISession $session,
81
                private SignatureBackgroundService $signatureBackgroundService,
82
                private CertificatePolicyService $certificatePolicyService,
83
                private ValidateService $validateService,
84
                private ReminderService $reminderService,
85
                private FooterService $footerService,
86
                private DocMdpConfigService $docMdpConfigService,
87
                private IdentifyMethodService $identifyMethodService,
88
                private FileMapper $fileMapper,
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'])) {
×
NEW
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 {
261
                /** @var LibresignConfigureChecksResponse $configureCheckList */
262
                $configureCheckList = array_values($this->configureCheckService->checkAll());
×
263
                return new DataResponse(
×
264
                        $configureCheckList
×
265
                );
×
266
        }
267

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

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

286
                return new DataResponse();
×
287
        }
288

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

889
        /**
890
         * Preview footer template as PDF
891
         *
892
         * @param string $template Template to preview
893
         * @param int $width Width of preview in points (default: 595 - A4 width)
894
         * @param int $height Height of preview in points (default: 50)
895
         * @return DataDownloadResponse<Http::STATUS_OK, 'application/pdf', array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
896
         *
897
         * 200: OK
898
         * 400: Bad request
899
         */
900
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/footer-template/preview-pdf', requirements: ['apiVersion' => '(v1)'])]
901
        #[NoCSRFRequired]
902
        #[NoAdminRequired]
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 list<string> $groups List of group IDs allowed to request signatures
967
         * @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignErrorResponse, array{}>
968
         *
969
         * 200: Settings saved
970
         * 500: Internal server error
971
         */
972
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/groups-request-sign/config', requirements: ['apiVersion' => '(v1)'])]
973
        public function setGroupsRequestSignConfig(array $groups = []): DataResponse {
974
                try {
975
                        $normalizedGroups = array_values(array_map(static fn (mixed $group): string => (string)$group, $groups));
2✔
976
                        $this->appConfig->setValueArray(Application::APP_ID, 'groups_request_sign', $normalizedGroups);
2✔
977

978
                        return new DataResponse([
2✔
979
                                'message' => $this->l10n->t('Settings saved'),
2✔
980
                        ]);
2✔
981
                } catch (\Exception $e) {
×
982
                        return new DataResponse([
×
983
                                'error' => $e->getMessage(),
×
984
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
985
                }
986
        }
987

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

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

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

1023
                        $this->appConfig->setValueString(
×
1024
                                Application::APP_ID,
×
1025
                                'signature_flow',
×
1026
                                $signatureFlow->value
×
1027
                        );
×
1028

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

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

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

1063
                                $this->docMdpConfigService->setLevel($level);
×
1064
                        }
1065

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

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

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

1100
                        return new DataResponse([
×
1101
                                'data' => $result,
×
1102
                        ]);
×
1103
                } catch (\Exception $e) {
×
1104
                        return new DataResponse([
×
1105
                                'error' => $e->getMessage(),
×
1106
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1107
                }
1108
        }
1109
}
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