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

LibreSign / libresign / 22099025905

17 Feb 2026 12:46PM UTC coverage: 52.533%. First build
22099025905

Pull #6927

github

web-flow
Merge bb92e9539 into 1a7855380
Pull Request #6927: fix: signing mode sync default

2 of 4 new or added lines in 4 files covered. (50.0%)

9332 of 17764 relevant lines covered (52.53%)

6.24 hits per line

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

12.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\ResponseDefinitions;
21
use OCA\Libresign\Service\Certificate\ValidateService;
22
use OCA\Libresign\Service\CertificatePolicyService;
23
use OCA\Libresign\Service\DocMdpConfigService;
24
use OCA\Libresign\Service\FooterService;
25
use OCA\Libresign\Service\IdentifyMethodService;
26
use OCA\Libresign\Service\Install\ConfigureCheckService;
27
use OCA\Libresign\Service\Install\InstallService;
28
use OCA\Libresign\Service\ReminderService;
29
use OCA\Libresign\Service\SignatureBackgroundService;
30
use OCA\Libresign\Service\SignatureTextService;
31
use OCA\Libresign\Settings\Admin;
32
use OCP\AppFramework\Http;
33
use OCP\AppFramework\Http\Attribute\ApiRoute;
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 LibresignEngineHandler from ResponseDefinitions
50
 * @psalm-import-type LibresignCetificateDataGenerated from ResponseDefinitions
51
 * @psalm-import-type LibresignConfigureCheck from ResponseDefinitions
52
 * @psalm-import-type LibresignRootCertificate from ResponseDefinitions
53
 * @psalm-import-type LibresignReminderSettings from ResponseDefinitions
54
 */
