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

LibreSign / libresign / 19072335922

04 Nov 2025 02:39PM UTC coverage: 39.625%. First build
19072335922

Pull #5731

github

web-flow
Merge 904f89e48 into e77ceb5b6
Pull Request #5731: feat: add multiple ou

34 of 97 new or added lines in 9 files covered. (35.05%)

4570 of 11533 relevant lines covered (39.63%)

2.95 hits per line

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

16.41
/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) {
×
NEW
148
                                if (is_array($item['value'])) {
×
NEW
149
                                        $trimmedValues = array_map('trim', $item['value']);
×
NEW
150
                                        $names[$item['id']]['value'] = array_filter($trimmedValues, fn ($val) => $val !== '');
×
151
                                } else {
NEW
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']),
×
159
                        $names,
×
160
                        $properties,
×
161
                );
×
162

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

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

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

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

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

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

228
                return new DataResponse();
×
229
        }
230

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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