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

LibreSign / libresign / 19941205528

04 Dec 2025 07:22PM UTC coverage: 41.587%. First build
19941205528

Pull #6000

github

web-flow
Merge e06db9341 into de01f7f95
Pull Request #6000: feat: footer service empty string handling

3 of 5 new or added lines in 2 files covered. (60.0%)

5190 of 12480 relevant lines covered (41.59%)

4.18 hits per line

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

15.54
/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\FooterService;
21
use OCA\Libresign\Service\Install\ConfigureCheckService;
22
use OCA\Libresign\Service\Install\InstallService;
23
use OCA\Libresign\Service\ReminderService;
24
use OCA\Libresign\Service\SignatureBackgroundService;
25
use OCA\Libresign\Service\SignatureTextService;
26
use OCA\Libresign\Settings\Admin;
27
use OCP\AppFramework\Http;
28
use OCP\AppFramework\Http\Attribute\ApiRoute;
29
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
30
use OCP\AppFramework\Http\ContentSecurityPolicy;
31
use OCP\AppFramework\Http\DataDownloadResponse;
32
use OCP\AppFramework\Http\DataResponse;
33
use OCP\AppFramework\Http\FileDisplayResponse;
34
use OCP\Files\SimpleFS\InMemoryFile;
35
use OCP\IAppConfig;
36
use OCP\IEventSource;
37
use OCP\IEventSourceFactory;
38
use OCP\IL10N;
39
use OCP\IRequest;
40
use OCP\ISession;
41
use UnexpectedValueException;
42

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

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

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

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

167
                return $this->certificateEngineFactory->getEngine();
×
168
        }
169

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

192
                return new DataResponse($certificate);
1✔
193
        }
194

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

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

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

232
                return new DataResponse();
×
233
        }
234

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

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

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

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

331
                return new DataResponse(
×
332
                        [
×
333
                                'status' => 'success',
×
334
                        ]
×
335
                );
×
336
        }
337

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

729
                $authType = $tsa_auth_type ?? 'none';
1✔
730
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
731

732
                if ($authType === 'basic') {
1✔
733
                        $hasUsername = !empty($tsa_username);
1✔
734
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
735

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

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

765
                return new DataResponse(['status' => 'success']);
1✔
766
        }
767

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

782
                foreach ($fields as $field) {
2✔
783
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
784
                }
785

786
                return new DataResponse(['status' => 'success']);
2✔
787
        }
788

789
        /**
790
         * Get footer template
791
         *
792
         * Returns the current footer template if set, otherwise returns the default template.
793
         *
794
         * @return DataResponse<Http::STATUS_OK, array{template: string, isDefault: bool}, array{}>
795
         *
796
         * 200: OK
797
         */
798
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/footer-template', requirements: ['apiVersion' => '(v1)'])]
799
        public function getFooterTemplate(): DataResponse {
800
                return new DataResponse([
×
801
                        'template' => $this->footerService->getTemplate(),
×
802
                        'isDefault' => $this->footerService->isDefaultTemplate(),
×
803
                ]);
×
804
        }
805

806
        /**
807
         * Save footer template and render preview
808
         *
809
         * Saves the footer template and returns the rendered PDF preview.
810
         *
811
         * @param string $template The Twig template to save (empty to reset to default)
812
         * @param int $width Width of preview in points (default: 595 - A4 width)
813
         * @param int $height Height of preview in points (default: 50)
814
         * @return DataDownloadResponse<Http::STATUS_OK, 'application/pdf', array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
815
         *
816
         * 200: OK
817
         * 400: Bad request
818
         */
819
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/footer-template', requirements: ['apiVersion' => '(v1)'])]
820
        public function saveFooterTemplate(string $template = '', int $width = 595, int $height = 50) {
821
                try {
822
                        $this->footerService->saveTemplate($template);
×
NEW
823
                        $pdf = $this->footerService->renderPreviewPdf('', $width, $height);
×
824

825
                        return new DataDownloadResponse($pdf, 'footer-preview.pdf', 'application/pdf');
×
826
                } catch (\Exception $e) {
×
827
                        return new DataResponse([
×
828
                                'error' => $e->getMessage(),
×
829
                        ], Http::STATUS_BAD_REQUEST);
×
830
                }
831
        }
832

833
        /**
834
         * Preview footer template as PDF
835
         *
836
         * @NoAdminRequired
837
         * @NoCSRFRequired
838
         *
839
         * @param string $template Template to preview
840
         * @param int $width Width of preview in points (default: 595 - A4 width)
841
         * @param int $height Height of preview in points (default: 50)
842
         * @return DataDownloadResponse<Http::STATUS_OK, 'application/pdf', array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
843
         *
844
         * 200: OK
845
         * 400: Bad request
846
         */
847
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/footer-template/preview-pdf', requirements: ['apiVersion' => '(v1)'])]
848
        public function footerTemplatePreviewPdf(string $template = '', int $width = 595, int $height = 50) {
849
                try {
850
                        $pdf = $this->footerService->renderPreviewPdf($template ?: null, $width, $height);
×
851
                        return new DataDownloadResponse($pdf, 'footer-preview.pdf', 'application/pdf');
×
852
                } catch (\Exception $e) {
×
853
                        return new DataResponse([
×
854
                                'error' => $e->getMessage(),
×
855
                        ], Http::STATUS_BAD_REQUEST);
×
856
                }
857
        }
858
}
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