55
class AdminController extends AEnvironmentAwareController {
56
        private IEventSource $eventSource;
57
        public function __construct(
58
                IRequest $request,
59
                private IAppConfig $appConfig,
60
                private ConfigureCheckService $configureCheckService,
61
                private InstallService $installService,
62
                private CertificateEngineFactory $certificateEngineFactory,
63
                private IEventSourceFactory $eventSourceFactory,
64
                private SignatureTextService $signatureTextService,
65
                private IL10N $l10n,
66
                protected ISession $session,
67
                private SignatureBackgroundService $signatureBackgroundService,
68
                private CertificatePolicyService $certificatePolicyService,
69
                private ValidateService $validateService,
70
                private ReminderService $reminderService,
71
                private FooterService $footerService,
72
                private DocMdpConfigService $docMdpConfigService,
73
                private IdentifyMethodService $identifyMethodService,
74
                private FileMapper $fileMapper,
75
        ) {
76
                parent::__construct(Application::APP_ID, $request);
5✔
77
                $this->eventSource = $this->eventSourceFactory->create();
5✔
78
        }
79

80
        /**
81
         * Generate certificate using CFSSL engine
82
         *
83
         * @param array{commonName: string, names: array<string, array{value:string|array<string>}>} $rootCert fields of root certificate
84
         * @param string $cfsslUri URI of CFSSL API
85
         * @param string $configPath Path of config files of CFSSL
86
         * @return DataResponse<Http::STATUS_OK, array{data: LibresignEngineHandler}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
87
         *
88
         * 200: OK
89
         * 401: Account not found
90
         */
91
        #[NoCSRFRequired]
92
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate/cfssl', requirements: ['apiVersion' => '(v1)'])]
93
        public function generateCertificateCfssl(
94
                array $rootCert,
95
                string $cfsslUri = '',
96
                string $configPath = '',
97
        ): DataResponse {
98
                try {
99
                        $engineHandler = $this->generateCertificate($rootCert, [
×
100
                                'engine' => 'cfssl',
×
101
                                'configPath' => trim($configPath),
×
102
                                'cfsslUri' => trim($cfsslUri),
×
103
                        ])->toArray();
×
104
                        return new DataResponse([
×
105
                                'data' => $engineHandler,
×
106
                        ]);
×
107
                } catch (\Exception $exception) {
×
108
                        return new DataResponse(
×
109
                                [
×
110
                                        'message' => $exception->getMessage()
×
111
                                ],
×
112
                                Http::STATUS_UNAUTHORIZED
×
113
                        );
×
114
                }
115
        }
116

117
        /**
118
         * Generate certificate using OpenSSL engine
119
         *
120
         * @param array{commonName: string, names: array<string, array{value:string|array<string>}>} $rootCert fields of root certificate
121
         * @param string $configPath Path of config files of CFSSL
122
         * @return DataResponse<Http::STATUS_OK, array{data: LibresignEngineHandler}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
123
         *
124
         * 200: OK
125
         * 401: Account not found
126
         */
127
        #[NoCSRFRequired]
128
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate/openssl', requirements: ['apiVersion' => '(v1)'])]
129
        public function generateCertificateOpenSsl(
130
                array $rootCert,
131
                string $configPath = '',
132
        ): DataResponse {
133
                try {
134
                        $engineHandler = $this->generateCertificate($rootCert, [
1✔
135
                                'engine' => 'openssl',
1✔
136
                                'configPath' => trim($configPath),
1✔
137
                        ])->toArray();
1✔
138
                        return new DataResponse([
×
139
                                'data' => $engineHandler,
×
140
                        ]);
×
141
                } catch (\Exception $exception) {
1✔
142
                        return new DataResponse(
1✔
143
                                [
1✔
144
                                        'message' => $exception->getMessage()
1✔
145
                                ],
1✔
146
                                Http::STATUS_UNAUTHORIZED
1✔
147
                        );
1✔
148
                }
149
        }
150

151
        /**
152
         * Set certificate engine
153
         *
154
         * Sets the certificate engine (openssl, cfssl, or none) and automatically configures identify_methods when needed
155
         *
156
         * @param string $engine The certificate engine to use (openssl, cfssl, or none)
157
         * @return DataResponse<Http::STATUS_OK, array{engine: string, identify_methods: array<array<string, mixed>>}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{message: string}, array{}>
158
         *
159
         * 200: OK
160
         * 400: Invalid engine
161
         */
162
        #[NoCSRFRequired]
163
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate/engine', requirements: ['apiVersion' => '(v1)'])]
164
        public function setCertificateEngine(string $engine): DataResponse {
165
                $validEngines = ['openssl', 'cfssl', 'none'];
×
166
                if (!in_array($engine, $validEngines, true)) {
×
167
                        return new DataResponse(
×
168
                                ['message' => 'Invalid engine. Must be one of: ' . implode(', ', $validEngines)],
×
169
                                Http::STATUS_BAD_REQUEST
×
170
                        );
×
171
                }
172

173
                $handler = $this->certificateEngineFactory->getEngine();
×
174
                $handler->setEngine($engine);
×
175
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings();
×
176

177
                return new DataResponse([
×
178
                        'engine' => $engine,
×
179
                        'identify_methods' => $identifyMethods,
×
180
                ]);
×
181
        }
182

183
        private function generateCertificate(
184
                array $rootCert,
185
                array $properties = [],
186
        ): IEngineHandler {
187
                $names = [];
1✔
188
                if (isset($rootCert['names'])) {
1✔
189
                        $this->validateService->validateNames($rootCert['names']);
1✔
190
                        foreach ($rootCert['names'] as $item) {
×
191
                                if (is_array($item['value'])) {
×
192
                                        $trimmedValues = array_map('trim', $item['value']);
×
193
                                        $names[$item['id']]['value'] = array_filter($trimmedValues, fn ($val) => $val !== '');
×
194
                                } else {
195
                                        $names[$item['id']]['value'] = trim((string)$item['value']);
×
196
                                }
197
                        }
198
                }
199
                $this->validateService->validate('CN', $rootCert['commonName']);
×
200
                $this->installService->generate(
×
201
                        trim((string)$rootCert['commonName']),
×
202
                        $properties['engine'],
×
203
                        $names,
×
204
                        $properties,
×
205
                );
×
206

207
                return $this->certificateEngineFactory->getEngine();
×
208
        }
209

210
        /**
211
         * Load certificate data
212
         *
213
         * Return all data of root certificate and a field called `generated` with a boolean value.
214
         *
215
         * @return DataResponse<Http::STATUS_OK, LibresignCetificateDataGenerated, array{}>
216
         *
217
         * 200: OK
218
         */
219
        #[NoCSRFRequired]
220
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/certificate', requirements: ['apiVersion' => '(v1)'])]
221
        public function loadCertificate(): DataResponse {
222
                $engine = $this->certificateEngineFactory->getEngine();
1✔
223
                /** @var LibresignEngineHandler */
224
                $certificate = $engine->toArray();
1✔
225
                $configureResult = $engine->configureCheck();
1✔
226
                $success = array_filter(
1✔
227
                        $configureResult,
1✔
228
                        fn (ConfigureCheckHelper $config) => $config->getStatus() === 'success'
1✔
229
                );
1✔
230
                $certificate['generated'] = count($success) === count($configureResult);
1✔
231

232
                return new DataResponse($certificate);
1✔
233
        }
234

235
        /**
236
         * Check the configuration of LibreSign
237
         *
238
         * Return the status of necessary configuration and tips to fix the problems.
239
         *
240
         * @return DataResponse<Http::STATUS_OK, LibresignConfigureCheck[], array{}>
241
         *
242
         * 200: OK
243
         */
244
        #[NoCSRFRequired]
245
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/configure-check', requirements: ['apiVersion' => '(v1)'])]
246
        public function configureCheck(): DataResponse {
247
                /** @var LibresignConfigureCheck[] */
248
                $configureCheckList = $this->configureCheckService->checkAll();
×
249
                return new DataResponse(
×
250
                        $configureCheckList
×
251
                );
×
252
        }
253

254
        /**
255
         * Disable hate limit to current session
256
         *
257
         * This will disable hate limit to current session.
258
         *
259
         * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
260
         *
261
         * 200: OK
262
         */
263
        #[NoCSRFRequired]
264
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/disable-hate-limit', requirements: ['apiVersion' => '(v1)'])]
265
        public function disableHateLimit(): DataResponse {
266
                $this->session->set('app_api', true);
×
267

268
                // TODO: Remove after drop support NC29
269
                // deprecated since AppAPI 2.8.0
270
                $this->session->set('app_api_system', true);
×
271

272
                return new DataResponse();
×
273
        }
274

275
        /**
276
         * @IgnoreOpenAPI
277
         */
278
        #[NoCSRFRequired]
279
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/install-and-validate', requirements: ['apiVersion' => '(v1)'])]
280
        public function installAndValidate(): void {
281
                try {
282
                        $async = \function_exists('proc_open');
×
283
                        $this->installService->installJava($async);
×
284
                        $this->installService->installJSignPdf($async);
×
285
                        $this->installService->installPdftk($async);
×
286
                        if ($this->appConfig->getValueString(Application::APP_ID, 'certificate_engine') === 'cfssl') {
×
287
                                $this->installService->installCfssl($async);
×
288
                        }
289

290
                        $this->configureCheckService->disableCache();
×
291
                        $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
292
                        $seconds = 0;
×
293
                        while ($this->installService->isDownloadWip()) {
×
294
                                $totalSize = $this->installService->getTotalSize();
×
295
                                $this->eventSource->send('total_size', json_encode($totalSize));
×
296
                                if ($errors = $this->installService->getErrorMessages()) {
×
297
                                        $this->eventSource->send('errors', json_encode($errors));
×
298
                                }
299
                                usleep(200000); // 0.2 seconds
×
300
                                $seconds += 0.2;
×
301
                                if ($seconds === 5.0) {
×
302
                                        $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
303
                                        $seconds = 0;
×
304
                                }
305
                        }
306
                        if ($errors = $this->installService->getErrorMessages()) {
×
307
                                $this->eventSource->send('errors', json_encode($errors));
×
308
                        }
309
                } catch (\Exception $exception) {
×
310
                        $this->eventSource->send('errors', json_encode([
×
311
                                $this->l10n->t('Could not download binaries.'),
×
312
                                $exception->getMessage(),
×
313
                        ]));
×
314
                }
315

316
                $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
317
                $this->eventSource->send('done', '');
×
318
                $this->eventSource->close();
×
319
                // Nextcloud inject a lot of headers that is incompatible with SSE
320
                exit();
×
321
        }
322

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

371
                return new DataResponse(
×
372
                        [
×
373
                                'status' => 'success',
×
374
                        ]
×
375
                );
×
376
        }
