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

LibreSign / libresign / 19118950120

05 Nov 2025 10:58PM UTC coverage: 39.847%. First build
19118950120

Pull #5754

github

web-flow
Merge adfafab73 into 681cce019
Pull Request #5754: feat: preserve previous root cert

86 of 120 new or added lines in 10 files covered. (71.67%)

4633 of 11627 relevant lines covered (39.85%)

3.06 hits per line

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

16.36
/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\Exception\LibresignException;
14
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
15
use OCA\Libresign\Handler\CertificateEngine\IEngineHandler;
16
use OCA\Libresign\Helper\ConfigureCheckHelper;
17
use OCA\Libresign\ResponseDefinitions;
18
use OCA\Libresign\Service\Certificate\ValidateService;
19
use OCA\Libresign\Service\CertificatePolicyService;
20
use OCA\Libresign\Service\Install\ConfigureCheckService;
21
use OCA\Libresign\Service\Install\InstallService;
22
use OCA\Libresign\Service\ReminderService;
23
use OCA\Libresign\Service\SignatureBackgroundService;
24
use OCA\Libresign\Service\SignatureTextService;
25
use OCA\Libresign\Settings\Admin;
26
use OCP\AppFramework\Http;
27
use OCP\AppFramework\Http\Attribute\ApiRoute;
28
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
29
use OCP\AppFramework\Http\ContentSecurityPolicy;
30
use OCP\AppFramework\Http\DataResponse;
31
use OCP\AppFramework\Http\FileDisplayResponse;
32
use OCP\Files\SimpleFS\InMemoryFile;
33
use OCP\IAppConfig;
34
use OCP\IEventSource;
35
use OCP\IEventSourceFactory;
36
use OCP\IL10N;
37
use OCP\IRequest;
38
use OCP\ISession;
39
use UnexpectedValueException;
40

41
/**
42
 * @psalm-import-type LibresignEngineHandler from ResponseDefinitions
43
 * @psalm-import-type LibresignCetificateDataGenerated from ResponseDefinitions
44
 * @psalm-import-type LibresignConfigureCheck from ResponseDefinitions
45
 * @psalm-import-type LibresignRootCertificate from ResponseDefinitions
46
 * @psalm-import-type LibresignReminderSettings from ResponseDefinitions
47
 */
