Feature/save signs (#2127)

* apply fix

* Fixes empty th:action

* Update build.gradle

* fix

* formatting

* Save signatures

* Fix code scanning alert no. 42: Uncontrolled data used in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix UserServiceInterface

* Merge branch 'feature/saveSigns' of
git@github.com:Stirling-Tools/Stirling-PDF.git into feature/saveSigns

* 0.31.0 bump and further csrf

* formatting

* preview name

* add

* sign doc

* Update translation files (#2128)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: Dimitrios Kaitantzidis <james_k23@hotmail.gr>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: a <a>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Anthony Stirling 2024-10-30 12:46:44 +00:00 committed by GitHub
parent ed75fa4e1b
commit 27d2681a97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 741 additions and 44 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ bin/
tmp/
*.tmp
*.bak
*.exe
*.swp
*~.nib
local.properties

View File

@ -166,6 +166,13 @@ Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "pod
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
## Reuse stored files
Certain functionality like ``Sign`` Supports pre-saved files stored at ``/customFiles/signatures/``, image files placed within here will be accesable to be used via webUI
Currently this supports two folder types
- ``/customFiles/signatures/ALL_USERS`` accessible to all users, useful for orginasations were many users use same files or for users not using authentication
- ``/customFiles/signatures/{username}`` such as ``/customFiles/signatures/froodle`` accessible to only the ``froodle`` username, private for all others
## Supported Languages
Stirling PDF currently supports 38!

View File

@ -22,7 +22,7 @@ ext {
}
group = "stirling.software"
version = "0.30.1"
version = "0.31.0"
java {
// 17 is lowest but we support and recommend 21

View File

@ -104,7 +104,32 @@ public class SecurityConfiguration {
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.csrfTokenRepository(cookieRepo)
csrf.ignoringRequestMatchers(
request -> {
String apiKey = request.getHeader("X-API-Key");
// If there's no API key, don't ignore CSRF
// (return false)
if (apiKey == null || apiKey.trim().isEmpty()) {
return false;
}
// Validate API key using existing UserService
try {
Optional<User> user =
userService.getUserByApiKey(apiKey);
// If API key is valid, ignore CSRF (return
// true)
// If API key is invalid, don't ignore CSRF
// (return false)
return user.isPresent();
} catch (Exception e) {
// If there's any error validating the API
// key, don't ignore CSRF
return false;
}
})
.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);

View File

@ -96,17 +96,18 @@ public class CertSignController {
public CreateSignature(KeyStore keystore, char[] pin)
throws KeyStoreException,
UnrecoverableKeyException,
NoSuchAlgorithmException,
IOException,
CertificateException {
UnrecoverableKeyException,
NoSuchAlgorithmException,
IOException,
CertificateException {
super(keystore, pin);
ClassPathResource resource = new ClassPathResource("static/images/signature.png");
imageFile = resource.getFile();
}
public InputStream createVisibleSignature(PDDocument srcDoc, PDSignature signature, Integer pageNumber,
Boolean showImage) throws IOException {
public InputStream createVisibleSignature(
PDDocument srcDoc, PDSignature signature, Integer pageNumber, Boolean showImage)
throws IOException {
// modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(srcDoc.getPage(pageNumber).getMediaBox());
@ -151,8 +152,8 @@ public class CertSignController {
extState.setNonStrokingAlphaConstant(0.5f);
cs.setGraphicsStateParameters(extState);
cs.transform(Matrix.getScaleInstance(0.08f, 0.08f));
PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile,
doc);
PDImageXObject img =
PDImageXObject.createFromFileByExtension(imageFile, doc);
cs.drawImage(img, 100, 0);
cs.restoreGraphicsState();
}
@ -200,7 +201,10 @@ public class CertSignController {
}
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
@Operation(
summary = "Sign PDF with a Digital Certificate",
description =
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
MultipartFile pdf = request.getFileInput();
@ -229,7 +233,7 @@ public class CertSignController {
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
ks.setKeyEntry(
"alias", privateKey, password.toCharArray(), new Certificate[] { cert });
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
break;
case "PKCS12":
ks = KeyStore.getInstance("PKCS12");
@ -245,7 +249,15 @@ public class CertSignController {
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, showSignature, pageNumber, name, location,
sign(
pdfDocumentFactory,
pdf.getBytes(),
baos,
createSignature,
showSignature,
pageNumber,
name,
location,
reason);
return WebResponseUtils.boasToWebResponse(
baos,
@ -274,8 +286,8 @@ public class CertSignController {
if (showSignature) {
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions
.setVisualSignature(instance.createVisibleSignature(doc, signature, pageNumber, true));
signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, true));
signatureOptions.setPage(pageNumber);
doc.addSignature(signature, instance, signatureOptions);
@ -291,19 +303,22 @@ public class CertSignController {
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
throws IOException, OperatorCreationException, PKCSException {
try (PEMParser pemParser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
try (PEMParser pemParser =
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
Object pemObject = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKeyInfo pkInfo;
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder()
.build(password.toCharArray());
InputDecryptorProvider decProv =
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
} else if (pemObject instanceof PEMEncryptedKeyPair) {
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
pkInfo = ((PEMEncryptedKeyPair) pemObject)
.decryptKeyPair(decProv)
.getPrivateKeyInfo();
PEMDecryptorProvider decProv =
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
pkInfo =
((PEMEncryptedKeyPair) pemObject)
.decryptKeyPair(decProv)
.getPrivateKeyInfo();
} else {
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
}

View File

@ -31,6 +31,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.SignatureFile;
import stirling.software.SPDF.service.SignatureService;
@Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController {
@ -171,11 +175,28 @@ public class GeneralWebController {
return "split-pdfs";
}
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
@Autowired private SignatureService signatureService;
@Autowired(required = false)
private UserServiceInterface userService;
@GetMapping("/sign")
@Hidden
public String signForm(Model model) {
String username = "";
if (userService != null) {
username = userService.getCurrentUsername();
}
// Get signatures from both personal and ALL_USERS folders
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames());
model.addAttribute("signatures", signatures);
return "sign";
}

View File

@ -0,0 +1,44 @@
package stirling.software.SPDF.controller.web;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.service.SignatureService;
@Controller
@RequestMapping("/api/v1/general/")
public class SignatureController {
@Autowired private SignatureService signatureService;
@Autowired(required = false)
private UserServiceInterface userService;
@GetMapping("/sign/{fileName}")
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName)
throws IOException {
String username = "NON_SECURITY_USER";
if (userService != null) {
username = userService.getCurrentUsername();
}
// Verify access permission
if (!signatureService.hasAccessToFile(username, fileName)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
byte[] imageBytes = signatureService.getSignatureBytes(username, fileName);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG) // Adjust based on file type
.body(imageBytes);
}
}

View File

@ -0,0 +1,11 @@
package stirling.software.SPDF.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class SignatureFile {
private String fileName;
private String category; // "Personal" or "Shared"
}

View File

@ -0,0 +1,100 @@
package stirling.software.SPDF.service;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.SignatureFile;
@Service
@Slf4j
public class SignatureService {
private static final String SIGNATURE_BASE_PATH = "customFiles/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
public boolean hasAccessToFile(String username, String fileName) throws IOException {
validateFileName(fileName);
// Check if file exists in user's personal folder or ALL_USERS folder
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
return Files.exists(userPath) || Files.exists(allUsersPath);
}
public List<SignatureFile> getAvailableSignatures(String username) {
List<SignatureFile> signatures = new ArrayList<>();
// Get signatures from user's personal folder
if (!StringUtils.isEmptyOrWhitespace(username)) {
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
if (Files.exists(userFolder)) {
try {
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
} catch (IOException e) {
log.error("Error reading user signatures folder", e);
}
}
}
// Get signatures from ALL_USERS folder
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
if (Files.exists(allUsersFolder)) {
try {
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
} catch (IOException e) {
log.error("Error reading shared signatures folder", e);
}
}
return signatures;
}
private List<SignatureFile> getSignaturesFromFolder(Path folder, String category)
throws IOException {
return Files.list(folder)
.filter(path -> isImageFile(path))
.map(path -> new SignatureFile(path.getFileName().toString(), category))
.collect(Collectors.toList());
}
public byte[] getSignatureBytes(String username, String fileName) throws IOException {
validateFileName(fileName);
// First try user's personal folder
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
if (Files.exists(userPath)) {
return Files.readAllBytes(userPath);
}
// Then try ALL_USERS folder
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
if (Files.exists(allUsersPath)) {
return Files.readAllBytes(allUsersPath);
}
throw new FileNotFoundException("Signature file not found");
}
private boolean isImageFile(Path path) {
String fileName = path.getFileName().toString().toLowerCase();
return fileName.endsWith(".jpg")
|| fileName.endsWith(".jpeg")
|| fileName.endsWith(".png")
|| fileName.endsWith(".gif");
}
private void validateFileName(String fileName) {
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
throw new IllegalArgumentException("Invalid filename");
}
}
}

View File

@ -30,17 +30,19 @@ public class ImageProcessingUtils {
BufferedImage convertedImage;
switch (colorType) {
case "greyscale":
convertedImage = new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
BufferedImage.TYPE_BYTE_GRAY);
convertedImage =
new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
BufferedImage.TYPE_BYTE_GRAY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
case "blackwhite":
convertedImage = new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
BufferedImage.TYPE_BYTE_BINARY);
convertedImage =
new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
BufferedImage.TYPE_BYTE_BINARY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
default: // full color
@ -79,7 +81,8 @@ public class ImageProcessingUtils {
public static double extractImageOrientation(InputStream is) throws IOException {
try {
Metadata metadata = ImageMetadataReader.readMetadata(is);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
ExifSubIFDDirectory directory =
metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory == null) {
return 0;
}
@ -106,10 +109,11 @@ public class ImageProcessingUtils {
if (orientation == 0) {
return image;
}
AffineTransform transform = AffineTransform.getRotateInstance(
Math.toRadians(orientation),
image.getWidth() / 2.0,
image.getHeight() / 2.0);
AffineTransform transform =
AffineTransform.getRotateInstance(
Math.toRadians(orientation),
image.getWidth() / 2.0,
image.getHeight() / 2.0);
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
return op.filter(image, null);
}

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=رسم التوقيع
sign.text=إدخال النص
sign.clear=مسح
sign.add=إضافة
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Страница
pages=Страници
loading=Loading...
addToDoc=Add to Document
legal.privacy=Политика за поверителност
legal.terms=Правила и условия
@ -808,6 +809,11 @@ sign.draw=Начертайте подпис
sign.text=Въвеждане на текст
sign.clear=Изчисти
sign.add=Добави
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Dibuixa la signatura
sign.text=Entrada de text
sign.clear=Esborrar
sign.add=Afegeix
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Nakreslit podpis
sign.text=Vstup textu
sign.clear=Vymazat
sign.add=Přidat
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Tegn Underskrift
sign.text=Tekstinput
sign.clear=Ryd
sign.add=Tilføj
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Datenschutz
legal.terms=AGB
@ -808,6 +809,11 @@ sign.draw=Signatur zeichnen
sign.text=Texteingabe
sign.clear=Leeren
sign.add=Signieren
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Σχεδίαση υπογραφής
sign.text=Εισαγωγή κειμένου
sign.clear=Καθάρισμα
sign.add=Προσθήκη
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Draw Signature
sign.text=Text Input
sign.clear=Clear
sign.add=Add
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Draw Signature
sign.text=Text Input
sign.clear=Clear
sign.add=Add
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Página
pages=Páginas
loading=Loading...
addToDoc=Add to Document
legal.privacy=Política de Privacidad
legal.terms=Términos y Condiciones
@ -808,6 +809,11 @@ sign.draw=Dibujar firma
sign.text=Entrada de texto
sign.clear=Borrar
sign.add=Agregar
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Marraztu sinadura
sign.text=Testua sartzea
sign.clear=Garbitu
sign.add=Gehitu
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Politique de Confidentialité
legal.terms=Conditions Générales
@ -808,6 +809,11 @@ sign.draw=Dessiner une signature
sign.text=Saisir de texte
sign.clear=Effacer
sign.add=Ajouter
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Tarraing Síniú
sign.text=Ionchur Téacs
sign.clear=Glan
sign.add=Cuir
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=पृष्ठ
pages=पृष्ठों
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=हस्ताक्षर बनाएँ
sign.text=पाठ इनपुट
sign.clear=साफ़ करें
sign.add=जोड़ें
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Nacrtaj potpis
sign.text=Tekstualni unos
sign.clear=Obriši
sign.add=Dodaj
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Aláírás rajzolása
sign.text=Szöveg beírása
sign.clear=Törlés
sign.add=Hozzáadás
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Halaman
pages=Halaman-halaman
loading=Loading...
addToDoc=Add to Document
legal.privacy=Kebijakan Privasi
legal.terms=Syarat dan Ketentuan
@ -808,6 +809,11 @@ sign.draw=Gambar Tanda Tangan
sign.text=Masukan Teks
sign.clear=Hapus
sign.add=Tambah
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Pagina
pages=Pagine
loading=Loading...
addToDoc=Add to Document
legal.privacy=Informativa sulla privacy
legal.terms=Termini e Condizioni
@ -808,6 +809,11 @@ sign.draw=Disegna Firma
sign.text=Testo
sign.clear=Cancella
sign.add=Aggiungi
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=プライバシーポリシー
legal.terms=利用規約
@ -808,6 +809,11 @@ sign.draw=署名を書く
sign.text=テキスト入力
sign.clear=クリア
sign.add=追加
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=서명 그리기
sign.text=텍스트 입력
sign.clear=초기화
sign.add=추가
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Handtekening tekenen
sign.text=Tekstinvoer
sign.clear=Wissen
sign.add=Toevoegen
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Tegn signatur
sign.text=Tekstinput
sign.clear=Slett
sign.add=Legg til
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Strona
pages=Strony
loading=Loading...
addToDoc=Add to Document
legal.privacy=Polityka Prywatności
legal.terms=Zasady i Postanowienia
@ -808,6 +809,11 @@ sign.draw=Narysuj podpis
sign.text=Wprowadź tekst
sign.clear=Wyczyść
sign.add=Dodaj
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Página
pages=Páginas
loading=Loading...
addToDoc=Add to Document
legal.privacy=Política de Privacidade
legal.terms=Termos e Condições
@ -808,6 +809,11 @@ sign.draw=Desenhar Assinatura
sign.text=Inserir texto
sign.clear=Limpar
sign.add=Adicionar
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Desenhar Assinatura
sign.text=Inserir Texto
sign.clear=Limpar
sign.add=Adicionar
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Desenează Semnătura
sign.text=Introdu Textul
sign.clear=Curăță
sign.add=Adaugă
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Нарисовать подпись
sign.text=Ввод текста
sign.clear=Очистить
sign.add=Добавить
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Kresliť podpis
sign.text=Textový vstup
sign.clear=Vymazať
sign.add=Pridať
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Nacrtaj potpis
sign.text=Tekstualni unos
sign.clear=Obriši
sign.add=Dodaj
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Rita signatur
sign.text=Textinmatning
sign.clear=Rensa
sign.add=Lägg till
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=วาดลายเซ็น
sign.text=ป้อนข้อความ
sign.clear=ล้าง
sign.add=เพิ่ม
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Gizlilik Politikası
legal.terms=Şartlar ve koşullar
@ -808,6 +809,11 @@ sign.draw=İmza Çiz
sign.text=Metin Girişi
sign.clear=Temizle
sign.add=Ekle
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Намалювати підпис
sign.text=Ввід тексту
sign.clear=Очистити
sign.add=Додати
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=Vẽ chữ ký
sign.text=Nhập văn bản
sign.clear=Xóa
sign.add=Thêm
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -808,6 +809,11 @@ sign.draw=绘制签名
sign.text=文本输入
sign.clear=清除
sign.add=添加
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -80,6 +80,7 @@ pro=專業版
page=頁面
pages=頁面
loading=Loading...
addToDoc=Add to Document
legal.privacy=隱私權政策
legal.terms=使用條款
@ -808,6 +809,11 @@ sign.draw=繪製簽章
sign.text=文字輸入
sign.clear=清除
sign.add=新增
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
#repair

View File

@ -62,3 +62,53 @@ select#font-select option {
background-color: rgba(52, 152, 219, 0.2);
/* Darken background on hover */
}
.signature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
padding: 1rem;
max-height: 400px;
overflow-y: auto;
}
.signature-list {
max-height: 400px;
overflow-y: auto;
}
.signature-list-item {
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 4px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: background-color 0.2s;
}
.signature-list-item:hover {
background-color: #f8f9fa;
}
.signature-list-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.signature-list-name {
font-weight: 500;
}
.signature-list-details {
color: #6c757d;
font-size: 0.875rem;
}
.signature-list-details small:not(:last-child) {
margin-right: 1rem;
}
.view-toggle {
text-align: right;
padding: 0.5rem 1rem;
}

View File

@ -0,0 +1,29 @@
window.fetchWithCsrf = async function(url, options = {}) {
function getCsrfToken() {
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (cookieValue) {
return cookieValue;
}
const csrfElement = document.querySelector('input[name="_csrf"]');
return csrfElement ? csrfElement.value : null;
}
// Create a new options object to avoid modifying the passed object
const fetchOptions = { ...options };
// Ensure headers object exists
fetchOptions.headers = { ...options.headers };
// Add CSRF token if available
const csrfToken = getCsrfToken();
if (csrfToken) {
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
}
return fetch(url, fetchOptions);
}

View File

@ -119,7 +119,7 @@ document.getElementById("submitConfigBtn").addEventListener("click", function ()
formData.append("json", pipelineConfigJson);
console.log("formData", formData);
fetch("api/v1/pipeline/handleData", {
fetchWithCsrf("api/v1/pipeline/handleData", {
method: "POST",
body: formData,
})
@ -154,7 +154,7 @@ let apiDocs = {};
let apiSchemas = {};
let operationSettings = {};
fetch("v1/api-docs")
fetchWithCsrf("v1/api-docs")
.then((response) => response.json())
.then((data) => {
apiDocs = data.paths;

View File

@ -384,7 +384,7 @@
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
/*<![CDATA[*/
@ -398,7 +398,7 @@
});
/*]]>*/
function setAnalytics(enabled) {
fetch('api/v1/settings/update-enable-analytics', {
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@ -20,7 +20,7 @@
</div>
<!-- pdf selector -->
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let originalFileName = '';
@ -46,7 +46,7 @@
</script>
<div class="tab-group show-on-file-selected">
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
<script>
const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', e => {

View File

@ -20,6 +20,7 @@
<span class="tool-header-text" th:text="#{compress.header}"></span>
</div>
<form action="#" th:action="@{'/api/v1/misc/compress-pdf'}" method="post" enctype="multipart/form-data">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>

View File

@ -39,6 +39,7 @@
<!-- Button to download the JSON -->
<a href="#" id="downloadJS" class="btn btn-primary mt-3" style="display: none;" th:text="#{showJS.downloadJS}">Download JSON</a>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script>
document.querySelector('#pdfInfoForm').addEventListener('submit', function(event){
event.preventDefault();
@ -46,7 +47,7 @@
// Fetch the formData
const formData = new FormData(event.target);
fetch('api/v1/misc/show-javascript', {
fetchWithCsrf('api/v1/misc/show-javascript', {
method: 'POST',
body: formData
}).then(response => response.text())

View File

@ -192,6 +192,7 @@
</div>
</div>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:src="@{'/js/pipeline.js'}"></script>\
</div>
</div>

View File

@ -31,12 +31,16 @@
<a href="#" id="downloadJson" class="btn btn-primary mt-3" style="display: none;" th:text="#{getPdfInfo.downloadJson}">Download JSON</a>
</div>
<script>
import { fetchWithCsrf } from 'js/fetch-utils.js';
document.getElementById("pdfInfoForm").addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(event.target);
fetch('api/v1/security/get-info-on-pdf', {
fetchWithCsrf('api/v1/security/get-info-on-pdf', {
method: 'POST',
body: formData
}).then(response => response.json()).then(data => {

View File

@ -43,6 +43,53 @@
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let currentPreviewSrc = null;
function toggleSignatureView() {
const gridView = document.getElementById('gridView');
const listView = document.getElementById('listView');
const gridText = document.querySelector('.grid-view-text');
const listText = document.querySelector('.list-view-text');
if (gridView.style.display !== 'none') {
gridView.style.display = 'none';
listView.style.display = 'block';
gridText.style.display = 'none';
listText.style.display = 'inline';
} else {
gridView.style.display = 'block';
listView.style.display = 'none';
gridText.style.display = 'inline';
listText.style.display = 'none';
}
}
function previewSignature(element) {
const src = element.dataset.src;
currentPreviewSrc = src;
// Extract filename from the data source path
const filename = element.querySelector('.signature-list-name').textContent;
// Update preview modal
const previewImage = document.getElementById('previewImage');
const previewFileName = document.getElementById('previewFileName');
previewImage.src = src;
previewFileName.textContent = filename;
const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
modal.show();
}
function addSignatureFromPreview() {
if (currentPreviewSrc) {
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
}
}
let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const file = event.target.files[0];
@ -68,7 +115,7 @@
<div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{sign.upload}">
<div
th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div>
<script>
const imageUpload = document.querySelector('input[name=image-upload]');
@ -165,6 +212,126 @@
</script>
</div>
<div class="tab-container" th:title="#{sign.saved}">
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
<!-- View Toggle Button -->
<div class="view-toggle mb-3">
<button class="btn btn-outline-secondary btn-sm" onclick="toggleSignatureView()">
<span class="material-symbols-rounded grid-view-text">view_list</span>
<span class="material-symbols-rounded list-view-text" style="display: none;">grid_view</span>
</button>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="signaturePreview" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><span id="previewFileName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<img id="previewImage" src="" alt="Signature Preview" style="max-width: 100%;">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
<button type="button" class="btn btn-primary" onclick="addSignatureFromPreview()" th:text="#{addToDoc}">Add to Document</button>
</div>
</div>
</div>
</div>
<!-- Grid View -->
<div id="gridView">
<!-- Personal Signatures -->
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
<h5 th:text="#{sign.personalSigs}"></h5>
<div class="signature-grid">
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}" class="signature-item">
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
th:alt="${sig.fileName}"
th:data-filename="${sig.fileName}"
style="max-width: 200px; cursor: pointer;"
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)"/>
<div class="signature-name" th:text="${sig.fileName}"></div>
</div>
</div>
</div>
<!-- Shared Signatures -->
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
<h5 th:text="#{sign.sharedSigs}"></h5>
<div class="signature-grid">
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}" class="signature-item">
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
th:alt="${sig.fileName}"
th:data-filename="${sig.fileName}"
style="max-width: 200px; cursor: pointer;"
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)"/>
<div class="signature-name" th:text="${sig.fileName}"></div>
</div>
</div>
</div>
</div>
<!-- List View (Initially Hidden) -->
<div id="listView" style="display: none;">
<!-- Personal Signatures -->
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
<h5 th:text="#{sign.personalSigs}"></h5>
<div class="signature-list">
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
class="signature-list-item"
th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
onclick="previewSignature(this)">
<div class="signature-list-info">
<span th:text="${sig.fileName}" class="signature-list-name"></span>
</div>
</div>
</div>
</div>
<!-- Shared Signatures -->
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
<h5 th:text="#{sign.sharedSigs}"></h5>
<div class="signature-list">
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
class="signature-list-item"
th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
onclick="previewSignature(this)">
<div class="signature-list-info">
<span th:text="${sig.fileName}" class="signature-list-name"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
</div>
</div>
<div class="tab-container" th:title="#{sign.text}">
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>