377

378
        /**
379
         * Get custom background image
380
         *
381
         * @return FileDisplayResponse<Http::STATUS_OK, array{}>
382
         *
383
         * 200: Image returned
384
         */
385
        #[NoCSRFRequired]
386
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
387
        public function signatureBackgroundGet(): FileDisplayResponse {
388
                $file = $this->signatureBackgroundService->getImage();
×
389

390
                $response = new FileDisplayResponse($file);
×
391
                $csp = new ContentSecurityPolicy();
×
392
                $csp->allowInlineStyle();
×
393
                $response->setContentSecurityPolicy($csp);
×
394
                $response->cacheFor(3600);
×
395
                $response->addHeader('Content-Type', 'image/png');
×
396
                $response->addHeader('Content-Disposition', 'attachment; filename="background.png"');
×
397
                $response->addHeader('Content-Type', 'image/png');
×
398
                return $response;
×
399
        }
400

401
        /**
402
         * Reset the background image to be the default of LibreSign
403
         *
404
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
405
         *
406
         * 200: Image reseted to default
407
         */
408
        #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
409
        public function signatureBackgroundReset(): DataResponse {
410
                $this->signatureBackgroundService->reset();
×
411
                return new DataResponse(
×
412
                        [
×
413
                                'status' => 'success',
×
414
                        ]
×
415
                );
×
416
        }
