diff --git a/cms-api/src/main/java/com/condation/cms/api/Constants.java b/cms-api/src/main/java/com/condation/cms/api/Constants.java index 3ed5139e5..88da35848 100644 --- a/cms-api/src/main/java/com/condation/cms/api/Constants.java +++ b/cms-api/src/main/java/com/condation/cms/api/Constants.java @@ -78,6 +78,7 @@ public static class Folders { public static final String MODULES = "modules/"; public static final String HOSTS = "hosts/"; public static final String THEMES = "themes/"; + public static final String PUBLIC = "public/"; } public static class NodeType { diff --git a/cms-api/src/main/java/com/condation/cms/api/theme/Theme.java b/cms-api/src/main/java/com/condation/cms/api/theme/Theme.java index 8164a0013..4e42c7856 100644 --- a/cms-api/src/main/java/com/condation/cms/api/theme/Theme.java +++ b/cms-api/src/main/java/com/condation/cms/api/theme/Theme.java @@ -42,6 +42,8 @@ public interface Theme { Path extensionsPath (); Path assetsPath (); + + Path publicPath (); ThemeProperties properties(); diff --git a/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java b/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java index c48c25b71..548e8fbba 100644 --- a/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java @@ -54,32 +54,6 @@ public class ContentResolver { private final DB db; - public Optional getStaticContent (String uri) { - if (uri.endsWith(".md")) { - return Optional.empty(); - } - if (uri.startsWith("/")) { - uri = uri.substring(1); - } - var contentBase = db.getReadOnlyFileSystem().contentBase(); - ReadOnlyFile staticFile = contentBase.resolve(uri); - if (staticFile.isDirectory()) { - return Optional.empty(); - } - try { - if (staticFile.exists()) { - return Optional.ofNullable(new DefaultContentResponse( - staticFile.getContent(), - staticFile.getContentType(), - null - )); - } - } catch (IOException ex) { - log.error("", ex); - } - return Optional.empty(); - } - public Optional getContent (final RequestContext context) { return getContent(context, true); } diff --git a/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java b/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java index 899aab73c..304e50c03 100644 --- a/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java +++ b/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java @@ -129,7 +129,15 @@ public Path assetsPath() { } return themePath.resolve(Constants.Folders.ASSETS); } - + + @Override + public Path publicPath() { + if (themePath == null) { + return null; + } + return themePath.resolve(Constants.Folders.PUBLIC); + } + @Override public Path templatesPath() { if (themePath == null) { diff --git a/cms-media/src/test/java/com/condation/cms/media/TestTheme.java b/cms-media/src/test/java/com/condation/cms/media/TestTheme.java index 903989cc4..042c7ca4f 100644 --- a/cms-media/src/test/java/com/condation/cms/media/TestTheme.java +++ b/cms-media/src/test/java/com/condation/cms/media/TestTheme.java @@ -20,8 +20,6 @@ * along with this program. If not, see . * #L% */ - - import com.condation.cms.api.ThemeProperties; import com.condation.cms.api.messages.MessageSource; import com.condation.cms.api.theme.Theme; @@ -33,63 +31,68 @@ * @author t.marx */ @RequiredArgsConstructor -public class TestTheme implements Theme{ - - private final ThemeProperties properties; - - @Override - public String getName() { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Path templatesPath() { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Path extensionsPath() { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Path assetsPath() { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public ThemeProperties properties() { - return this.properties; - } - - @Override - public boolean empty() { - return false; - } - - @Override - public MessageSource getMessages() { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Theme getParentTheme() { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Path resolveExtension(String path) { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Path resolveAsset(String path) { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - - @Override - public Path resolveTemplate(String path) { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody - } - +public class TestTheme implements Theme { + + private final ThemeProperties properties; + + @Override + public String getName() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path templatesPath() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path extensionsPath() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path assetsPath() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path publicPath() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public ThemeProperties properties() { + return this.properties; + } + + @Override + public boolean empty() { + return false; + } + + @Override + public MessageSource getMessages() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Theme getParentTheme() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path resolveExtension(String path) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path resolveAsset(String path) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Path resolveTemplate(String path) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + } diff --git a/cms-server/src/main/java/com/condation/cms/server/JettyServer.java b/cms-server/src/main/java/com/condation/cms/server/JettyServer.java index 34516f3df..f007de505 100644 --- a/cms-server/src/main/java/com/condation/cms/server/JettyServer.java +++ b/cms-server/src/main/java/com/condation/cms/server/JettyServer.java @@ -111,7 +111,7 @@ public void startup() throws IOException { try { var host = new VHost(site.id(), site.basePath(), ServerUtil.getPath(Constants.Folders.MODULES), globalInjector); log.debug("warmup host {}", site.id()); - host.warmup(); + host.startUpWarmup(); log.debug("init host {}", site.id()); host.init(); vhosts.add(host); diff --git a/cms-server/src/main/java/com/condation/cms/server/annotations/Eager.java b/cms-server/src/main/java/com/condation/cms/server/annotations/Eager.java new file mode 100644 index 000000000..f4ba9dcec --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/annotations/Eager.java @@ -0,0 +1,35 @@ +package com.condation.cms.server.annotations; + +/*- + * #%L + * CMS Server + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import com.google.inject.BindingAnnotation; + +@Target({ FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +public @interface Eager {} \ No newline at end of file diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java index 2e3abf953..c88b2f0e6 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java @@ -42,6 +42,7 @@ import com.condation.cms.server.FileFolderPathResource; import com.condation.cms.server.filter.InitRequestContextFilter; import com.condation.cms.server.filter.PreviewFilter; +import com.condation.cms.server.handler.StaticFileHandler; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; import com.condation.cms.server.handler.content.JettyContentHandler; import com.condation.cms.server.handler.content.JettyTaxonomyHandler; @@ -56,6 +57,7 @@ import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.name.Named; +import java.util.List; import lombok.RequiredArgsConstructor; @@ -103,15 +105,15 @@ public JettyModuleHandler moduleHandler(Theme theme, ModuleManager moduleManager @Provides @Singleton - @Named("site") + @Named("site.media") public JettyMediaHandler mediaHandler(SiteMediaManager mediaManager) throws IOException { return new JettyMediaHandler(mediaManager); } @Provides @Singleton - @Named("site") - public ResourceHandler resourceHander (@Named("assets") Path assetBase, ServerProperties serverProperties) throws IOException { + @Named("site.assets") + public ResourceHandler assetsHandler (@Named("assets") Path assetBase, ServerProperties serverProperties) throws IOException { ResourceHandler assetsHandler = new ResourceHandler(); assetsHandler.setDirAllowed(false); assetsHandler.setBaseResource(new FileFolderPathResource(assetBase)); @@ -123,4 +125,11 @@ public ResourceHandler resourceHander (@Named("assets") Path assetBase, ServerPr return assetsHandler; } + + @Provides + @Singleton + @Named("site.public") + public StaticFileHandler publicHandler (@Named("public") Path publicBase, ServerProperties serverProperties) throws IOException { + return new StaticFileHandler(List.of(publicBase)); + } } diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java index 53102d88a..221218e16 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java @@ -36,7 +36,6 @@ import com.condation.cms.api.cache.ICache; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.ServerConfiguration; -import com.condation.cms.api.configuration.configs.SiteConfiguration; import com.condation.cms.api.content.ContentParser; import com.condation.cms.api.content.RenderContentFunction; import com.condation.cms.api.db.DB; @@ -53,7 +52,6 @@ import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.api.theme.Theme; import com.condation.cms.auth.services.AuthService; -import com.condation.cms.auth.services.UserService; import com.condation.cms.content.ContentRenderer; import com.condation.cms.content.ContentResolver; import com.condation.cms.content.DefaultContentParser; @@ -87,6 +85,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import com.condation.cms.server.annotations.Eager; /** * @@ -211,6 +210,13 @@ public Path assetsPath(DB db) { return db.getFileSystem().resolve(Constants.Folders.ASSETS); } + @Provides + @Singleton + @Named("public") + public Path publicPath(DB db) { + return db.getFileSystem().resolve(Constants.Folders.PUBLIC); + } + @Provides @Singleton @Named("templates") @@ -241,7 +247,8 @@ public MessageSource messages(SiteProperties site, DB db, CacheManager cacheMana @Provides @Singleton - public DB fileDb(SiteProperties site, DefaultContentParser contentParser, Configuration configuration, EventBus eventBus) throws IOException { + @Eager + public DB fileDb(SiteProperties site, DefaultContentParser contentParser, Configuration configuration, EventBus eventBus) throws IOException { var db = new FileDB(hostBase, eventBus, (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/ThemeModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/ThemeModule.java index 56dda1121..696e9b811 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/ThemeModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/ThemeModule.java @@ -29,11 +29,15 @@ import com.condation.cms.media.ThemeMediaManager; import com.condation.cms.server.handler.media.JettyMediaHandler; import com.condation.cms.server.FileFolderPathResource; +import com.condation.cms.server.handler.StaticFileHandler; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.name.Named; import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import org.eclipse.jetty.server.handler.ResourceHandler; @@ -46,42 +50,56 @@ @RequiredArgsConstructor public class ThemeModule extends AbstractModule { - @Provides - @Singleton - public ThemeMediaManager themeMediaManager(Theme theme, Configuration configuration, DB db, EventBus eventBus) throws IOException { - var mediaManager = new ThemeMediaManager(db.getFileSystem().resolve("temp"), theme, configuration); - eventBus.register(ConfigurationReloadEvent.class, mediaManager); - return mediaManager; - } + @Provides + @Singleton + public ThemeMediaManager themeMediaManager(Theme theme, Configuration configuration, DB db, EventBus eventBus) throws IOException { + var mediaManager = new ThemeMediaManager(db.getFileSystem().resolve("temp"), theme, configuration); + eventBus.register(ConfigurationReloadEvent.class, mediaManager); + return mediaManager; + } - @Provides - @Singleton - @Named("theme") - public JettyMediaHandler themeMediaHandler(ThemeMediaManager mediaManager) throws IOException { - return new JettyMediaHandler(mediaManager); - } + @Provides + @Singleton + @Named("theme.media") + public JettyMediaHandler themeMediaHandler(ThemeMediaManager mediaManager) throws IOException { + return new JettyMediaHandler(mediaManager); + } - @Provides - @Singleton - @Named("theme") - public ResourceHandler resourceHandler(Theme theme, ServerProperties serverProperties) { - ResourceHandler assetsHandler = new ResourceHandler(); - assetsHandler.setDirAllowed(false); + @Provides + @Singleton + @Named("theme.assets") + public ResourceHandler themeAssetsHandler(Theme theme, ServerProperties serverProperties) { + ResourceHandler assetsHandler = new ResourceHandler(); + assetsHandler.setDirAllowed(false); - if (theme.getParentTheme() != null) { - assetsHandler.setBaseResource(ResourceFactory.combine( - new FileFolderPathResource(theme.assetsPath()), - new FileFolderPathResource(theme.getParentTheme().assetsPath()) - )); - } else { - assetsHandler.setBaseResource(new FileFolderPathResource(theme.assetsPath())); - } + if (theme.getParentTheme() != null) { + assetsHandler.setBaseResource(ResourceFactory.combine( + new FileFolderPathResource(theme.assetsPath()), + new FileFolderPathResource(theme.getParentTheme().assetsPath()) + )); + } else { + assetsHandler.setBaseResource(new FileFolderPathResource(theme.assetsPath())); + } - if (serverProperties.dev()) { - assetsHandler.setCacheControl("no-cache"); - } else { - assetsHandler.setCacheControl("max-age=" + TimeUnit.HOURS.toSeconds(24)); - } - return assetsHandler; - } + if (serverProperties.dev()) { + assetsHandler.setCacheControl("no-cache"); + } else { + assetsHandler.setCacheControl("max-age=" + TimeUnit.HOURS.toSeconds(24)); + } + return assetsHandler; + } + + @Provides + @Singleton + @Named("theme.public") + public StaticFileHandler themePublicHandler(Theme theme, ServerProperties serverProperties) { + List paths = new ArrayList<>(); + paths.add(theme.publicPath()); + + if (theme.getParentTheme() != null) { + paths.add(theme.getParentTheme().publicPath()); + } + + return new StaticFileHandler(paths); + } } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/StaticFileHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/StaticFileHandler.java new file mode 100644 index 000000000..9493904f0 --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/handler/StaticFileHandler.java @@ -0,0 +1,131 @@ +package com.condation.cms.server.handler; + +/*- + * #%L + * CMS Server + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.api.utils.RequestUtil; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +@Slf4j +public class StaticFileHandler extends AbstractHandler { + + private final List bases; + + public StaticFileHandler(List bases) { + this.bases = bases; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + String relativePath = getRelativePath(request); + + for (Path base : bases) { + Path requested = base.resolve(relativePath).normalize(); + + if (!Files.exists(requested) + || !Files.isRegularFile(requested) + || !PathUtil.isChild(base, requested)) { + continue; // 👉 nächster Base-Pfad + } + + return serveFile(request, response, callback, requested); + } + + return false; // 👉 nichts gefunden → nächster Handler + } + + private boolean serveFile(Request request, Response response, Callback callback, Path requested) { + + try { + long size = Files.size(requested); + + response.setStatus(200); + response.getHeaders().put("Content-Type", guessContentType(requested)); + response.getHeaders().put("Content-Length", String.valueOf(size)); + + // HEAD support + if ("HEAD".equalsIgnoreCase(request.getMethod())) { + callback.succeeded(); + return true; + } + + var in = Files.newInputStream(requested); + + Content.copy(Content.Source.from(in), response, new Callback() { + @Override + public void succeeded() { + try { in.close(); } catch (IOException ignore) {} + callback.succeeded(); + } + + @Override + public void failed(Throwable x) { + try { in.close(); } catch (IOException ignore) {} + callback.failed(x); + } + }); + + return true; + + } catch (IOException e) { + log.error("Error serving static file {}", requested, e); + callback.failed(e); + return true; + } + } + + private String getRelativePath(Request request) { + String path = request.getHttpURI().getPath(); + String contextPath = RequestUtil.getContextPath(request); + + if (!contextPath.endsWith("/")) { + contextPath += "/"; + } + + return path.replaceFirst("^" + contextPath, ""); + } + + private String guessContentType(Path path) { + try { + String type = Files.probeContentType(path); + if (type != null) return type; + + String p = path.toString(); + if (p.endsWith(".js")) return "application/javascript"; + if (p.endsWith(".css")) return "text/css"; + if (p.endsWith(".html")) return "text/html"; + + return "application/octet-stream"; + } catch (IOException e) { + return "application/octet-stream"; + } + } +} diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java index 6b00a16a2..100fa3f49 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java @@ -33,9 +33,6 @@ import com.condation.cms.content.ContentResolver; import com.condation.cms.request.RequestContextFactory; import com.google.inject.Inject; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -52,71 +49,69 @@ * @author t.marx */ @RequiredArgsConstructor(onConstructor = @__({ - @Inject})) + @Inject})) @Slf4j public class JettyContentHandler extends Handler.Abstract { - private final ContentResolver contentResolver; - private final RequestContextFactory requestContextFactory; + private final ContentResolver contentResolver; + private final RequestContextFactory requestContextFactory; - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { // var uri = request.getHttpURI().getPath(); - var uri = RequestUtil.getContentPath(request); - var queryParameters = HTTPUtil.queryParameters(request.getHttpURI().getQuery()); - var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + var uri = RequestUtil.getContentPath(request); + var queryParameters = HTTPUtil.queryParameters(request.getHttpURI().getQuery()); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); - // handle enabled spa mode - var spaEnabled = requestContext.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties().spaEnabled(); - var notFoundContent = "/.technical/404"; - if (spaEnabled) { - uri = ""; - notFoundContent = "/"; - } + // handle enabled spa mode + var spaEnabled = requestContext.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties().spaEnabled(); + var notFoundContent = "/.technical/404"; + if (spaEnabled) { + uri = ""; + notFoundContent = "/"; + } - try { - Optional content = contentResolver.getContent(requestContext); - response.setStatus(200); + try { + Optional content = contentResolver.getContent(requestContext); + response.setStatus(200); - if (!content.isPresent()) { + if (content.isEmpty()) { + log.debug("content not found {}", uri); + try (var errorContext = requestContextFactory.create(request.getContext().getContextPath(), + notFoundContent, + queryParameters, Optional.of(request))) { + content = contentResolver.getErrorContent(errorContext); + response.setStatus(404); + } + } - // try to resolve static files - content = contentResolver.getStaticContent(uri); - if (content.isEmpty()) { - log.debug("content not found {}", uri); - try (var errorContext = requestContextFactory.create(request.getContext().getContextPath(), - notFoundContent, - queryParameters, Optional.of(request))) { - content = contentResolver.getErrorContent(errorContext); - response.setStatus(404); - } - } + var contentResponse = content.get(); + switch (contentResponse) { + case RedirectContentResponse redirectContent -> { + response.getHeaders().add(HttpHeader.LOCATION, redirectContent.location()); + response.setStatus(redirectContent.status()); + callback.succeeded(); + } + case DefaultContentResponse defaultContent -> { + response.getHeaders().add(HttpHeader.CONTENT_TYPE, "%s; charset=utf-8".formatted(defaultContent.contentType())); + Content.Sink.write(response, true, defaultContent.content(), callback); + } + default -> { + response.setStatus(404); + callback.succeeded(); + } + } - } + } catch (Exception e) { + log.error("error handling content", e); + response.setStatus(500); + response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html; charset=utf-8"); - var contentResponse = content.get(); - if (contentResponse instanceof RedirectContentResponse redirectContent) { - response.getHeaders().add(HttpHeader.LOCATION, redirectContent.location()); - response.setStatus(redirectContent.status()); - callback.succeeded(); - } else if (contentResponse instanceof DefaultContentResponse defaultContent) { - response.getHeaders().add(HttpHeader.CONTENT_TYPE, "%s; charset=utf-8".formatted(defaultContent.contentType())); - Content.Sink.write(response, true, defaultContent.content(), callback); - } else { - response.setStatus(404); - callback.succeeded(); - } - - } catch (Exception e) { - log.error("error handling content", e); - response.setStatus(500); - response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html; charset=utf-8"); - - if (ServerContext.IS_DEV) { - var stacktrace = ExceptionUtils.getStackTrace(e); - Content.Sink.write(response, true, "
%s
".formatted(stacktrace), callback); - } - } - return true; - } + if (ServerContext.IS_DEV) { + var stacktrace = ExceptionUtils.getStackTrace(e); + Content.Sink.write(response, true, "
%s
".formatted(stacktrace), callback); + } + } + return true; + } } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java index f6ef92d7a..d028b6ace 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java @@ -26,7 +26,6 @@ import com.condation.cms.api.content.TaxonomyResponse; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.TaxonomyResolver; -import com.condation.cms.server.filter.CreateRequestContextFilter; import com.google.inject.Inject; import java.util.Optional; import lombok.RequiredArgsConstructor; diff --git a/cms-server/src/main/java/com/condation/cms/server/host/VHost.java b/cms-server/src/main/java/com/condation/cms/server/host/VHost.java index f91b7517a..d13a2bdc7 100644 --- a/cms-server/src/main/java/com/condation/cms/server/host/VHost.java +++ b/cms-server/src/main/java/com/condation/cms/server/host/VHost.java @@ -20,7 +20,6 @@ * along with this program. If not, see . * #L% */ - import com.condation.cms.api.Constants; import java.io.IOException; import java.nio.file.Path; @@ -72,6 +71,7 @@ import com.condation.cms.server.filter.PooledRequestContextFilter; import com.condation.cms.server.filter.RequestLoggingFilter; import com.condation.cms.server.filter.PreviewFilter; +import com.condation.cms.server.handler.StaticFileHandler; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; import com.condation.cms.server.handler.content.JettyContentHandler; import com.condation.cms.server.handler.content.JettyTaxonomyHandler; @@ -87,9 +87,16 @@ import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.name.Names; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.InstanceBinding; +import java.lang.annotation.Annotation; +import com.condation.cms.server.annotations.Eager; /** * @@ -98,288 +105,313 @@ @Slf4j public class VHost { - private final String siteId; - private final Path hostBase; - - @Getter - private Handler hostHandler; - - @Getter - protected Injector injector; - - private final Configuration configuration = new Configuration(); - - public VHost(final String siteId, final Path hostBase, Path modulesPath, Injector globalInjector) { - this.siteId = siteId; - this.hostBase = hostBase; - - this.injector = globalInjector.createChildInjector(new SiteGlobalModule(), - new SiteModule(siteId, hostBase, this.configuration), - new SiteModulesModule(modulesPath), - new SiteHandlerModule(), - new ThemeModule()); - - // start configuration managment - injector.getInstance(ConfigManagement.class).initConfiguration(configuration); - // run site initializer - injector.getInstance(SiteConfigInitializer.class).init(); - } - - public String id() { - return injector.getInstance(Configuration.class).get(SiteConfiguration.class).siteProperties().id(); - } - - public void shutdown() { - try { - injector.getInstance(EventBus.class).syncPublish(new HostStoppedEvent(id())); - injector.getInstance(FileDB.class).close(); - } catch (Exception ex) { - log.error("", ex); - } - } - - public void reload() { - log.trace("reload theme"); - - try { - injector.getInstance(ConfigManagement.class).reload(); - - var theme = this.injector.getInstance(Theme.class); - - this.injector.getInstance(SiteMediaManager.class).reloadTheme(theme); - - this.injector.getInstance(ThemeMediaManager.class).reloadTheme(theme); - - ResourceHandler themeAssetsHandler = this.injector.getInstance(Key.get(ResourceHandler.class, Names.named("theme"))); - themeAssetsHandler.stop(); - themeAssetsHandler.setBaseResource(new FileFolderPathResource(theme.assetsPath())); - themeAssetsHandler.start(); - - this.injector.getInstance(TemplateEngine.class).updateTheme(theme); - this.injector.getInstance(SiteModuleContext.class).get(ThemeFeature.class).updateTheme(theme); - - injector.getInstance(EventBus.class).syncPublish(new HostReloadedEvent(id())); - } catch (Exception e) { - log.error("", e); - } - } - - public List hostnames() { - return injector.getInstance(SiteProperties.class).hostnames(); - } - - public void warmup () { - injector.getAllBindings().values().stream() - .map(Binding::getProvider) - .forEach(Provider::get); // Trigger eager Load - } - - public void init() throws IOException { - - var moduleManager = injector.getInstance(ModuleManager.class); - moduleManager.initModules(); - List activeModules = getActiveModules(); - activeModules.stream() - .filter(module_id -> moduleManager.getModuleIds().contains(module_id)) - .forEach(module_id -> { - try { - log.debug("activate module {}", module_id); - moduleManager.activateModule(module_id); - } catch (IOException ex) { - log.error(null, ex); - } - }); - - injector.getInstance(EventBus.class).register(InvalidateContentCacheEvent.class, (EventListener) (InvalidateContentCacheEvent event) -> { - log.debug("invalidate content cache"); + private final String siteId; + private final Path hostBase; + + @Getter + private Handler hostHandler; + + @Getter + protected Injector injector; + + private final Configuration configuration = new Configuration(); + + List requiredDirs = List.of("content", "assets", "public"); + + public VHost(final String siteId, final Path hostBase, Path modulesPath, Injector globalInjector) { + this.siteId = siteId; + this.hostBase = hostBase; + + this.injector = globalInjector.createChildInjector(new SiteGlobalModule(), + new SiteModule(siteId, hostBase, this.configuration), + new SiteModulesModule(modulesPath), + new SiteHandlerModule(), + new ThemeModule()); + + // start configuration managment + injector.getInstance(ConfigManagement.class).initConfiguration(configuration); + // run site initializer + injector.getInstance(SiteConfigInitializer.class).init(); + } + + public String id() { + return injector.getInstance(Configuration.class).get(SiteConfiguration.class).siteProperties().id(); + } + + public void shutdown() { + try { + injector.getInstance(EventBus.class).syncPublish(new HostStoppedEvent(id())); + injector.getInstance(FileDB.class).close(); + } catch (Exception ex) { + log.error("", ex); + } + } + + public void reload() { + log.trace("reload theme"); + + try { + injector.getInstance(ConfigManagement.class).reload(); + + var theme = this.injector.getInstance(Theme.class); + + this.injector.getInstance(SiteMediaManager.class).reloadTheme(theme); + + this.injector.getInstance(ThemeMediaManager.class).reloadTheme(theme); + + ResourceHandler themeAssetsHandler = this.injector.getInstance(Key.get(ResourceHandler.class, Names.named("theme.assets"))); + themeAssetsHandler.stop(); + themeAssetsHandler.setBaseResource(new FileFolderPathResource(theme.assetsPath())); + themeAssetsHandler.start(); + + this.injector.getInstance(TemplateEngine.class).updateTheme(theme); + this.injector.getInstance(SiteModuleContext.class).get(ThemeFeature.class).updateTheme(theme); + + injector.getInstance(EventBus.class).syncPublish(new HostReloadedEvent(id())); + } catch (Exception e) { + log.error("", e); + } + } + + public List hostnames() { + return injector.getInstance(SiteProperties.class).hostnames(); + } + + public void startUpWarmup() { + injector.getAllBindings().values().forEach(binding -> { + + Class type = binding.getKey().getTypeLiteral().getRawType(); + + if (type.isAnnotationPresent(Eager.class)) { + injector.getInstance(type); + } + }); + } + + public void init() throws IOException { + + // create required folders + for (String dir : requiredDirs) { + Path path = hostBase.resolve(dir); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } + + var moduleManager = injector.getInstance(ModuleManager.class); + moduleManager.initModules(); + List activeModules = getActiveModules(); + activeModules.stream() + .filter(module_id -> moduleManager.getModuleIds().contains(module_id)) + .forEach(module_id -> { + try { + log.debug("activate module {}", module_id); + moduleManager.activateModule(module_id); + } catch (IOException ex) { + log.error(null, ex); + } + }); + + injector.getInstance(EventBus.class).register(InvalidateContentCacheEvent.class, (EventListener) (InvalidateContentCacheEvent event) -> { + log.debug("invalidate content cache"); injector.getInstance(ContentParser.class).clearCache(); injector.getInstance(CacheManager.class).get(Constants.CacheNames.RESPONSE).ifPresent(cache -> cache.invalidate()); injector.getInstance(CacheManager.class).get(Constants.CacheNames.MARKDOWN).ifPresent(cache -> cache.invalidate()); }); - injector.getInstance(EventBus.class).register(InvalidateTemplateCacheEvent.class, (EventListener) (InvalidateTemplateCacheEvent event) -> { - log.debug("invalidate template cache"); - injector.getInstance(TemplateEngine.class).invalidateCache(); - }); - - Initializer initializer = new Initializer(this); - initializer.initServices(); - initSiteGlobals(); - } - - private void initSiteGlobals() throws IOException { - var globalJs = injector.getInstance(DB.class).getReadOnlyFileSystem().resolve("site.globals.js"); - if (globalJs.exists()) { - var context = injector.getInstance(GlobalExtensions.class); - context.evaluate(globalJs.getContent()); - - injector.getInstance(GlobalHooks.class).registerCronJob(); - } - } - - protected List getActiveModules() { - return SiteUtil.getActiveModules( - injector.getInstance(SiteProperties.class), - injector.getInstance(Theme.class) - ); - } - - public Handler buildHttpHandler() { - - Handler contentHandler = null; - - contentHandler = injector.getInstance(JettyContentHandler.class); - - var taxonomyHandler = injector.getInstance(JettyTaxonomyHandler.class); - var viewHandler = injector.getInstance(JettyViewHandler.class); - var routesHandler = injector.getInstance(RoutesHandler.class); - var authHandler = injector.getInstance(JettyAuthenticationHandler.class); - var initContextHandler = injector.getInstance(InitRequestContextFilter.class); - - var uiPreviewFilter = injector.getInstance(PreviewFilter.class); - - var defaultHandlerSequence = new Handler.Sequence( - authHandler, - initContextHandler, - uiPreviewFilter, - routesHandler, - viewHandler, - taxonomyHandler, - contentHandler - ); - - log.debug("create assets handler for site"); - ResourceHandler assetsHandler = injector.getInstance(Key.get(ResourceHandler.class, Names.named("site"))); - - ResourceHandler faviconHandler = new ResourceHandler(); - faviconHandler.setDirAllowed(false); - var assetBase = this.injector.getInstance(Key.get(Path.class, Names.named("assets"))); - faviconHandler.setBaseResource(new FileFolderPathResource(assetBase.resolve("favicon.ico"))); - - PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); - pathMappingsHandler.addMapping( - PathSpec.from("/"), - defaultHandlerSequence - ); - pathMappingsHandler.addMapping(PathSpec.from("/assets/*"), assetsHandler); - pathMappingsHandler.addMapping(PathSpec.from("/favicon.ico"), faviconHandler); - - var assetsMediaManager = this.injector.getInstance(SiteMediaManager.class); - injector.getInstance(EventBus.class).register(ConfigurationReloadEvent.class, assetsMediaManager); - injector.getInstance(EventBus.class).register(InvalidateMediaCache.class, (event) -> { - if (event.mediaPath() != null) { - assetsMediaManager.deleteTempFile(event.mediaPath()); - } else { - assetsMediaManager.clearTempDirectory(); - } - }); - final JettyMediaHandler mediaHandler = this.injector.getInstance(Key.get(JettyMediaHandler.class, Names.named("site"))); - - var siteMediaHandlerSequence = new Handler.Sequence( - uiPreviewFilter, - mediaHandler - ); - pathMappingsHandler.addMapping(PathSpec.from("/media/*"), siteMediaHandlerSequence); - - pathMappingsHandler.addMapping(PathSpec.from("/" + JettyModuleHandler.PATH + "/*"), - createModuleHandler() - ); - - pathMappingsHandler.addMapping(PathSpec.from("/" + JettyHttpHandlerExtensionHandler.PATH + "/*"), - createExtensionHandler() - ); - - pathMappingsHandler.addMapping(PathSpec.from("/" + APIHandler.PATH + "/*"), - createAPIHandler() - ); - - ContextHandler defaultContextHandler = new ContextHandler( - pathMappingsHandler, - injector.getInstance(SiteProperties.class).contextPath() - ); - defaultContextHandler.setVirtualHosts(injector.getInstance(SiteProperties.class).hostnames()); - - ContextHandlerCollection contextCollection = new ContextHandlerCollection( - defaultContextHandler - ); - - if (!injector.getInstance(Theme.class).empty()) { - var themeContextHandler = themeContextHandler(); - themeContextHandler.setVirtualHosts(injector.getInstance(SiteProperties.class).hostnames()); - contextCollection.addHandler(themeContextHandler); - } - - RequestLoggingFilter logContextHandler = new RequestLoggingFilter(contextCollection, injector.getInstance(SiteProperties.class)); - - hostHandler = logContextHandler; - - - return requestContextFilter(hostHandler, injector); - } - - private Handler createAPIHandler() { - var authHandler = injector.getInstance(JettyAuthenticationHandler.class); - var initContextHandler = injector.getInstance(InitRequestContextFilter.class); - var apiHandler = injector.getInstance(APIHandler.class); - var handlerSequence = new Handler.Sequence( - authHandler, - initContextHandler, - apiHandler - ); - return handlerSequence; - } - - private Handler createExtensionHandler() { - var authHandler = injector.getInstance(JettyAuthenticationHandler.class); - var initContextHandler = injector.getInstance(InitRequestContextFilter.class); - var extensionHandler = injector.getInstance(JettyHttpHandlerExtensionHandler.class); - var handlerSequence = new Handler.Sequence( - authHandler, - initContextHandler, - extensionHandler - ); - return handlerSequence; - } - - private Handler createModuleHandler() { - var authHandler = injector.getInstance(JettyAuthenticationHandler.class); - var initContextHandler = injector.getInstance(InitRequestContextFilter.class); - var modulehandler = injector.getInstance(JettyModuleHandler.class); - var handlerSequence = new Handler.Sequence( - authHandler, - initContextHandler, - modulehandler - ); - return handlerSequence; - } - - private Handler.Wrapper requestContextFilter(Handler handler, Injector injector) { - var performance = injector.getInstance(Configuration.class).get(ServerConfiguration.class).serverProperties().performance(); - if (performance.pool_enabled()) { - return new PooledRequestContextFilter(handler, injector.getInstance(RequestContextFactory.class), performance); - } - return new CreateRequestContextFilter(handler, injector.getInstance(RequestContextFactory.class)); - } - - private String appendContextIfNeeded(final String path) { - var contextPath = injector.getInstance(SiteProperties.class).contextPath(); - - if ("/".equals(contextPath)) { - return path; - } - - return contextPath + path; - } - - private ContextHandler themeContextHandler() { - final MediaManager themeAssetsMediaManager = this.injector.getInstance(ThemeMediaManager.class); - injector.getInstance(EventBus.class).register(ConfigurationReloadEvent.class, themeAssetsMediaManager); - JettyMediaHandler mediaHandler = this.injector.getInstance(Key.get(JettyMediaHandler.class, Names.named("theme"))); - ResourceHandler assetsHandler = this.injector.getInstance(Key.get(ResourceHandler.class, Names.named("theme"))); - - PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); - pathMappingsHandler.addMapping(PathSpec.from("/assets/*"), assetsHandler); - pathMappingsHandler.addMapping(PathSpec.from("/media/*"), mediaHandler); - - return new ContextHandler(pathMappingsHandler, appendContextIfNeeded("/theme")); - } + injector.getInstance(EventBus.class).register(InvalidateTemplateCacheEvent.class, (EventListener) (InvalidateTemplateCacheEvent event) -> { + log.debug("invalidate template cache"); + injector.getInstance(TemplateEngine.class).invalidateCache(); + }); + + Initializer initializer = new Initializer(this); + initializer.initServices(); + initSiteGlobals(); + } + + private void initSiteGlobals() throws IOException { + var globalJs = injector.getInstance(DB.class).getReadOnlyFileSystem().resolve("site.globals.js"); + if (globalJs.exists()) { + var context = injector.getInstance(GlobalExtensions.class); + context.evaluate(globalJs.getContent()); + + injector.getInstance(GlobalHooks.class).registerCronJob(); + } + } + + protected List getActiveModules() { + return SiteUtil.getActiveModules( + injector.getInstance(SiteProperties.class), + injector.getInstance(Theme.class) + ); + } + + public Handler buildHttpHandler() { + + Handler contentHandler = null; + + contentHandler = injector.getInstance(JettyContentHandler.class); + + var taxonomyHandler = injector.getInstance(JettyTaxonomyHandler.class); + var viewHandler = injector.getInstance(JettyViewHandler.class); + var routesHandler = injector.getInstance(RoutesHandler.class); + var authHandler = injector.getInstance(JettyAuthenticationHandler.class); + var initContextHandler = injector.getInstance(InitRequestContextFilter.class); + + var uiPreviewFilter = injector.getInstance(PreviewFilter.class); + + var publicHandler = injector.getInstance(Key.get(StaticFileHandler.class, Names.named("site.public"))); + var publicHandlerList = new ArrayList(); + publicHandlerList.add(publicHandler); + if (!injector.getInstance(Theme.class).empty()) { + var themePublicHandler = injector.getInstance(Key.get(StaticFileHandler.class, Names.named("theme.public"))); + publicHandlerList.add(themePublicHandler); + } + + var publicHandlerSequence = new Handler.Sequence(publicHandlerList); + + var defaultHandlerSequence = new Handler.Sequence( + authHandler, + publicHandlerSequence, + initContextHandler, + uiPreviewFilter, + routesHandler, + viewHandler, + taxonomyHandler, + contentHandler + ); + + log.debug("create assets handler for site"); + ResourceHandler assetsHandler = injector.getInstance(Key.get(ResourceHandler.class, Names.named("site.assets"))); + + ResourceHandler faviconHandler = new ResourceHandler(); + faviconHandler.setDirAllowed(false); + var assetBase = this.injector.getInstance(Key.get(Path.class, Names.named("assets"))); + faviconHandler.setBaseResource(new FileFolderPathResource(assetBase.resolve("favicon.ico"))); + + PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); + pathMappingsHandler.addMapping( + PathSpec.from("/"), + defaultHandlerSequence + ); + pathMappingsHandler.addMapping(PathSpec.from("/assets/*"), assetsHandler); + pathMappingsHandler.addMapping(PathSpec.from("/favicon.ico"), faviconHandler); + + var assetsMediaManager = this.injector.getInstance(SiteMediaManager.class); + injector.getInstance(EventBus.class).register(ConfigurationReloadEvent.class, assetsMediaManager); + injector.getInstance(EventBus.class).register(InvalidateMediaCache.class, (event) -> { + if (event.mediaPath() != null) { + assetsMediaManager.deleteTempFile(event.mediaPath()); + } else { + assetsMediaManager.clearTempDirectory(); + } + }); + final JettyMediaHandler mediaHandler = this.injector.getInstance(Key.get(JettyMediaHandler.class, Names.named("site.media"))); + + var siteMediaHandlerSequence = new Handler.Sequence( + uiPreviewFilter, + mediaHandler + ); + pathMappingsHandler.addMapping(PathSpec.from("/media/*"), siteMediaHandlerSequence); + + pathMappingsHandler.addMapping(PathSpec.from("/" + JettyModuleHandler.PATH + "/*"), + createModuleHandler() + ); + + pathMappingsHandler.addMapping(PathSpec.from("/" + JettyHttpHandlerExtensionHandler.PATH + "/*"), + createExtensionHandler() + ); + + pathMappingsHandler.addMapping(PathSpec.from("/" + APIHandler.PATH + "/*"), + createAPIHandler() + ); + + ContextHandler defaultContextHandler = new ContextHandler( + pathMappingsHandler, + injector.getInstance(SiteProperties.class).contextPath() + ); + defaultContextHandler.setVirtualHosts(injector.getInstance(SiteProperties.class).hostnames()); + + ContextHandlerCollection contextCollection = new ContextHandlerCollection( + defaultContextHandler + ); + + if (!injector.getInstance(Theme.class).empty()) { + var themeContextHandler = themeContextHandler(); + themeContextHandler.setVirtualHosts(injector.getInstance(SiteProperties.class).hostnames()); + contextCollection.addHandler(themeContextHandler); + } + + RequestLoggingFilter logContextHandler = new RequestLoggingFilter(contextCollection, injector.getInstance(SiteProperties.class)); + + hostHandler = logContextHandler; + + return requestContextFilter(hostHandler, injector); + } + + private Handler createAPIHandler() { + var authHandler = injector.getInstance(JettyAuthenticationHandler.class); + var initContextHandler = injector.getInstance(InitRequestContextFilter.class); + var apiHandler = injector.getInstance(APIHandler.class); + var handlerSequence = new Handler.Sequence( + authHandler, + initContextHandler, + apiHandler + ); + return handlerSequence; + } + + private Handler createExtensionHandler() { + var authHandler = injector.getInstance(JettyAuthenticationHandler.class); + var initContextHandler = injector.getInstance(InitRequestContextFilter.class); + var extensionHandler = injector.getInstance(JettyHttpHandlerExtensionHandler.class); + var handlerSequence = new Handler.Sequence( + authHandler, + initContextHandler, + extensionHandler + ); + return handlerSequence; + } + + private Handler createModuleHandler() { + var authHandler = injector.getInstance(JettyAuthenticationHandler.class); + var initContextHandler = injector.getInstance(InitRequestContextFilter.class); + var modulehandler = injector.getInstance(JettyModuleHandler.class); + var handlerSequence = new Handler.Sequence( + authHandler, + initContextHandler, + modulehandler + ); + return handlerSequence; + } + + private Handler.Wrapper requestContextFilter(Handler handler, Injector injector) { + var performance = injector.getInstance(Configuration.class).get(ServerConfiguration.class).serverProperties().performance(); + if (performance.pool_enabled()) { + return new PooledRequestContextFilter(handler, injector.getInstance(RequestContextFactory.class), performance); + } + return new CreateRequestContextFilter(handler, injector.getInstance(RequestContextFactory.class)); + } + + private String appendContextIfNeeded(final String path) { + var contextPath = injector.getInstance(SiteProperties.class).contextPath(); + + if ("/".equals(contextPath)) { + return path; + } + + return contextPath + path; + } + + private ContextHandler themeContextHandler() { + final MediaManager themeAssetsMediaManager = this.injector.getInstance(ThemeMediaManager.class); + injector.getInstance(EventBus.class).register(ConfigurationReloadEvent.class, themeAssetsMediaManager); + JettyMediaHandler mediaHandler = this.injector.getInstance(Key.get(JettyMediaHandler.class, Names.named("theme.media"))); + ResourceHandler assetsHandler = this.injector.getInstance(Key.get(ResourceHandler.class, Names.named("theme.assets"))); + + PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); + pathMappingsHandler.addMapping(PathSpec.from("/assets/*"), assetsHandler); + pathMappingsHandler.addMapping(PathSpec.from("/media/*"), mediaHandler); + + return new ContextHandler(pathMappingsHandler, appendContextIfNeeded("/theme")); + } } diff --git a/modules/ui-module/src/main/resources/manager/css/manager.css b/modules/ui-module/src/main/resources/manager/css/manager.css index dc58a5c53..8f40dc4da 100644 --- a/modules/ui-module/src/main/resources/manager/css/manager.css +++ b/modules/ui-module/src/main/resources/manager/css/manager.css @@ -8,45 +8,16 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . * #L% */ -/* -html, -body { - height: 100%; - margin: 0; - display: flex; - flex-direction: column; -} - -#content { - flex: 1 1 auto; - overflow: hidden; - display: flex; - flex-direction: column; -} - -#previewWrapper { - flex: 1; - position: relative; -} - -iframe#contentPreview { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - border: none; -} -*/ i[data-cms-section-handle] { cursor: pointer; @@ -73,6 +44,9 @@ tr[data-cms-file-directory] { display: none; } +/* ========================================================= + IMAGE HOVER +========================================================= */ .cms-image-hover-wrapper { position: relative; @@ -80,7 +54,6 @@ tr[data-cms-file-directory] { .cms-small-image { width: 50px; - /* oder dein Wunschwert */ height: auto; cursor: pointer; } @@ -106,6 +79,22 @@ tr[data-cms-file-directory] { height: auto; } +/* ========================================================= + FORM +========================================================= */ + +.cms-form-field[data-cms-form-field-type="markdown"] { + margin-bottom: 2rem; +} + +/* WICHTIG: + flex-grow-1 auf mehreren Cherry Editoren kann Layoutprobleme erzeugen. + Falls möglich: + class="mb-3 cms-form-field" + statt + class="mb-3 cms-form-field flex-grow-1" +*/ + .cms-form .cms-drop-zone { border: 2px dashed #6c757d; border-radius: 0.5rem; @@ -129,16 +118,93 @@ tr[data-cms-file-directory] { object-fit: cover; border-radius: 0.25rem; } + .cms-form .cms-drop-zone.drag-over { border: 2px dashed #007bff; - background-color: rgba(0,123,255,0.1); + background-color: rgba(0, 123, 255, 0.1); } .cms-toolbar-button:hover { cursor: pointer; } -/* focal poit editor */ +/* ========================================================= + CHERRY MARKDOWN EDITOR FIXES +========================================================= */ + +/* + * Fix für mehrere Cherry Editoren untereinander. + * Verhindert Überlagerungen und falsche Höhenberechnung. + */ + +.cherry-editor-container { + position: relative; + height: auto !important; + min-height: 300px; + overflow: visible; + display: flex; + flex-direction: column; +} + +/* + * Cherry selbst als Flex Container + */ +.cherry-editor-container .cherry { + display: flex; + flex-direction: column; + min-height: 300px; + height: auto !important; +} + +/* + * Editorbereich soll sauber wachsen + */ +.cherry-editor-container .cherry-editor { + flex: 1 1 auto; + height: auto !important; + min-height: 250px; +} + +/* + * WICHTIG: + * height:100% entfernt + */ +.cherry-editor-container .cherry-markdown, +.cherry-editor-container .cm-editor { + height: auto !important; + min-height: 250px; + overflow: auto; +} + +/* + * CodeMirror Scrollbereich + */ +.cherry-editor-container .cm-scroller { + min-height: 250px; +} + +/* + * Floating Elemente dürfen nicht abgeschnitten werden + */ +.cherry-editor-container .cherry-dropdown, +.cherry-editor-container .cherry-bubble, +.cherry-editor-container .cherry-floatmenu { + z-index: 2000; +} + +/* + * Toolbar stabil halten + */ +.cherry-editor-container .cherry-toolbar { + position: sticky; + top: 0; + z-index: 5; +} + +/* ========================================================= + FOCAL POINT EDITOR +========================================================= */ + .cms-focal-wrapper { position: relative; display: inline-block; @@ -160,10 +226,11 @@ tr[data-cms-file-directory] { transform: translate(-50%, -50%); pointer-events: none; } -/* end focal point editor */ -/* sidebar */ -/* Resizable Offcanvas Sidebar */ +/* ========================================================= + SIDEBAR / OFFCANVAS +========================================================= */ + .offcanvas-end.resizable { min-width: 250px; max-width: 90vw; @@ -172,11 +239,26 @@ tr[data-cms-file-directory] { right: 0; top: 0; bottom: 0; - overflow: auto; + + /* WICHTIG */ + overflow-x: hidden; + overflow-y: auto; + transition: box-shadow 0.2s ease; + scroll-behavior: smooth; +} + +/* + * Bootstrap Offcanvas Body + * verhindert abgeschnittene Cherry Elemente + */ +.offcanvas-body { + overflow-x: hidden; + overflow-y: auto; } -/* Resize Handle - standardmäßig unsichtbar */ +/* Resize Handle */ + .cms-resize-handle { position: absolute; left: -5px; @@ -190,36 +272,34 @@ tr[data-cms-file-directory] { opacity: 0; } -/* Handle wird sichtbar beim Hover über Sidebar */ .offcanvas-end.resizable:hover .cms-resize-handle { opacity: 1; } -/* Handle Hover State */ .cms-resize-handle:hover { - background: linear-gradient(to right, + background: linear-gradient( + to right, transparent, rgba(13, 110, 253, 0.1) 30%, rgba(13, 110, 253, 0.15) 50%, rgba(13, 110, 253, 0.1) 70%, transparent - ); + ); } -/* Handle Active State - bleibt sichtbar während Resize */ .cms-resize-handle:active, .offcanvas-end.resizable.is-resizing .cms-resize-handle { opacity: 1; - background: linear-gradient(to right, + background: linear-gradient( + to right, transparent, rgba(13, 110, 253, 0.2) 30%, rgba(13, 110, 253, 0.25) 50%, rgba(13, 110, 253, 0.2) 70%, transparent - ); + ); } -/* Visual indicator in the middle of the handle */ .cms-resize-handle::after { content: ''; position: absolute; @@ -233,23 +313,17 @@ tr[data-cms-file-directory] { transition: opacity 0.2s ease; } -/* Body während Resize */ body.resizing-sidebar { user-select: none !important; cursor: col-resize !important; } -/* Offcanvas während Resize - verhindert Transition-Ruckeln */ .offcanvas-end.resizable.is-resizing { transition: none; } -/* Smooth scrolling */ -.offcanvas-end.resizable { - scroll-behavior: smooth; -} +/* Scrollbar */ -/* Optional: Schönere Scrollbar */ .offcanvas-end.resizable::-webkit-scrollbar { width: 8px; } @@ -267,7 +341,10 @@ body.resizing-sidebar { background: var(--bs-dark, #343a40); } -/* Responsive Anpassungen */ +/* ========================================================= + RESPONSIVE +========================================================= */ + @media (max-width: 768px) { .offcanvas-end.resizable { min-width: 100vw !important; @@ -278,89 +355,76 @@ body.resizing-sidebar { display: none; } } -/* end sidebar */ -/* Fix für Modal über Offcanvas */ -/* Stelle sicher dass Modal Inputs fokussierbar sind */ -/* Modal über Offcanvas - pointer-events fix */ -.modal.show { - pointer-events: auto !important; -} - -.modal-dialog { - pointer-events: auto !important; -} - -.modal-content { - pointer-events: auto !important; -} - -.modal-body { - pointer-events: auto !important; -} +/* ========================================================= + MODAL FIXES +========================================================= */ +.modal.show, +.modal-dialog, +.modal-content, +.modal-body, .modal-body input, .modal-body textarea, .modal-body select { pointer-events: auto !important; } -/* Backdrop darf nicht blockieren */ .modal-backdrop.show { pointer-events: none !important; } -.cherry-editor-container .cherry-markdown, -.cherry-editor-container .cm-editor { - height: 100%; - overflow: auto; -} - -.cms-node-status-published{ - --bs-btn-color:#fff; - --bs-btn-bg:#5cb85c; - --bs-btn-border-color:#5cb85c; - --bs-btn-hover-color:#fff; - --bs-btn-hover-bg:#4e9c4e; - --bs-btn-hover-border-color:#4a934a; - --bs-btn-focus-shadow-rgb:116,195,116; - --bs-btn-active-color:#fff; - --bs-btn-active-bg:#4a934a; - --bs-btn-active-border-color:#458a45; - --bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color:#fff; - --bs-btn-disabled-bg:#5cb85c; - --bs-btn-disabled-border-color:#5cb85c -} -.cms-node-status-published-not-visible{ - --bs-btn-color:#fff; - --bs-btn-bg:#5bc0de; - --bs-btn-border-color:#5bc0de; - --bs-btn-hover-color:#fff; - --bs-btn-hover-bg:#4da3bd; - --bs-btn-hover-border-color:#499ab2; - --bs-btn-focus-shadow-rgb:116,201,227; - --bs-btn-active-color:#fff; - --bs-btn-active-bg:#499ab2; - --bs-btn-active-border-color:#4490a7; - --bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color:#fff; - --bs-btn-disabled-bg:#5bc0de; - --bs-btn-disabled-border-color:#5bc0de -} -.cms-node-status-unpublished{ - --bs-btn-color:#fff; - --bs-btn-bg:#ffc107; - --bs-btn-border-color:#ffc107; - --bs-btn-hover-color:#fff; - --bs-btn-hover-bg:#d9a406; - --bs-btn-hover-border-color:#cc9a06; - --bs-btn-focus-shadow-rgb:255,202,44; - --bs-btn-active-color:#fff; - --bs-btn-active-bg:#cc9a06; - --bs-btn-active-border-color:#bf9105; - --bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color:#fff; - --bs-btn-disabled-bg:#ffc107; - --bs-btn-disabled-border-color:#ffc107 -} +/* ========================================================= + NODE STATUS BUTTONS +========================================================= */ + +.cms-node-status-published { + --bs-btn-color: #fff; + --bs-btn-bg: #5cb85c; + --bs-btn-border-color: #5cb85c; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #4e9c4e; + --bs-btn-hover-border-color: #4a934a; + --bs-btn-focus-shadow-rgb: 116, 195, 116; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #4a934a; + --bs-btn-active-border-color: #458a45; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #5cb85c; + --bs-btn-disabled-border-color: #5cb85c; +} + +.cms-node-status-published-not-visible { + --bs-btn-color: #fff; + --bs-btn-bg: #5bc0de; + --bs-btn-border-color: #5bc0de; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #4da3bd; + --bs-btn-hover-border-color: #499ab2; + --bs-btn-focus-shadow-rgb: 116, 201, 227; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #499ab2; + --bs-btn-active-border-color: #4490a7; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #5bc0de; + --bs-btn-disabled-border-color: #5bc0de; +} + +.cms-node-status-unpublished { + --bs-btn-color: #fff; + --bs-btn-bg: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #d9a406; + --bs-btn-hover-border-color: #cc9a06; + --bs-btn-focus-shadow-rgb: 255, 202, 44; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #cc9a06; + --bs-btn-active-border-color: #bf9105; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #ffc107; + --bs-btn-disabled-border-color: #ffc107; +} \ No newline at end of file diff --git a/test-server/hosts/demo/content/marken/index.md b/test-server/hosts/demo/content/marken/index.md new file mode 100644 index 000000000..65679f29e --- /dev/null +++ b/test-server/hosts/demo/content/marken/index.md @@ -0,0 +1,21 @@ +--- +title: 'Our brands' +template: taxonomy.single.brand.html +parent: + text: testsdf +count: 46 +description: desc +features: [ + ] +background_color: '#000000' +range_test: 0 +unpublish_date: null +published: true +publish_date: null +translations: + de: /ueber +--- + +# Out brands + +Best brand in the world diff --git a/test-server/hosts/demo/assets/favicon.ico b/test-server/hosts/demo/public/favicon.ico similarity index 100% rename from test-server/hosts/demo/assets/favicon.ico rename to test-server/hosts/demo/public/favicon.ico diff --git a/test-server/hosts/demo/content/robots.txt b/test-server/hosts/demo/public/robots.txt similarity index 82% rename from test-server/hosts/demo/content/robots.txt rename to test-server/hosts/demo/public/robots.txt index aadfde984..d107a5f08 100644 --- a/test-server/hosts/demo/content/robots.txt +++ b/test-server/hosts/demo/public/robots.txt @@ -1,3 +1,3 @@ User-agent: * Disallow: /about/impressum -Allow: / \ No newline at end of file +Allow: / diff --git a/test-server/server.toml b/test-server/server.toml index c18094592..5daacce38 100644 --- a/test-server/server.toml +++ b/test-server/server.toml @@ -1,5 +1,5 @@ # environment: dev and prod -env = "prod" +env = "dev" # server settings [server] diff --git a/test-server/themes/demo/extensions/theme.manager.js b/test-server/themes/demo/extensions/theme.manager.js index f7298e066..2f8828b3b 100644 --- a/test-server/themes/demo/extensions/theme.manager.js +++ b/test-server/themes/demo/extensions/theme.manager.js @@ -206,6 +206,11 @@ $hooks.registerFilter("manager/contentTypes/register", (context) => { type: "markdown", name: "about1", title: "About1" + }, + { + type: "markdown", + name: "about2", + title: "About2" } ] } diff --git a/test-server/themes/demo/public/theme.txt b/test-server/themes/demo/public/theme.txt new file mode 100644 index 000000000..02a8b94db --- /dev/null +++ b/test-server/themes/demo/public/theme.txt @@ -0,0 +1 @@ +demo theme \ No newline at end of file diff --git a/test-server/themes/demo/templates/taxonomy.html b/test-server/themes/demo/templates/taxonomy.html new file mode 100644 index 000000000..98f29b82c --- /dev/null +++ b/test-server/themes/demo/templates/taxonomy.html @@ -0,0 +1,23 @@ + + + + + + + + + {% include "libs/fragments.html" %} + + + + + + +
+

{{ taxonomy.title }}

+
+ + + + + \ No newline at end of file