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

LibreSign / libresign / 20041537490

08 Dec 2025 08:17PM UTC coverage: 44.146%. First build
20041537490

Pull #6021

github

web-flow
Merge 9088d4fab into eb7183cb5
Pull Request #6021: feat: docmdp implementation

66 of 113 new or added lines in 10 files covered. (58.41%)

5678 of 12862 relevant lines covered (44.15%)

5.1 hits per line

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

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

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

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

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

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

170
                return $this->certificateEngineFactory->getEngine();
×
171
        }
172

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

195
                return new DataResponse($certificate);
1✔
196
        }
197

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

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

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

235
                return new DataResponse();
×
236
        }
237

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

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

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

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

334
                return new DataResponse(
×
335
                        [
×
336
                                'status' => 'success',
×
337
                        ]
×
338
                );
×
339
        }
340

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

732
                $authType = $tsa_auth_type ?? 'none';
1✔
733
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
734

735
                if ($authType === 'basic') {
1✔
736
                        $hasUsername = !empty($tsa_username);
1✔
737
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
738

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

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

768
                return new DataResponse(['status' => 'success']);
1✔
769
        }
770

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

785
                foreach ($fields as $field) {
2✔
786
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
787
                }
788

789
                return new DataResponse(['status' => 'success']);
2✔
790
        }
791

792
        /**
793
         * Get footer template
794
         *
795
         * Returns the current footer template if set, otherwise returns the default template.
796
         *
797
         * @return DataResponse<Http::STATUS_OK, array{template: string, isDefault: bool, preview_width: int, preview_height: int}, array{}>
798
         *
799
         * 200: OK
800
         */
801
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/footer-template', requirements: ['apiVersion' => '(v1)'])]
802
        public function getFooterTemplate(): DataResponse {
803
                return new DataResponse([
×
804
                        'template' => $this->footerService->getTemplate(),
×
805
                        'isDefault' => $this->footerService->isDefaultTemplate(),
×
806
                        'preview_width' => $this->appConfig->getValueInt(Application::APP_ID, 'footer_preview_width', 595),
×
807
                        'preview_height' => $this->appConfig->getValueInt(Application::APP_ID, 'footer_preview_height', 100),
×
808
                ]);
×
809
        }
810

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

830
                        return new DataDownloadResponse($pdf, 'footer-preview.pdf', 'application/pdf');
×
831
                } catch (\Exception $e) {
×
832
                        return new DataResponse([
×
833
                                'error' => $e->getMessage(),
×
834
                        ], Http::STATUS_BAD_REQUEST);
×
835
                }
836
        }
837

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

864
        /**
865
         * Set DocMDP configuration
866
         *
867
         * @param bool $enabled Enable or disable DocMDP certification
868
         * @param int $defaultLevel Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations
869
         * @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
870
         *
871
         * 200: Configuration saved successfully
872
         * 400: Invalid DocMDP level provided
873
         * 500: Internal server error
874
         */
875
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/docmdp/config', requirements: ['apiVersion' => '(v1)'])]
876
        public function setDocMdpConfig(bool $enabled, int $defaultLevel): DataResponse {
877
                try {
NEW
878
                        $this->docMdpConfigService->setEnabled($enabled);
×
879

NEW
880
                        if ($enabled) {
×
NEW
881
                                $level = DocMdpLevel::tryFrom($defaultLevel);
×
NEW
882
                                if ($level === null) {
×
NEW
883
                                        return new DataResponse([
×
NEW
884
                                                'error' => $this->l10n->t('Invalid DocMDP level'),
×
NEW
885
                                        ], Http::STATUS_BAD_REQUEST);
×
886
                                }
887

NEW
888
                                $this->docMdpConfigService->setLevel($level);
×
889
                        }
890

NEW
891
                        return new DataResponse([
×
NEW
892
                                'message' => $this->l10n->t('Settings saved'),
×
NEW
893
                        ]);
×
NEW
894
                } catch (\Exception $e) {
×
NEW
895
                        return new DataResponse([
×
NEW
896
                                'error' => $e->getMessage(),
×
NEW
897
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
898
                }
899
        }
900
}
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