417

418
        /**
419
         * Delete background image
420
         *
421
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
422
         *
423
         * 200: Deleted with success
424
         */
425
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
426
        public function signatureBackgroundDelete(): DataResponse {
427
                $this->signatureBackgroundService->delete();
×
428
                return new DataResponse(
×
429
                        [
×
430
                                'status' => 'success',
×
431
                        ]
×
432
                );
×
433
        }
434

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

485
        /**
486
         * Get parsed signature text service
487
         *
488
         * @param string $template Template to signature text
489
         * @param string $context Context for parsing the template
490
         * @return DataResponse<Http::STATUS_OK, array{template: string,parsed: string, templateFontSize: float, signatureFontSize: float, signatureWidth: float, signatureHeight: float, renderMode: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
491
         *
492
         * 200: OK
493
         * 400: Bad request
494
         */
495
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-text', requirements: ['apiVersion' => '(v1)'])]
496
        public function signatureTextGet(string $template = '', string $context = ''): DataResponse {
497
                $context = json_decode($context, true) ?? [];
×
498
                try {
499
                        $return = $this->signatureTextService->parse($template, $context);
×
500
                        return new DataResponse(
×
501
                                $return,
×
502
                                Http::STATUS_OK
×
503
                        );
×
504
                } catch (LibresignException $th) {
×
505
                        return new DataResponse(
×
506
                                [
×
507
                                        'error' => $th->getMessage(),
×
508
                                ],
×
509
                                Http::STATUS_BAD_REQUEST
×
510
                        );
×
511
                }
512
        }
513

514
        /**
515
         * Get signature settings
516
         *
517
         * @return DataResponse<Http::STATUS_OK, array{default_signature_text_template: string, signature_available_variables: array<string, string>}, array{}>
518
         *
519
         * 200: OK
520
         */
