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

LibreSign / libresign / 20137037473

11 Dec 2025 02:46PM UTC coverage: 43.856%. First build
20137037473

Pull #6100

github

web-flow
Merge 3a5558793 into e5d703c51
Pull Request #6100: refactor: organize enums in dedicated folder

6 of 12 new or added lines in 6 files covered. (50.0%)

5760 of 13134 relevant lines covered (43.86%)

5.12 hits per line

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

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

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

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

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

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

170
                $handler = $this->certificateEngineFactory->getEngine();
×
171
                $handler->setEngine($engine);
×
172
                $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings();
×
173

174
                return new DataResponse([
×
175
                        'engine' => $engine,
×
176
                        'identify_methods' => $identifyMethods,
×
177
                ]);
×
178
        }
179

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

204
                return $this->certificateEngineFactory->getEngine();
×
205
        }
206

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

229
                return new DataResponse($certificate);
1✔
230
        }
231

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

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

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

269
                return new DataResponse();
×
270
        }
271

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

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

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

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

368
                return new DataResponse(
×
369
                        [
×
370
                                'status' => 'success',
×
371
                        ]
×
372
                );
×
373
        }
374

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

766
                $authType = $tsa_auth_type ?? 'none';
1✔
767
                $this->appConfig->setValueString(Application::APP_ID, 'tsa_auth_type', $authType);
1✔
768

769
                if ($authType === 'basic') {
1✔
770
                        $hasUsername = !empty($tsa_username);
1✔
771
                        $hasPassword = !empty($tsa_password) && $tsa_password !== Admin::PASSWORD_PLACEHOLDER;
1✔
772

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

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

802
                return new DataResponse(['status' => 'success']);
1✔
803
        }
804

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

819
                foreach ($fields as $field) {
2✔
820
                        $this->appConfig->deleteKey(Application::APP_ID, $field);
2✔
821
                }
822

823
                return new DataResponse(['status' => 'success']);
2✔
824
        }
825

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

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

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

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

898
        /**
899
         * Set signature flow configuration
900
         *
901
         * @param string $mode Signature flow mode: 'parallel' or 'ordered_numeric'
902
         * @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{}>
903
         *
904
         * 200: Configuration saved successfully
905
         * 400: Invalid signature flow mode provided
906
         * 500: Internal server error
907
         */
908
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/signature-flow/config', requirements: ['apiVersion' => '(v1)'])]
909
        public function setSignatureFlowConfig(string $mode): DataResponse {
910
                try {
NEW
911
                        $signatureFlow = \OCA\Libresign\Enum\SignatureFlow::from($mode);
×
912
                } catch (\ValueError) {
×
913
                        return new DataResponse([
×
914
                                'error' => $this->l10n->t('Invalid signature flow mode. Use "parallel" or "ordered_numeric".'),
×
915
                        ], Http::STATUS_BAD_REQUEST);
×
916
                }
917

918
                try {
NEW
919
                        if ($signatureFlow === \OCA\Libresign\Enum\SignatureFlow::PARALLEL) {
×
920
                                $this->appConfig->deleteKey(Application::APP_ID, 'signature_flow');
×
921
                        } else {
922
                                $this->appConfig->setValueString(
×
923
                                        Application::APP_ID,
×
924
                                        'signature_flow',
×
925
                                        $signatureFlow->value
×
926
                                );
×
927
                        }
928

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

939
        /**
940
         * Set DocMDP configuration
941
         *
942
         * @param bool $enabled Enable or disable DocMDP certification
943
         * @param int $defaultLevel Default DocMDP level (0-3): 0=none, 1=no changes, 2=form fill, 3=form fill + annotations
944
         * @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{}>
945
         *
946
         * 200: Configuration saved successfully
947
         * 400: Invalid DocMDP level provided
948
         * 500: Internal server error
949
         */
950
        #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/admin/docmdp/config', requirements: ['apiVersion' => '(v1)'])]
951
        public function setDocMdpConfig(bool $enabled, int $defaultLevel): DataResponse {
952
                try {
953
                        $this->docMdpConfigService->setEnabled($enabled);
×
954

955
                        if ($enabled) {
×
956
                                $level = DocMdpLevel::tryFrom($defaultLevel);
×
957
                                if ($level === null) {
×
958
                                        return new DataResponse([
×
959
                                                'error' => $this->l10n->t('Invalid DocMDP level'),
×
960
                                        ], Http::STATUS_BAD_REQUEST);
×
961
                                }
962

963
                                $this->docMdpConfigService->setLevel($level);
×
964
                        }
965

966
                        return new DataResponse([
×
967
                                'message' => $this->l10n->t('Settings saved'),
×
968
                        ]);
×
969
                } catch (\Exception $e) {
×
970
                        return new DataResponse([
×
971
                                'error' => $e->getMessage(),
×
972
                        ], Http::STATUS_INTERNAL_SERVER_ERROR);
×
973
                }
974
        }
975
}
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