From e15338a0bf7ac9edc27ebef42d40c9787fc47798 Mon Sep 17 00:00:00 2001 From: Saeed Date: Wed, 4 Feb 2026 15:36:22 +0330 Subject: [PATCH] FIX Replacing usage of getJavaClass().getSimpleName() with schema..getSimpleName() to bypass Thymleaf's sandbox security --- .../external/dbmapping/DbObjectSchema.java | 250 ++++++++++-------- .../templates/snapadmin/model/create.html | 182 +++++++++---- .../templates/snapadmin/model/list.html | 4 +- .../templates/snapadmin/model/schema.html | 201 +++++++++----- .../templates/snapadmin/model/show.html | 199 +++++++++----- 5 files changed, 537 insertions(+), 299 deletions(-) diff --git a/src/main/java/tech/ailef/snapadmin/external/dbmapping/DbObjectSchema.java b/src/main/java/tech/ailef/snapadmin/external/dbmapping/DbObjectSchema.java index a61353ec..269a891f 100644 --- a/src/main/java/tech/ailef/snapadmin/external/dbmapping/DbObjectSchema.java +++ b/src/main/java/tech/ailef/snapadmin/external/dbmapping/DbObjectSchema.java @@ -5,7 +5,6 @@ */ - package tech.ailef.snapadmin.external.dbmapping; import java.lang.reflect.InvocationTargetException; @@ -53,148 +52,170 @@ public class DbObjectSchema { */ @JsonIgnore private List fields = new ArrayList<>(); - + /** * The methods designated as computed columns in the `@Entity` class. */ @JsonIgnore private Map computedColumns = new HashMap<>(); - + /** * A JPA repository to operate on the database */ private CustomJpaRepository jpaRepository; - + private SnapAdmin snapAdmin; - + /** * The corresponding `@Entity` class that this schema describes */ @JsonIgnore private Class entityClass; - + /** * The name of this table on the database */ private String tableName; - + private List errors = new ArrayList<>(); - + /** - * Initializes this schema for the specific `@Entity` class. + * Initializes this schema for the specific `@Entity` class. * Determines the table name from the `@Table` annotation and also * which methods are `@ComputedColumn`s - * @param klass the `@Entity` class + * + * @param klass the `@Entity` class * @param snapAdmin the SnapAdmin instance */ public DbObjectSchema(Class klass, SnapAdmin snapAdmin) { this.snapAdmin = snapAdmin; this.entityClass = klass; - + Table tableAnnotation = klass.getAnnotation(Table.class); - + String tableName = Utils.camelToSnake(getJavaClass().getSimpleName()); if (tableAnnotation != null && tableAnnotation.name() != null - && !tableAnnotation.name().isBlank()) { + && !tableAnnotation.name().isBlank()) { tableName = tableAnnotation.name(); } this.tableName = tableName; - + List methods = Arrays.stream(entityClass.getMethods()) .filter(m -> m.getAnnotation(ComputedColumn.class) != null) .collect(Collectors.toList()); for (Method m : methods) { if (m.getParameterCount() > 0) throw new SnapAdminException("@ComputedColumn can only be applied on no-args methods"); - + String name = m.getAnnotation(ComputedColumn.class).name(); if (name.isBlank()) name = Utils.camelToSnake(m.getName()); - + computedColumns.put(name, m); } } - + public String getBasePackage() { return entityClass.getPackageName(); } - + /** * Returns the SnapAdmin instance + * * @return the SnapAdmin instance */ public SnapAdmin getSnapAdmin() { return snapAdmin; } - + /** * Returns the Java class for the underlying `@Entity` this schema * corresponds to - * @return the Java class for the `@Entity` this schema corresponds to + * + * @return the Java class for the `@Entity` this schema corresponds to */ @JsonIgnore public Class getJavaClass() { return entityClass; } - + /** * Returns the name of the Java class for the underlying `@Entity` this schema * corresponds to - * @return the name of the Java class for the `@Entity` this schema corresponds to + * + * @return the name of the Java class for the `@Entity` this schema corresponds + * to */ @JsonIgnore public String getClassName() { return entityClass.getName(); } - + + /** + * Returns the human readable name of the Java class for the underlying `@Entity` this schema + * corresponds to + * + * @return the human readable name of the Java class for the `@Entity` this schema corresponds + * to + */ + @JsonIgnore + public String getSimpleName() { + return entityClass.getSimpleName(); + } + /** * Returns an unmodifiable list of all the fields in the schema + * * @return an unmodifiable list of all the fields in the schema */ public List getFields() { return Collections.unmodifiableList(fields); } - + public List getErrors() { return Collections.unmodifiableList(errors); } - + /** * Get a field by its Java name, i.e. the name of the instance variable * in the `@Entity` class - * @param name name of the instance variable - * @return the DbField if found, null otherwise + * + * @param name name of the instance variable + * @return the DbField if found, null otherwise */ public DbField getFieldByJavaName(String name) { return fields.stream().filter(f -> f.getJavaName().equals(name)).findFirst().orElse(null); } - + /** * Get a field by its database name, i.e. the name of the column corresponding * to the field - * @param name name of the column - * @return the DbField if found, null otherwise + * + * @param name name of the column + * @return the DbField if found, null otherwise */ public DbField getFieldByName(String name) { return fields.stream().filter(f -> f.getName().equals(name)).findFirst().orElse(null); } - + /** * Adds a field to this schema. This is used by the SnapAdmin instance * during initialization and it's not supposed to be called afterwards - * @param f the DbField to add + * + * @param f the DbField to add */ public void addField(DbField f) { fields.add(f); } - + public void addError(MappingError error) { errors.add(error); } - + /** * Returns the underlying CustomJpaRepository + * * @return */ public CustomJpaRepository getJpaRepository() { @@ -203,32 +224,35 @@ public CustomJpaRepository getJpaRepository() { /** * Sets the underlying CustomJpaRepository + * * @param jpaRepository */ public void setJpaRepository(CustomJpaRepository jpaRepository) { this.jpaRepository = jpaRepository; } - + /** - * Returns the inferred table name for this schema + * Returns the inferred table name for this schema + * * @return */ public String getTableName() { return tableName; } - + /** - * See {@link DbObjectSchema#getSortedFields()} + * See {@link DbObjectSchema#getSortedFields()} + * * @return */ @JsonIgnore public List getSortedFields() { return getSortedFields(true); } - + /** * Returns a sorted list of physical fields (i.e., fields that correspond to - * a column in the table as opposed to fields that are just present as + * a column in the table as opposed to fields that are just present as * instance variables, like relationship fields). Sorted alphabetically * with priority the primary key, and non nullable fields. * @@ -237,54 +261,55 @@ public List getSortedFields() { * hidden columns are included if they are not nullable. * * @param readOnly whether we only need to read the fields are create/edit - * @return + * @return */ public List getSortedFields(boolean readOnly) { return getFields().stream() - .filter(f -> { - boolean toMany = f.getPrimitiveField().getAnnotation(OneToMany.class) == null - && f.getPrimitiveField().getAnnotation(ManyToMany.class) == null; - - OneToOne oneToOne = f.getPrimitiveField().getAnnotation(OneToOne.class); - boolean mappedBy = oneToOne != null && !oneToOne.mappedBy().isBlank(); - - boolean hidden = f.getPrimitiveField().getAnnotation(HiddenColumn.class) != null; - - - return toMany && !mappedBy && (!hidden || !readOnly); - }) - .sorted((a, b) -> { - if (a.isPrimaryKey() && !b.isPrimaryKey()) - return -1; - if (b.isPrimaryKey() && !a.isPrimaryKey()) - return 1; - - if (!a.isNullable() && b.isNullable()) - return -1; - if (a.isNullable() && !b.isNullable()) - return 1; - - return a.getName().compareTo(b.getName()); - }).collect(Collectors.toList()); - } - + .filter(f -> { + boolean toMany = f.getPrimitiveField().getAnnotation(OneToMany.class) == null + && f.getPrimitiveField().getAnnotation(ManyToMany.class) == null; + + OneToOne oneToOne = f.getPrimitiveField().getAnnotation(OneToOne.class); + boolean mappedBy = oneToOne != null && !oneToOne.mappedBy().isBlank(); + + boolean hidden = f.getPrimitiveField().getAnnotation(HiddenColumn.class) != null; + + return toMany && !mappedBy && (!hidden || !readOnly); + }) + .sorted((a, b) -> { + if (a.isPrimaryKey() && !b.isPrimaryKey()) + return -1; + if (b.isPrimaryKey() && !a.isPrimaryKey()) + return 1; + + if (!a.isNullable() && b.isNullable()) + return -1; + if (a.isNullable() && !b.isNullable()) + return 1; + + return a.getName().compareTo(b.getName()); + }).collect(Collectors.toList()); + } + /** * Returns the list of relationship fields + * * @return */ public List getRelationshipFields() { List res = getFields().stream().filter(f -> { return f.getPrimitiveField().getAnnotation(OneToMany.class) != null - || f.getPrimitiveField().getAnnotation(ManyToMany.class) != null; + || f.getPrimitiveField().getAnnotation(ManyToMany.class) != null; }) - .filter(f -> snapAdmin.isManagedClass(f.getConnectedType())) - .collect(Collectors.toList()); + .filter(f -> snapAdmin.isManagedClass(f.getConnectedType())) + .collect(Collectors.toList()); return res; } - + /** * Returns the list of ManyToMany fields owned by this class (i.e. they * do not have "mappedBy") + * * @return */ public List getManyToManyOwnedFields() { @@ -294,9 +319,10 @@ public List getManyToManyOwnedFields() { }).collect(Collectors.toList()); return res; } - + /** * Returns the DbField which serves as the primary key for this schema + * * @return */ @JsonIgnore @@ -305,54 +331,59 @@ public DbField getPrimaryKey() { if (pk.isPresent()) return pk.get(); else - throw new RuntimeException("No primary key defined on " + entityClass.getName() + " (table `" + tableName + "`)"); + throw new RuntimeException( + "No primary key defined on " + entityClass.getName() + " (table `" + tableName + "`)"); } - + /** * Returns the names of the `@ComputedColumn`s in this schema + * * @return */ public List getComputedColumnNames() { return computedColumns.keySet().stream().sorted().toList(); } - + /** * Returns the method for the given `@ComputedColumn` name + * * @param name the name of the `@ComputedColumn` * @return the corresponding instance method if found, null otherwise */ public Method getComputedColumn(String name) { return computedColumns.get(name); } - + /** * Returns the list of fields that are `@Filterable` - * @return + * + * @return */ public List getFilterableFields() { - return getSortedFields().stream().filter(f -> { + return getSortedFields().stream().filter(f -> { return !f.isBinary() && !f.isPrimaryKey() && f.isFilterable(); }).toList(); } - + public boolean isDeleteEnabled() { return entityClass.getAnnotation(DisableDelete.class) == null; } - + public boolean isEditEnabled() { return entityClass.getAnnotation(DisableEdit.class) == null; } - + public boolean isCreateEnabled() { return entityClass.getAnnotation(DisableCreate.class) == null; } - + public boolean isExportEnabled() { return entityClass.getAnnotation(DisableExport.class) == null; } - + /** * Returns all the data in this schema, as `DbObject`s + * * @return */ public List findAll() { @@ -364,62 +395,63 @@ public DbObject buildObject(Map params, Map