521
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-settings', requirements: ['apiVersion' => '(v1)'])]
522
        public function getSignatureSettings(): DataResponse {
523
                $response = [
×
524
                        'signature_available_variables' => $this->signatureTextService->getAvailableVariables(),
×
525
                        'default_signature_text_template' => $this->signatureTextService->getDefaultTemplate(),
×
526
                ];
×
527
                return new DataResponse($response);
×
528
        }
529

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

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

632
        /**
633
         * Delete certificate policy of this instance
634
         *
635
         * @return DataResponse<Http::STATUS_OK, array{}, array{}>
636
         *
637
         * 200: OK
638
         * 404: Not found
639
         */
640
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
641
        public function deleteCertificatePolicy(): DataResponse {
642
                $this->certificatePolicyService->deleteFile();
×
643
                return new DataResponse();
×
644
        }
645

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

675
        /**
676
         * Get reminder settings
677
         *
678
         * @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
679
         *
680
         * 200: OK
681
         */
682
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
683
        public function reminderFetch(): DataResponse {
684
                $response = $this->reminderService->getSettings();
×
685
                if ($response['next_run'] instanceof \DateTime) {
×
686
                        $response['next_run'] = $response['next_run']->format(DateTimeInterface::ATOM);
×
687
                }
688
                return new DataResponse($response);
×
689
        }
690

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

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

745
                $trimmedUrl = trim($tsa_url);
1✔
746
                if (!filter_var($trimmedUrl, FILTER_VALIDATE_URL)
1✔
747
                        || !in_array(parse_url($trimmedUrl, PHP_URL_SCHEME), ['http', 'https'])) {
1✔
748
                        return new DataResponse([
×
749
                                'status' => 'error',
×
750
                                'message' => 'Invalid URL format'
×
751
                        ], Http::STATUS_BAD_REQUEST);
×
752
                }
753

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

756
                if (empty($tsa_policy_oid)) {
1✔
757
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_policy_oid');
1✔
758
                } else {
759
                        $trimmedOid = trim($tsa_policy_oid);
×
760
                        if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $trimmedOid)) {
×
761
                                return new DataResponse([
×
762
                                        'status' => 'error',
×
763
                                        'message' => 'Invalid OID format'
×
764
                                ], Http::STATUS_BAD_REQUEST);
×
765
                        }
766
                        $this->appConfig->setValueString(Application::APP_ID, 'tsa_policy_oid', $trimmedOid);
×
767
                }
768

769
                $authType = $tsa_auth_type ?? 'none';
1✔
770
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
771

772
                if ($authType === 'basic') {
1✔
773
                        $hasUsername = !empty($tsa_username);
1✔
774
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
775

776
                        if (!$hasUsername && !$hasPassword) {
1✔
777
                                return new DataResponse([
×
778
                                        'status' => 'error',
×
779
                                        'message' => 'Username and password are required for basic authentication'
×
780
                                ], Http::STATUS_BAD_REQUEST);
×
781
                        } elseif (!$hasUsername) {
1✔
782
                                return new DataResponse([
×
783
                                        'status' => 'error',
×
784
                                        'message' => 'Username is required'
×
785
                                ], Http::STATUS_BAD_REQUEST);
×
786
                        } elseif (!$hasPassword) {
1✔
787
                                return new DataResponse([
×
788
                                        'status' => 'error',
×
789
                                        'message' => 'Password is required'
×
790
                                ], Http::STATUS_BAD_REQUEST);
×
791
                        }
792

793
                        $this->appConfig->setValueString(Application::APP_ID, 'tsa_username', trim($tsa_username));
1✔
794
                        $this->appConfig->setValueString(
1✔
795
                                Application::APP_ID,
1✔
796
                                key: 'tsa_password',
1✔
797
                                value: $tsa_password,
1✔
798
                                sensitive: true,
1✔
799
                        );
1✔
800
                } else {
801
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_username');
×
802
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_password');
×
803
                }
804

805
                return new DataResponse(['status' => 'success']);
1✔
806
        }
807