48
class AdminController extends AEnvironmentAwareController {
49
        private IEventSource $eventSource;
50
        public function __construct(
51
                IRequest $request,
52
                private IAppConfig $appConfig,
53
                private ConfigureCheckService $configureCheckService,
54
                private InstallService $installService,
55
                private CertificateEngineFactory $certificateEngineFactory,
56
                private IEventSourceFactory $eventSourceFactory,
57
                private SignatureTextService $signatureTextService,
58
                private IL10N $l10n,
59
                protected ISession $session,
60
                private SignatureBackgroundService $signatureBackgroundService,
61
                private CertificatePolicyService $certificatePolicyService,
62
                private ValidateService $validateService,
63
                private ReminderService $reminderService,
64
        ) {
65
                parent::__construct(Application::APP_ID, $request);
5✔
66
                $this->eventSource = $this->eventSourceFactory->create();
5✔
67
        }
68

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

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

140
        private function generateCertificate(
141
                array $rootCert,
142
                array $properties = [],
143
        ): IEngineHandler {
144
                $names = [];
1✔
145
                if (isset($rootCert['names'])) {
1✔
146
                        $this->validateService->validateNames($rootCert['names']);
1✔
147
                        foreach ($rootCert['names'] as $item) {
×
148
                                if (is_array($item['value'])) {
×
149
                                        $trimmedValues = array_map('trim', $item['value']);
×
150
                                        $names[$item['id']]['value'] = array_filter($trimmedValues, fn ($val) => $val !== '');
×
151
                                } else {
152
                                        $names[$item['id']]['value'] = trim((string)$item['value']);
×
153
                                }
154
                        }
155
                }
156
                $this->validateService->validate('CN', $rootCert['commonName']);
×
157
                $this->installService->generate(
×
158
                        trim((string)$rootCert['commonName']),
×
NEW
159
                        $properties['engine'],
×
160
                        $names,
×
161
                        $properties,
×
162
                );
×
163

164
                return $this->certificateEngineFactory->getEngine();
×
165
        }
166

167
        /**
168
         * Load certificate data
169
         *
170
         * Return all data of root certificate and a field called `generated` with a boolean value.
171
         *
172
         * @return DataResponse<Http::STATUS_OK, LibresignCetificateDataGenerated, array{}>
173
         *
174
         * 200: OK
175
         */
176
        #[NoCSRFRequired]
177
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/certificate', requirements: ['apiVersion' => '(v1)'])]
178
        public function loadCertificate(): DataResponse {
179
                $engine = $this->certificateEngineFactory->getEngine();
1✔
180
                /** @var LibresignEngineHandler */
181
                $certificate = $engine->toArray();
1✔
182
                $configureResult = $engine->configureCheck();
1✔
183
                $success = array_filter(
1✔
184
                        $configureResult,
1✔
185
                        fn (ConfigureCheckHelper $config) => $config->getStatus() === 'success'
1✔
186
                );
1✔
187
                $certificate['generated'] = count($success) === count($configureResult);
1✔
188

189
                return new DataResponse($certificate);
1✔
190
        }
191

192
        /**
193
         * Check the configuration of LibreSign
194
         *
195
         * Return the status of necessary configuration and tips to fix the problems.
196
         *
197
         * @return DataResponse<Http::STATUS_OK, LibresignConfigureCheck[], array{}>
198
         *
199
         * 200: OK
200
         */
201
        #[NoCSRFRequired]
202
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/configure-check', requirements: ['apiVersion' => '(v1)'])]
203
        public function configureCheck(): DataResponse {
204
                /** @var LibresignConfigureCheck[] */
205
                $configureCheckList = $this->configureCheckService->checkAll();
×
206
                return new DataResponse(
×
207
                        $configureCheckList
×
208
                );
×
209
        }
210

211
        /**
212
         * Disable hate limit to current session
213
         *
214
         * This will disable hate limit to current session.
215
         *
216
         * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
217
         *
218
         * 200: OK
219
         */
220
        #[NoCSRFRequired]
221
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/disable-hate-limit', requirements: ['apiVersion' => '(v1)'])]
222
        public function disableHateLimit(): DataResponse {
223
                $this->session->set('app_api', true);
×
224

225
                // TODO: Remove after drop support NC29
226
                // deprecated since AppAPI 2.8.0
227
                $this->session->set('app_api_system', true);
×
228

229
                return new DataResponse();
×
230
        }
231

232
        /**
233
         * @IgnoreOpenAPI
234
         */
235
        #[NoCSRFRequired]
236
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/install-and-validate', requirements: ['apiVersion' => '(v1)'])]
237
        public function installAndValidate(): void {
238
                try {
239
                        $async = \function_exists('proc_open');
×
240
                        $this->installService->installJava($async);
×
241
                        $this->installService->installJSignPdf($async);
×
242
                        $this->installService->installPdftk($async);
×
243
                        if ($this->appConfig->getValueString(Application::APP_ID, 'certificate_engine') === 'cfssl') {
×
244
                                $this->installService->installCfssl($async);
×
245
                        }
246

247
                        $this->configureCheckService->disableCache();
×
248
                        $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
249
                        $seconds = 0;
×
250
                        while ($this->installService->isDownloadWip()) {
×
251
                                $totalSize = $this->installService->getTotalSize();
×
252
                                $this->eventSource->send('total_size', json_encode($totalSize));
×
253
                                if ($errors = $this->installService->getErrorMessages()) {
×
254
                                        $this->eventSource->send('errors', json_encode($errors));
×
255
                                }
256
                                usleep(200000); // 0.2 seconds
×
257
                                $seconds += 0.2;
×
258
                                if ($seconds === 5.0) {
×
259
                                        $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
260
                                        $seconds = 0;
×
261
                                }
262
                        }
263
                        if ($errors = $this->installService->getErrorMessages()) {
×
264
                                $this->eventSource->send('errors', json_encode($errors));
×
265
                        }
266
                } catch (\Exception $exception) {
×
267
                        $this->eventSource->send('errors', json_encode([
×
268
                                $this->l10n->t('Could not download binaries.'),
×
269
                                $exception->getMessage(),
×
270
                        ]));
×
271
                }
272

273
                $this->eventSource->send('configure_check', $this->configureCheckService->checkAll());
×
274
                $this->eventSource->send('done', '');
×
275
                $this->eventSource->close();
×
276
                // Nextcloud inject a lot of headers that is incompatible with SSE
277
                exit();
×
278
        }
279

280
        /**
281
         * Add custom background image
282
         *
283
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{status: 'failure', message: string}, array{}>
284
         *
285
         * 200: OK
286
         * 422: Error
287
         */
288
        #[NoCSRFRequired]
289
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
290
        public function signatureBackgroundSave(): DataResponse {
291
                $image = $this->request->getUploadedFile('image');
×
292
                $phpFileUploadErrors = [
×
293
                        UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
×
294
                        UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
×
295
                        UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
×
296
                        UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
×
297
                        UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
×
298
                        UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
×
299
                        UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
×
300
                        UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
×
301
                ];
×
302
                if (empty($image)) {
×
303
                        $error = $this->l10n->t('No file uploaded');
×
304
                } elseif (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
×
305
                        $error = $phpFileUploadErrors[$image['error']];
×
306
                }
307
                if ($error !== null) {
×
308
                        return new DataResponse(
×
309
                                [
×
310
                                        'message' => $error,
×
311
                                        'status' => 'failure',
×
312
                                ],
×
313
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
314
                        );
×
315
                }
316
                try {
317
                        $this->signatureBackgroundService->updateImage($image['tmp_name']);
×
318
                } catch (\Exception $e) {
×
319
                        return new DataResponse(
×
320
                                [
×
321
                                        'message' => $e->getMessage(),
×
322
                                        'status' => 'failure',
×
323
                                ],
×
324
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
325
                        );
×
326
                }
327

328
                return new DataResponse(
×
329
                        [
×
330
                                'status' => 'success',
×
331
                        ]
×
332
                );
×
333
        }
334

335
        /**
336
         * Get custom background image
337
         *
338
         * @return FileDisplayResponse<Http::STATUS_OK, array{}>
339
         *
340
         * 200: Image returned
341
         */
342
        #[NoCSRFRequired]
343
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
344
        public function signatureBackgroundGet(): FileDisplayResponse {
345
                $file = $this->signatureBackgroundService->getImage();
×
346

347
                $response = new FileDisplayResponse($file);
×
348
                $csp = new ContentSecurityPolicy();
×
349
                $csp->allowInlineStyle();
×
350
                $response->setContentSecurityPolicy($csp);
×
351
                $response->cacheFor(3600);
×
352
                $response->addHeader('Content-Type', 'image/png');
×
353
                $response->addHeader('Content-Disposition', 'attachment; filename="background.png"');
×
354
                $response->addHeader('Content-Type', 'image/png');
×
355
                return $response;
×
356
        }
357

358
        /**
359
         * Reset the background image to be the default of LibreSign
360
         *
361
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
362
         *
363
         * 200: Image reseted to default
364
         */
365
        #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
366
        public function signatureBackgroundReset(): DataResponse {
367
                $this->signatureBackgroundService->reset();
×
368
                return new DataResponse(
×
369
                        [
×
370
                                'status' => 'success',
×
371
                        ]
×
372
                );
×
373
        }
374

375
        /**
376
         * Delete background image
377
         *
378
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
379
         *
380
         * 200: Deleted with success
381
         */
382
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/signature-background', requirements: ['apiVersion' => '(v1)'])]
383
        public function signatureBackgroundDelete(): DataResponse {
384
                $this->signatureBackgroundService->delete();
×
385
                return new DataResponse(
×
386
                        [
×
387
                                'status' => 'success',
×
388
                        ]
×
389
                );
×
390
        }
391

392
        /**
393
         * Save signature text service
394
         *
395
         * @param string $template Template to signature text
396
         * @param float $templateFontSize Font size used when print the parsed text of this template at PDF file
397
         * @param float $signatureFontSize Font size used when the signature mode is SIGNAME_AND_DESCRIPTION
398
         * @param float $signatureWidth Signature width
399
         * @param float $signatureHeight Signature height
400
         * @param string $renderMode Signature render mode
401
         * @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{}>
402
         *
403
         * 200: OK
404
         * 400: Bad request
405
         */
406
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-text', requirements: ['apiVersion' => '(v1)'])]
407
        public function signatureTextSave(
408
                string $template,
409
                /** @todo openapi package don't evaluate SignatureTextService::TEMPLATE_DEFAULT_FONT_SIZE */
410
                float $templateFontSize = 10,
411
                /** @todo openapi package don't evaluate SignatureTextService::SIGNATURE_DEFAULT_FONT_SIZE */
412
                float $signatureFontSize = 20,
413
                /** @todo openapi package don't evaluate SignatureTextService::DEFAULT_SIGNATURE_WIDTH */
414
                float $signatureWidth = 350,
415
                /** @todo openapi package don't evaluate SignatureTextService::DEFAULT_SIGNATURE_HEIGHT */
416
                float $signatureHeight = 100,
417
                string $renderMode = 'GRAPHIC_AND_DESCRIPTION',
418
        ): DataResponse {
419
                try {
420
                        $return = $this->signatureTextService->save(
×
421
                                $template,
×
422
                                $templateFontSize,
×
423
                                $signatureFontSize,
×
424
                                $signatureWidth,
×
425
                                $signatureHeight,
×
426
                                $renderMode,
×
427
                        );
×
428
                        return new DataResponse(
×
429
                                $return,
×
430
                                Http::STATUS_OK
×
431
                        );
×
432
                } catch (LibresignException $th) {
×
433
                        return new DataResponse(
×
434
                                [
×
435
                                        'error' => $th->getMessage(),
×
436
                                ],
×
437
                                Http::STATUS_BAD_REQUEST
×
438
                        );
×
439
                }
440
        }
441

442
        /**
443
         * Get parsed signature text service
444
         *
445
         * @param string $template Template to signature text
446
         * @param string $context Context for parsing the template
447
         * @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{}>
448
         *
449
         * 200: OK
450
         * 400: Bad request
451
         */
452
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-text', requirements: ['apiVersion' => '(v1)'])]
453
        public function signatureTextGet(string $template = '', string $context = ''): DataResponse {
454
                $context = json_decode($context, true) ?? [];
×
455
                try {
456
                        $return = $this->signatureTextService->parse($template, $context);
×
457
                        return new DataResponse(
×
458
                                $return,
×
459
                                Http::STATUS_OK
×
460
                        );
×
461
                } catch (LibresignException $th) {
×
462
                        return new DataResponse(
×
463
                                [
×
464
                                        'error' => $th->getMessage(),
×
465
                                ],
×
466
                                Http::STATUS_BAD_REQUEST
×
467
                        );
×
468
                }
469
        }
470

471
        /**
472
         * Get signature settings
473
         *
474
         * @return DataResponse<Http::STATUS_OK, array{default_signature_text_template: string, signature_available_variables: array<string, string>}, array{}>
475
         *
476
         * 200: OK
477
         */
478
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signature-settings', requirements: ['apiVersion' => '(v1)'])]
479
        public function getSignatureSettings(): DataResponse {
480
                $response = [
×
481
                        'signature_available_variables' => $this->signatureTextService->getAvailableVariables(),
×
482
                        'default_signature_text_template' => $this->signatureTextService->getDefaultTemplate(),
×
483
                ];
×
484
                return new DataResponse($response);
×
485
        }
486

487
        /**
488
         * Convert signer name as image
489
         *
490
         * @param int $width Image width,
491
         * @param int $height Image height
492
         * @param string $text Text to be added to image
493
         * @param float $fontSize Font size of text
494
         * @param bool $isDarkTheme Color of text, white if is tark theme and black if not
495
         * @param string $align Align of text: left, center or right
496
         * @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{}>
497
         *
498
         * 200: OK
499
         * 400: Bad request
500
         */
501
        #[NoCSRFRequired]
502
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/signer-name', requirements: ['apiVersion' => '(v1)'])]
503
        public function signerName(
504
                int $width,
505
                int $height,
506
                string $text,
507
                float $fontSize,
508
                bool $isDarkTheme,
509
                string $align,
510
        ):  FileDisplayResponse|DataResponse {
511
                try {
512
                        $blob = $this->signatureTextService->signerNameImage(
×
513
                                width: $width,
×
514
                                height: $height,
×
515
                                text: $text,
×
516
                                fontSize: $fontSize,
×
517
                                isDarkTheme: $isDarkTheme,
×
518
                                align: $align,
×
519
                        );
×
520
                        $file = new InMemoryFile('signer-name.png', $blob);
×
521
                        return new FileDisplayResponse($file, Http::STATUS_OK, [
×
522
                                'Content-Disposition' => 'inline; filename="signer-name.png"',
×
523
                                'Content-Type' => 'image/png',
×
524
                        ]);
×
525
                } catch (LibresignException $th) {
×
526
                        return new DataResponse(
×
527
                                [
×
528
                                        'error' => $th->getMessage(),
×
529
                                ],
×
530
                                Http::STATUS_BAD_REQUEST
×
531
                        );
×
532
                }
533
        }
534

535
        /**
536
         * Update certificate policy of this instance
537
         *
538
         * @return DataResponse<Http::STATUS_OK, array{status: 'success', CPS: string}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{status: 'failure', message: string}, array{}>
539
         *
540
         * 200: OK
541
         * 422: Not found
542
         */
543
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
544
        public function saveCertificatePolicy(): DataResponse {
545
                $pdf = $this->request->getUploadedFile('pdf');
×
546
                $phpFileUploadErrors = [
×
547
                        UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
×
548
                        UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
×
549
                        UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
×
550
                        UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
×
551
                        UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
×
552
                        UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
×
553
                        UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
×
554
                        UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
×
555
                ];
×
556
                if (empty($pdf)) {
×
557
                        $error = $this->l10n->t('No file uploaded');
×
558
                } elseif (!empty($pdf) && array_key_exists('error', $pdf) && $pdf['error'] !== UPLOAD_ERR_OK) {
×
559
                        $error = $phpFileUploadErrors[$pdf['error']];
×
560
                }
561
                if ($error !== null) {
×
562
                        return new DataResponse(
×
563
                                [
×
564
                                        'message' => $error,
×
565
                                        'status' => 'failure',
×
566
                                ],
×
567
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
568
                        );
×
569
                }
570
                try {
571
                        $cps = $this->certificatePolicyService->updateFile($pdf['tmp_name']);
×
572
                } catch (UnexpectedValueException $e) {
×
573
                        return new DataResponse(
×
574
                                [
×
575
                                        'message' => $e->getMessage(),
×
576
                                        'status' => 'failure',
×
577
                                ],
×
578
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
579
                        );
×
580
                }
581
                return new DataResponse(
×
582
                        [
×
583
                                'CPS' => $cps,
×
584
                                'status' => 'success',
×
585
                        ]
×
586
                );
×
587
        }
588

589
        /**
590
         * Delete certificate policy of this instance
591
         *
592
         * @return DataResponse<Http::STATUS_OK, array{}, array{}>
593
         *
594
         * 200: OK
595
         * 404: Not found
596
         */
597
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/certificate-policy', requirements: ['apiVersion' => '(v1)'])]
598
        public function deleteCertificatePolicy(): DataResponse {
599
                $this->certificatePolicyService->deleteFile();
×
600
                return new DataResponse();
×
601
        }
602

603
        /**
604
         * Update OID
605
         *
606
         * @param string $oid OID is a unique numeric identifier for certificate policies in digital certificates.
607
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, array{status: 'failure', message: string}, array{}>
608
         *
609
         * 200: OK
610
         * 422: Validation error
611
         */
612
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/certificate-policy/oid', requirements: ['apiVersion' => '(v1)'])]
613
        public function updateOID(string $oid): DataResponse {
614
                try {
615
                        $this->certificatePolicyService->updateOid($oid);
×
616
                        return new DataResponse(
×
617
                                [
×
618
                                        'status' => 'success',
×
619
                                ]
×
620
                        );
×
621
                } catch (\Exception $e) {
×
622
                        return new DataResponse(
×
623
                                [
×
624
                                        'message' => $e->getMessage(),
×
625
                                        'status' => 'failure',
×
626
                                ],
×
627
                                Http::STATUS_UNPROCESSABLE_ENTITY
×
628
                        );
×
629
                }
630
        }
631

632
        /**
633
         * Get reminder settings
634
         *
635
         * @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
636
         *
637
         * 200: OK
638
         */
639
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
640
        public function reminderFetch(): DataResponse {
641
                $response = $this->reminderService->getSettings();
×
642
                if ($response['next_run'] instanceof \DateTime) {
×
643
                        $response['next_run'] = $response['next_run']->format(DateTimeInterface::ATOM);
×
644
                }
645
                return new DataResponse($response);
×
646
        }
647

648
        /**
649
         * Save reminder
650
         *
651
         * @param int $daysBefore First reminder after (days)
652
         * @param int $daysBetween Days between reminders
653
         * @param int $max Max reminders per signer
654
         * @param string $sendTimer Send time (HH:mm)
655
         * @return DataResponse<Http::STATUS_OK, LibresignReminderSettings, array{}>
656
         *
657
         * 200: OK
658
         */
659
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/reminder', requirements: ['apiVersion' => '(v1)'])]
660
        public function reminderSave(
661
                int $daysBefore,
662
                int $daysBetween,
663
                int $max,
664
                string $sendTimer,
665
        ): DataResponse {
666
                $response = $this->reminderService->save($daysBefore, $daysBetween, $max, $sendTimer);
×
667
                if ($response['next_run'] instanceof \DateTime) {
×
668
                        $response['next_run'] = $response['next_run']->format(DateTimeInterface::ATOM);
×
669
                }
670
                return new DataResponse($response);
×
671
        }
672

673
        /**
674
         * Set TSA configuration values with proper sensitive data handling
675
         *
676
         * Only saves configuration if tsa_url is provided. Automatically manages
677
         * username/password fields based on authentication type.
678
         *
679
         * @param string|null $tsa_url TSA server URL (required for saving)
680
         * @param string|null $tsa_policy_oid TSA policy OID
681
         * @param string|null $tsa_auth_type Authentication type (none|basic), defaults to 'none'
682
         * @param string|null $tsa_username Username for basic authentication
683
         * @param string|null $tsa_password Password for basic authentication (stored as sensitive data)
684
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{status: 'error', message: string}, array{}>
685
         *
686
         * 200: OK
687
         * 400: Validation error
688
         */
689
        #[NoCSRFRequired]
690
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
691
        public function setTsaConfig(
692
                ?string $tsa_url = null,
693
                ?string $tsa_policy_oid = null,
694
                ?string $tsa_auth_type = null,
695
                ?string $tsa_username = null,
696
                ?string $tsa_password = null,
697
        ): DataResponse {
698
                if (empty($tsa_url)) {
2✔
699
                        return $this->deleteTsaConfig();
1✔
700
                }
701

702
                $trimmedUrl = trim($tsa_url);
1✔
703
                if (!filter_var($trimmedUrl, FILTER_VALIDATE_URL)
1✔
704
                        || !in_array(parse_url($trimmedUrl, PHP_URL_SCHEME), ['http', 'https'])) {
1✔
705
                        return new DataResponse([
×
706
                                'status' => 'error',
×
707
                                'message' => 'Invalid URL format'
×
708
                        ], Http::STATUS_BAD_REQUEST);
×
709
                }
710

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

713
                if (empty($tsa_policy_oid)) {
1✔
714
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_policy_oid');
1✔
715
                } else {
716
                        $trimmedOid = trim($tsa_policy_oid);
×
717
                        if (!preg_match('/^[0-9]+(\.[0-9]+)*$/', $trimmedOid)) {
×
718
                                return new DataResponse([
×
719
                                        'status' => 'error',
×
720
                                        'message' => 'Invalid OID format'
×
721
                                ], Http::STATUS_BAD_REQUEST);
×
722
                        }
723
                        $this->appConfig->setValueString(Application::APP_ID, 'tsa_policy_oid', $trimmedOid);
×
724
                }
725

726
                $authType = $tsa_auth_type ?? 'none';
1✔
727
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
728

729
                if ($authType === 'basic') {
1✔
730
                        $hasUsername = !empty($tsa_username);
1✔
731
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
732

733
                        if (!$hasUsername && !$hasPassword) {
1✔
734
                                return new DataResponse([
×
735
                                        'status' => 'error',
×
736
                                        'message' => 'Username and password are required for basic authentication'
×
737
                                ], Http::STATUS_BAD_REQUEST);
×
738
                        } elseif (!$hasUsername) {
1✔
739
                                return new DataResponse([
×
740
                                        'status' => 'error',
×
741
                                        'message' => 'Username is required'
×
742
                                ], Http::STATUS_BAD_REQUEST);
×
743
                        } elseif (!$hasPassword) {
1✔
744
                                return new DataResponse([
×
745
                                        'status' => 'error',
×
746
                                        'message' => 'Password is required'
×
747
                                ], Http::STATUS_BAD_REQUEST);
×
748
                        }
749

750
                        $this->appConfig->setValueString(Application::APP_ID, 'tsa_username', trim($tsa_username));
1✔
751
                        $this->appConfig->setValueString(
1✔
752
                                Application::APP_ID,
1✔
753
                                key: 'tsa_password',
1✔
754
                                value: $tsa_password,
1✔
755
                                sensitive: true,
1✔
756
                        );
1✔
757
                } else {
758
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_username');
×
759
                        $this->appConfig->deleteKey(Application::APP_ID, 'tsa_password');
×
760
                }
761

762
                return new DataResponse(['status' => 'success']);
1✔
763
        }
764

765
        /**
766
         * Delete TSA configuration
767
         *
768
         * Delete all TSA configuration fields from the application settings.
769
         *
770
         * @return DataResponse<Http::STATUS_OK, array{status: 'success'}, array{}>
771
         *
772
         * 200: OK
773
         */
774
        #[NoCSRFRequired]
775
        #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/admin/tsa', requirements: ['apiVersion' => '(v1)'])]
776
        public function deleteTsaConfig(): DataResponse {
777
                $fields = ['tsa_url', 'tsa_policy_oid', 'tsa_auth_type', 'tsa_username', 'tsa_password'];
2✔
778

779
                foreach ($fields as $field) {
2✔
780
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
781
                }
782

783
                return new DataResponse(['status' => 'success']);
2✔
784
        }
785
}
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