refactor(ui): restrict external asset overrides to development mode
This commit is contained in:
+4
@@ -95,6 +95,10 @@ public class CyberCoreDesktopApplication extends Application {
|
||||
developmentMode = devMode;
|
||||
}
|
||||
|
||||
public static boolean isDevelopmentMode() {
|
||||
return developmentMode;
|
||||
}
|
||||
|
||||
public static MainAppView getMainAppView() {
|
||||
return mainAppView;
|
||||
}
|
||||
|
||||
+87
-57
@@ -39,15 +39,15 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import br.com.cybercore.signer.ui.CyberCoreDesktopApplication;
|
||||
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Gerenciador centralizado de recursos visuais (ícones e imagens).
|
||||
* Permite que o usuário customize ou substitua facilmente qualquer ícone ou imagem (SVG/PNG)
|
||||
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
|
||||
* na raiz do projeto ou no diretório ~/.cybercore/assets/.
|
||||
* Centraliza o carregamento de recursos do classpath em produção e permite
|
||||
* substituição via pasta externa 'assets/' exclusivamente em modo de desenvolvimento.
|
||||
*/
|
||||
public final class AssetManager {
|
||||
|
||||
@@ -55,9 +55,33 @@ public final class AssetManager {
|
||||
|
||||
private static final String[] RASTER_EXTENSIONS = {".png", ".jpg", ".jpeg"};
|
||||
|
||||
private static volatile Boolean externalOverridesOverride = null;
|
||||
|
||||
private AssetManager() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Define explicitamente se o carregamento de assets externos está habilitado (útil para testes).
|
||||
*/
|
||||
public static void setExternalOverridesEnabled(Boolean enabled) {
|
||||
externalOverridesOverride = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indica se o carregamento de assets externos (./assets ou ~/.cybercore/assets) é permitido.
|
||||
* Em ambiente de produção, este método retorna false por padrão para garantir
|
||||
* integridade e isolamento estrito de recursos empacotados.
|
||||
*/
|
||||
public static boolean isExternalOverridesAllowed() {
|
||||
if (externalOverridesOverride != null) {
|
||||
return externalOverridesOverride;
|
||||
}
|
||||
if (CyberCoreDesktopApplication.isDevelopmentMode()) {
|
||||
return true;
|
||||
}
|
||||
return Boolean.getBoolean("cybercore.dev") || Boolean.getBoolean("cybercore.assets.external");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o caminho do diretório externo prioritário de assets.
|
||||
*/
|
||||
@@ -82,31 +106,33 @@ public final class AssetManager {
|
||||
|
||||
String cleanName = cleanBaseName(baseName);
|
||||
|
||||
// 1. Procura no diretório local ./assets/{category}/
|
||||
Path localDir = Path.of("assets", category);
|
||||
for (String ext : RASTER_EXTENSIONS) {
|
||||
Path file = localDir.resolve(cleanName + ext);
|
||||
if (Files.isRegularFile(file)) {
|
||||
try (InputStream in = new FileInputStream(file.toFile())) {
|
||||
logger.debug("Carregando imagem bitmap do diretório local: {}", file);
|
||||
return Optional.of(new Image(in, width, height, true, true));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem customizada {}: {}", file, e.getMessage());
|
||||
if (isExternalOverridesAllowed()) {
|
||||
// 1. Procura no diretório local ./assets/{category}/
|
||||
Path localDir = Path.of("assets", category);
|
||||
for (String ext : RASTER_EXTENSIONS) {
|
||||
Path file = localDir.resolve(cleanName + ext);
|
||||
if (Files.isRegularFile(file)) {
|
||||
try (InputStream in = new FileInputStream(file.toFile())) {
|
||||
logger.debug("Carregando imagem bitmap do diretório local: {}", file);
|
||||
return Optional.of(new Image(in, width, height, true, true));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem customizada {}: {}", file, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Procura em ~/.cybercore/assets/{category}/
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
Path userDir = Path.of(userHome, ".cybercore", "assets", category);
|
||||
for (String ext : RASTER_EXTENSIONS) {
|
||||
Path file = userDir.resolve(cleanName + ext);
|
||||
if (Files.isRegularFile(file)) {
|
||||
try (InputStream in = new FileInputStream(file.toFile())) {
|
||||
logger.debug("Carregando imagem bitmap de ~/.cybercore: {}", file);
|
||||
return Optional.of(new Image(in, width, height, true, true));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem do usuário {}: {}", file, e.getMessage());
|
||||
// 2. Procura em ~/.cybercore/assets/{category}/
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
Path userDir = Path.of(userHome, ".cybercore", "assets", category);
|
||||
for (String ext : RASTER_EXTENSIONS) {
|
||||
Path file = userDir.resolve(cleanName + ext);
|
||||
if (Files.isRegularFile(file)) {
|
||||
try (InputStream in = new FileInputStream(file.toFile())) {
|
||||
logger.debug("Carregando imagem bitmap de ~/.cybercore: {}", file);
|
||||
return Optional.of(new Image(in, width, height, true, true));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem do usuário {}: {}", file, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,17 +167,19 @@ public final class AssetManager {
|
||||
if (iconName != null && !iconName.isBlank()) {
|
||||
String cleanName = cleanBaseName(iconName);
|
||||
|
||||
// 1. Prioridade para SVG em ./assets/icons/
|
||||
Optional<Node> localSvg = loadSvgIconFromPath(Path.of("assets", "icons", cleanName + ".svg"), size, tint);
|
||||
if (localSvg.isPresent()) {
|
||||
return localSvg.get();
|
||||
}
|
||||
if (isExternalOverridesAllowed()) {
|
||||
// 1. Prioridade para SVG em ./assets/icons/
|
||||
Optional<Node> localSvg = loadSvgIconFromPath(Path.of("assets", "icons", cleanName + ".svg"), size, tint);
|
||||
if (localSvg.isPresent()) {
|
||||
return localSvg.get();
|
||||
}
|
||||
|
||||
// 2. Procura SVG em ~/.cybercore/assets/icons/
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
Optional<Node> userSvg = loadSvgIconFromPath(Path.of(userHome, ".cybercore", "assets", "icons", cleanName + ".svg"), size, tint);
|
||||
if (userSvg.isPresent()) {
|
||||
return userSvg.get();
|
||||
// 2. Procura SVG em ~/.cybercore/assets/icons/
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
Optional<Node> userSvg = loadSvgIconFromPath(Path.of(userHome, ".cybercore", "assets", "icons", cleanName + ".svg"), size, tint);
|
||||
if (userSvg.isPresent()) {
|
||||
return userSvg.get();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Procura SVG no Classpath interno
|
||||
@@ -188,32 +216,34 @@ public final class AssetManager {
|
||||
if (imageName != null && !imageName.isBlank()) {
|
||||
String cleanName = cleanBaseName(imageName);
|
||||
|
||||
// 1. Prioridade para SVG em ./assets/images/
|
||||
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
|
||||
if (Files.isRegularFile(localSvg)) {
|
||||
try (InputStream in = new FileInputStream(localSvg.toFile())) {
|
||||
Node node = parseSvgToNode(in, width, height, null);
|
||||
if (node != null) {
|
||||
logger.debug("Carregada imagem SVG local: {}", localSvg);
|
||||
return node;
|
||||
if (isExternalOverridesAllowed()) {
|
||||
// 1. Prioridade para SVG em ./assets/images/
|
||||
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
|
||||
if (Files.isRegularFile(localSvg)) {
|
||||
try (InputStream in = new FileInputStream(localSvg.toFile())) {
|
||||
Node node = parseSvgToNode(in, width, height, null);
|
||||
if (node != null) {
|
||||
logger.debug("Carregada imagem SVG local: {}", localSvg);
|
||||
return node;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Procura SVG em ~/.cybercore/assets/images/
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
|
||||
if (Files.isRegularFile(userSvg)) {
|
||||
try (InputStream in = new FileInputStream(userSvg.toFile())) {
|
||||
Node node = parseSvgToNode(in, width, height, null);
|
||||
if (node != null) {
|
||||
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
|
||||
return node;
|
||||
// 2. Procura SVG em ~/.cybercore/assets/images/
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
|
||||
if (Files.isRegularFile(userSvg)) {
|
||||
try (InputStream in = new FileInputStream(userSvg.toFile())) {
|
||||
Node node = parseSvgToNode(in, width, height, null);
|
||||
if (node != null) {
|
||||
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
|
||||
return node;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-14
@@ -82,20 +82,26 @@ class AssetManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve interpretar atributos style inline no padrão de exportação do Inkscape/Figma")
|
||||
void shouldParseSvgWithInlineStyles() {
|
||||
String svgWithStyle = """
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<rect x="10" y="10" width="28" height="28" style="fill:#d32f2f;stroke:#0288d1;stroke-width:2.5;opacity:0.9"/>
|
||||
<text x="24" y="24" style="fill:#ffffff;font-size:10px">OK</text>
|
||||
</svg>
|
||||
""";
|
||||
try (var in = new java.io.ByteArrayInputStream(svgWithStyle.getBytes(java.nio.charset.StandardCharsets.UTF_8))) {
|
||||
Node node = AssetManager.parseSvgToNode(in, 48, 48);
|
||||
assertThat(node).isNotNull();
|
||||
assertThat(node).isInstanceOf(javafx.scene.layout.Pane.class);
|
||||
} catch (Exception e) {
|
||||
org.junit.jupiter.api.Assertions.fail("Falha ao analisar SVG com estilo inline: " + e.getMessage());
|
||||
@DisplayName("Deve restringir overrides externos ao modo de desenvolvimento e isolar recursos em produção")
|
||||
void shouldIsolateAssetsInProductionMode() {
|
||||
try {
|
||||
// Em produção (overrides desabilitados):
|
||||
AssetManager.setExternalOverridesEnabled(false);
|
||||
assertThat(AssetManager.isExternalOverridesAllowed()).isFalse();
|
||||
|
||||
// Ícones devem carregar do classpath mesmo sem diretório externo
|
||||
Node certIcon = AssetManager.getIconOrDefault("cert", 24, null);
|
||||
assertThat(certIcon).isNotNull();
|
||||
|
||||
Node logoNode = AssetManager.getImageOrDefault("app-logo", 200, 60, null);
|
||||
assertThat(logoNode).isNotNull();
|
||||
|
||||
// Em modo desenvolvimento explicitamente habilitado:
|
||||
AssetManager.setExternalOverridesEnabled(true);
|
||||
assertThat(AssetManager.isExternalOverridesAllowed()).isTrue();
|
||||
|
||||
} finally {
|
||||
AssetManager.setExternalOverridesEnabled(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user