808
        /**
809
         * Delete TSA configuration
810
         *
811
         * Delete all TSA configuration fields from the application settings.
812
         *
813
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
814
         *
815
         * 200: OK
816
         */
817
        #[NoCSRFRequired]
818
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
819
        public function deleteTsaConfig(): DataResponse {
820
                $fields = ['tsa_url', 'tsa_policy_oid', 'tsa_auth_type', 'tsa_username', 'tsa_password'];
2✔
821

822
                foreach ($fields as $field) {
2✔
823
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
824
                }
825

826
                return new DataResponse(['status' => 'success']);
2✔
827
        }
828

829
        /**
830
         * Get footer template
831
         *
832
         * Returns the current footer template if set, otherwise returns the default template.
833
         *
834
         * @return DataResponse<Http::STATUS_OK, array{template: string, isDefault: bool, preview_width: int, preview_height: int}, array{}>
835
         *
836
         * 200: OK
837
         */
838
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/footer-template', requirements: ['apiVersion' => '(v1)'])]
839
        public function getFooterTemplate(): DataResponse {
840
                return new DataResponse([
×
841
                        'template' => $this->footerService->getTemplate(),
×
842
                        'isDefault' => $this->footerService->isDefaultTemplate(),
×
843
                        'preview_width' => $this->appConfig->getValueInt(Application::APP_ID, 'footer_preview_width', 595),
×
844
                        'preview_height' => $this->appConfig->getValueInt(Application::APP_ID, 'footer_preview_height', 100),
×
845
                ]);
×
846
        }
847

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

867
                        return new DataDownloadResponse($pdf, 'footer-preview.pdf', 'application/pdf');
×
868
                } catch (\Exception $e) {
×
869
                        return new DataResponse([
×
870
                                'error' => $e->getMessage(),
×
871
                        ], Http::STATUS_BAD_REQUEST);
×
872
                }
873
        }
874

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

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

923
                        if ($workerType !== null && !in_array($workerType, ['local', 'external'], true)) {
×
924
                                return new DataResponse([
×
925
                                        'error' => $this->l10n->t('Invalid worker type. Use "local" or "external".'),
×
926
                                ], Http::STATUS_BAD_REQUEST);
×
927
                        }
928

NEW
929
                        $this->saveOrDeleteConfig('signing_mode', $mode, 'sync');
×
930
                        $this->saveOrDeleteConfig('worker_type', $workerType, 'local');
×
931

932
                        return new DataResponse([
×
933
                                'message' => $this->l10n->t('Settings saved'),
×
934
                        ]);
