Skip to content

Commit 31e2cb5

Browse files
authored
fix: prevent embedded styles to leak main document (#19221)
* fix: prevent embedded styles to leak main document When using an exported a themed Flow web-component, Lumo style may leak the embedding document, causing invalid CSS rules to be applied. This change prevents applying Lumo global imports when the theme is applied to a web-component. Fixes #12704 * apply review suggestions
1 parent 0e15906 commit 31e2cb5

File tree

12 files changed

+123
-19
lines changed

12 files changed

+123
-19
lines changed

flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@
4343
import java.util.stream.Collectors;
4444
import java.util.stream.Stream;
4545

46-
import org.slf4j.Logger;
47-
4846
import com.vaadin.flow.internal.StringUtil;
4947
import com.vaadin.flow.internal.UrlUtil;
5048
import com.vaadin.flow.server.Constants;
@@ -55,6 +53,7 @@
5553
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
5654
import com.vaadin.flow.shared.ApplicationConstants;
5755
import com.vaadin.flow.theme.AbstractTheme;
56+
import org.slf4j.Logger;
5857

5958
import static com.vaadin.flow.server.Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT;
6059
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
@@ -96,6 +95,8 @@ abstract class AbstractUpdateImports implements Runnable {
9695
+ " return !ae || ae.blur() || ae.focus() || true;\n" + "}";
9796
private static final String IMPORT_TEMPLATE = "import '%s';";
9897
private static final Pattern STARTING_DOT_SLASH = Pattern.compile("^\\./+");
98+
private static final Pattern VAADIN_LUMO_GLOBAL_IMPORT = Pattern
99+
.compile(".*@vaadin/vaadin-lumo-styles/.*-global.js.*");
99100
final Options options;
100101

101102
private final UnaryOperator<String> themeToLocalPathConverter;
@@ -106,7 +107,8 @@ abstract class AbstractUpdateImports implements Runnable {
106107

107108
private ClassFinder classFinder;
108109

109-
private final File generatedFlowImports;
110+
final File generatedFlowImports;
111+
final File generatedFlowWebComponentImports;
110112
private final File generatedFlowDefinitions;
111113
private File chunkFolder;
112114

@@ -123,6 +125,11 @@ abstract class AbstractUpdateImports implements Runnable {
123125
generatedFlowDefinitions = new File(
124126
generatedFlowImports.getParentFile(),
125127
FrontendUtils.IMPORTS_D_TS_NAME);
128+
129+
generatedFlowWebComponentImports = new File(
130+
FrontendUtils.getFlowGeneratedWebComponentsFolder(
131+
options.getFrontendDirectory()),
132+
FrontendUtils.IMPORTS_WEB_COMPONENT_NAME);
126133
this.chunkFolder = new File(generatedFlowImports.getParentFile(),
127134
"chunks");
128135

@@ -138,6 +145,8 @@ public void run() {
138145

139146
Map<File, List<String>> output = process(css, javascript);
140147
writeOutput(output);
148+
writeWebComponentImports(
149+
filterWebComponentImports(output.get(generatedFlowImports)));
141150

142151
getLogger().debug("Imports and chunks update took {} ms.",
143152
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
@@ -198,6 +207,29 @@ protected void writeOutput(Map<File, List<String>> outputFiles) {
198207
}
199208
}
200209

210+
// Visible for test
211+
List<String> filterWebComponentImports(List<String> lines) {
212+
if (lines != null) {
213+
// Exclude Lumo global imports for exported web-component
214+
return lines.stream()
215+
.filter(VAADIN_LUMO_GLOBAL_IMPORT.asPredicate().negate())
216+
.collect(Collectors.toList());
217+
}
218+
return lines;
219+
}
220+
221+
private void writeWebComponentImports(List<String> lines) {
222+
if (lines != null) {
223+
try {
224+
FileIOUtils.writeIfChanged(generatedFlowWebComponentImports,
225+
lines);
226+
} catch (IOException e) {
227+
throw new IllegalStateException(
228+
"Failed to update the generated Flow imports", e);
229+
}
230+
}
231+
}
232+
201233
/**
202234
* Processes what the scanner found and produces a set of files to write to
203235
* the generated folder.

flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ public class FrontendUtils {
178178
*/
179179
public static final String IMPORTS_D_TS_NAME = "generated-flow-imports.d.ts";
180180

181+
/**
182+
* Name of the file that contains application imports, javascript, theme and
183+
* style annotations used when embedding Flow as web-component.
184+
*/
185+
public static final String IMPORTS_WEB_COMPONENT_NAME = "generated-flow-webcomponent-imports.js";
186+
181187
public static final String THEME_IMPORTS_D_TS_NAME = "theme.d.ts";
182188
public static final String THEME_IMPORTS_NAME = "theme.js";
183189

flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ static Set<String> getGeneratedModules(File frontendFolder) {
202202
return FileUtils
203203
.listFiles(webComponentsFolder, new String[] { "js" }, true)
204204
.stream()
205+
.filter(file -> !FrontendUtils.IMPORTS_WEB_COMPONENT_NAME
206+
.equals(file.getName()))
205207
.map(file -> unixPath
206208
.apply(baseDir.relativize(file.toURI()).getPath()))
207209
.collect(Collectors.toSet());

flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateWebComponentBootstrap.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ public class TaskGenerateWebComponentBootstrap
5252
protected String getFileContent() {
5353
List<String> lines = new ArrayList<>();
5454

55-
lines.add(
56-
"import 'Frontend/generated/flow/generated-flow-imports.js';");
55+
lines.add("import 'Frontend/generated/flow/web-components/"
56+
+ FrontendUtils.IMPORTS_WEB_COMPONENT_NAME + "';");
5757
lines.add("import { init } from '" + FrontendUtils.JAR_RESOURCES_IMPORT
5858
+ "FlowClient.js';");
5959
lines.add("init();");

flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) {
8181

8282
themeFileContent += `let needsReloadOnChanges = false;\n`;
8383
const imports = [];
84+
const deferredImports = [];
8485
const componentCssImports = [];
8586
const globalFileContent = [];
8687
const globalCssCode = [];
@@ -118,7 +119,9 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) {
118119
imports.push(`import { ${lumoImport} } from '@vaadin/vaadin-lumo-styles/${lumoImport}.js';\n`);
119120
if (lumoImport === 'utility' || lumoImport === 'badge' || lumoImport === 'typography' || lumoImport === 'color') {
120121
// Inject into main document the same way as other Lumo styles are injected
121-
imports.push(`import '@vaadin/vaadin-lumo-styles/${lumoImport}-global.js';\n`);
122+
// Defer import at runtime to prevent leaking document when theme is applied
123+
// to an embedded component
124+
deferredImports.push(`import('@vaadin/vaadin-lumo-styles/${lumoImport}-global.js');\n`);
122125
}
123126
});
124127

@@ -220,6 +223,8 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) {
220223
const removers = [];
221224
if (target !== document) {
222225
${shadowOnlyCss.join('')}
226+
} else {
227+
${deferredImports.join('\n')}
223228
}
224229
${parentTheme}
225230
${globalCssCode.join('')}

flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.List;
3232
import java.util.Map;
3333
import java.util.Set;
34+
import java.util.function.Predicate;
3435
import java.util.regex.Matcher;
3536
import java.util.regex.Pattern;
3637
import java.util.stream.Collectors;
@@ -124,6 +125,7 @@ public static class FooCssImport2 extends Component {
124125
class UpdateImports extends AbstractUpdateImports {
125126

126127
private Map<File, List<String>> output;
128+
private List<String> webComponentImports;
127129

128130
UpdateImports(FrontendDependenciesScanner scanner, Options options) {
129131
super(options, scanner);
@@ -134,6 +136,12 @@ protected void writeOutput(Map<File, List<String>> output) {
134136
this.output = output;
135137
}
136138

139+
@Override
140+
List<String> filterWebComponentImports(List<String> lines) {
141+
webComponentImports = super.filterWebComponentImports(lines);
142+
return webComponentImports;
143+
}
144+
137145
public Map<File, List<String>> getOutput() {
138146
return output;
139147
}
@@ -398,13 +406,41 @@ public void generate_containsLumoThemeFiles() {
398406
updater.run();
399407

400408
assertContainsImports(true, "@vaadin/vaadin-lumo-styles/color.js",
409+
"@vaadin/vaadin-lumo-styles/color-global.js",
401410
"@vaadin/vaadin-lumo-styles/typography.js",
411+
"@vaadin/vaadin-lumo-styles/typography-global.js",
402412
"@vaadin/vaadin-lumo-styles/sizing.js",
403413
"@vaadin/vaadin-lumo-styles/spacing.js",
404414
"@vaadin/vaadin-lumo-styles/style.js",
405415
"@vaadin/vaadin-lumo-styles/icons.js");
406416
}
407417

418+
@Test
419+
public void generate_embeddedImports_doNotContainLumoGlobalThemeFiles()
420+
throws IOException {
421+
updater.run();
422+
423+
List<String> flowImports = new ArrayList<>(
424+
updater.getOutput().get(updater.generatedFlowImports));
425+
426+
Predicate<String> lumoGlobalsMatcher = Pattern
427+
.compile("@vaadin/vaadin-lumo-styles/.*-global.js")
428+
.asPredicate();
429+
assertTrue(flowImports.stream().anyMatch(lumoGlobalsMatcher));
430+
431+
assertTrue(
432+
"Import for web-components should not contain lumo global imports",
433+
updater.webComponentImports.stream()
434+
.noneMatch(lumoGlobalsMatcher));
435+
436+
// Check that imports other than lumo globals are the same
437+
flowImports.removeAll(updater.webComponentImports);
438+
assertTrue(
439+
"Flow and web-component imports must be the same, except for lumo globals",
440+
flowImports.stream().allMatch(lumoGlobalsMatcher));
441+
442+
}
443+
408444
@Test
409445
public void jsModulesOrderIsPreservedAnsAfterJsModules() {
410446
updater.run();

flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeTestComponents.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ public static class MainView extends Component {
157157
* Lumo component theme class implementation.
158158
*/
159159
@JsModule("@vaadin/vaadin-lumo-styles/color.js")
160+
@JsModule("@vaadin/vaadin-lumo-styles/color-global.js")
160161
@JsModule("@vaadin/vaadin-lumo-styles/typography.js")
162+
@JsModule("@vaadin/vaadin-lumo-styles/typography-global.js")
161163
@JsModule("@vaadin/vaadin-lumo-styles/sizing.js")
162164
@JsModule("@vaadin/vaadin-lumo-styles/spacing.js")
163165
@JsModule("@vaadin/vaadin-lumo-styles/style.js")

flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdateTestUtil.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ List<String> getExpectedImports() {
8585
"@vaadin/vaadin-lumo-styles/icons.js",
8686
"@vaadin/vaadin-lumo-styles/style.js",
8787
"@vaadin/vaadin-lumo-styles/typography.js",
88+
"@vaadin/vaadin-lumo-styles/typography-global.js",
8889
"@vaadin/vaadin-lumo-styles/color.js",
90+
"@vaadin/vaadin-lumo-styles/color-global.js",
8991
"@vaadin/vaadin-lumo-styles/sizing.js",
9092
"@vaadin/vaadin-date-picker/theme/lumo/vaadin-date-picker.js",
9193
"@vaadin/vaadin-date-picker/src/vaadin-month-calendar.js",

flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateWebComponentBootstrapTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@
1717

1818
import java.io.File;
1919

20+
import com.vaadin.flow.di.Lookup;
21+
import com.vaadin.flow.server.ExecutionFailedException;
2022
import org.junit.Assert;
2123
import org.junit.Before;
2224
import org.junit.Rule;
2325
import org.junit.Test;
2426
import org.junit.rules.TemporaryFolder;
2527
import org.mockito.Mockito;
2628

27-
import com.vaadin.flow.di.Lookup;
28-
import com.vaadin.flow.server.ExecutionFailedException;
29-
3029
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
3130

3231
public class TaskGenerateWebComponentBootstrapTest {
@@ -56,8 +55,9 @@ public void should_importGeneratedImports()
5655
throws ExecutionFailedException {
5756
taskGenerateWebComponentBootstrap.execute();
5857
String content = taskGenerateWebComponentBootstrap.getFileContent();
59-
Assert.assertTrue(content.contains("import 'Frontend/generated/flow/"
60-
+ FrontendUtils.IMPORTS_NAME + "'"));
58+
Assert.assertTrue(content
59+
.contains("import 'Frontend/generated/flow/web-components/"
60+
+ FrontendUtils.IMPORTS_WEB_COMPONENT_NAME + "'"));
6161
}
6262

6363
@Test

flow-server/src/test/java/com/vaadin/flow/server/frontend/scanner/FullDependenciesScannerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public void getModules_explcitTheme_returnAllModulesExcludingNotUsedTheme_getCla
338338
return null;
339339
}, null);
340340

341-
DepsTests.assertImportCount(26, scanner.getModules());
341+
DepsTests.assertImportCount(28, scanner.getModules());
342342
List<String> modules = DepsTests.merge(scanner.getModules());
343343
assertJsModules(modules);
344344

0 commit comments

Comments
 (0)