×
935
                } catch (\Exception $e) {
×
936
                        return new DataResponse([
×
937
                                'error' => $e->getMessage(),
×
938
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
939
                }
940
        }
941

942
        private function saveOrDeleteConfig(string $key, ?string $value, string $default): void {
943
                if ($value === $default) {
×
944
                        $this->appConfig->deleteKey(Application::APP_ID, $key);
×
945
                } else {
946
                        $this->appConfig->setValueString(Application::APP_ID, $key, $value);
×
947
                }
948
        }
949

950
        /**
951
         * Set signature flow configuration
952
         *
953
         * @param bool $enabled Whether to force a signature flow for all documents
954
         * @param string|null $mode Signature flow mode: 'parallel' or 'ordered_numeric' (only used when enabled is true)
955
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
956
         *
957
         * 200: Configuration saved successfully
958
         * 400: Invalid signature flow mode provided
959
         * 500: Internal server error
960
         */
961
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-flow/config', requirements: ['apiVersion' => '(v1)'])]
962
        public function setSignatureFlowConfig(bool $enabled, ?string $mode = null): DataResponse {
963
                try {
964
                        if (!$enabled) {
×
965
                                $this->appConfig->deleteKey(Application::APP_ID, 'signature_flow');
×
966
                                return new DataResponse([
×
967
                                        'message' => $this->l10n->t('Settings saved'),
×
968
                                ]);
×
969
                        }
970

971
                        if ($mode === null) {
×
972
                                return new DataResponse([
×
973
                                        'error' => $this->l10n->t('Mode is required when signature flow is enabled.'),
×
974
                                ], Http::STATUS_BAD_REQUEST);
×
975
                        }
976

977
                        try {
978
                                $signatureFlow = \OCA\Libresign\Enum\SignatureFlow::from($mode);
×
979
                        } catch (\ValueError) {
×
980
                                return new DataResponse([
×
981
                                        'error' => $this->l10n->t('Invalid signature flow mode. Use "parallel" or "ordered_numeric".'),
×
982
                                ], Http::STATUS_BAD_REQUEST);
×
983
                        }
984

985
                        $this->appConfig->setValueString(
×
986
                                Application::APP_ID,
×
987
                                'signature_flow',
×
988
                                $signatureFlow->value
×
989
                        );
×
990

991
                        return new DataResponse([
×
992
                                'message' => $this->l10n->t('Settings saved'),
×
993
                        ]);
×
994
                } catch (\Exception $e) {
×
995
                        return new DataResponse([
×
996
                                'error' => $e->getMessage(),
×
997
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
998
                }
999
        }
1000

1001
        /**
1002
         * Configure DocMDP signature restrictions
1003
         *
1004
         * @param bool $enabled Whether to enable DocMDP restrictions
1005
         * @param int $defaultLevel DocMDP level: 1 (no changes), 2 (fill forms), 3 (add annotations)
1006
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
1007
         *
1008
         * 200: Configuration saved successfully
1009
         * 400: Invalid DocMDP level provided
1010
         * 500: Internal server error
1011
         */
1012
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/docmdp/config', requirements: ['apiVersion' => '(v1)'])]
1013
        public function setDocMdpConfig(bool $enabled, int $defaultLevel = 2): DataResponse {
1014
                try {
1015
                        $this->docMdpConfigService->setEnabled($enabled);
×
1016

1017
                        if ($enabled) {
×
1018
                                $level = DocMdpLevel::tryFrom($defaultLevel);
×
1019
                                if ($level === null) {
×
1020
                                        return new DataResponse([
×
1021
                                                'error' => $this->l10n->t('Invalid DocMDP level'),
×
1022
                                        ], Http::STATUS_BAD_REQUEST);
×
1023
                                }
1024

1025
                                $this->docMdpConfigService->setLevel($level);
×
1026
                        }
1027

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

1038
        /**
1039
         * Get list of files currently being signed (status = SIGNING_IN_PROGRESS)
1040
         *
1041
         * @return DataResponse<Http::STATUS_OK, array{data: list<array{id: int, uuid: string, name: string, signerEmail: string, signerDisplayName: string, updatedAt: int}>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
1042
         *
1043
         * 200: List of active signings
1044
         */
1045
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/active-signings', requirements: ['apiVersion' => '(v1)'])]
1046
        public function getActiveSignings(): DataResponse {
1047
                try {
1048
                        $activeSignings = $this->fileMapper->findByStatus(FileStatus::SIGNING_IN_PROGRESS->value);
×
1049

1050
                        $result = [];
×
1051
                        foreach ($activeSignings as $file) {
×
1052
                                $result[] = [
×
1053
                                        'id' => $file->getId(),
×
1054
                                        'uuid' => $file->getUuid(),
×
1055
                                        'name' => $file->getName(),
×
1056
                                        'signerEmail' => $file->getSignerEmail() ?? '',
×
1057
                                        'signerDisplayName' => $file->getSignerName() ?? '',
×
1058
                                        'updatedAt' => $file->getUpdatedAt(),
×
1059
                                ];
×
1060
                        }
1061

1062
                        return new DataResponse([
×
1063
                                'data' => $result,
×
1064
                        ]);
×
1065
                } catch (\Exception $e) {
×
1066
                        return new DataResponse([
×
1067
                                'error' => $e->getMessage(),
×
1068
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
1069
                }
1070
        }
1071
}
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