mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-18 09:34:34 +07:00
PY-79967 Support HTML injections inside template strings
GitOrigin-RevId: 435d956e12f08c2a51c1f772cdbfa91901135e4b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9d86a1d671
commit
2647731608
@@ -13,6 +13,11 @@
|
||||
<place><![CDATA[pyLiteralExpression().and(pyArgument("path", 0))]]></place>
|
||||
</injection>
|
||||
|
||||
<injection language="HTML" injector-id="python">
|
||||
<display-name>HTML injections inside f-strings and t-strings</display-name>
|
||||
<place><![CDATA[templateOrFormattedStringContainsHTML()]]></place>
|
||||
</injection>
|
||||
|
||||
<injection language="SQL" injector-id="python">
|
||||
<display-name>"SQL select/delete/insert/update/create"</display-name>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.intellij.patterns.InitialPatternCondition;
|
||||
import com.intellij.patterns.PlatformPatterns;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.util.ProcessingContext;
|
||||
import com.jetbrains.python.ast.PyAstStringElement;
|
||||
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
|
||||
import com.jetbrains.python.documentation.docstrings.DocStringUtil;
|
||||
import com.jetbrains.python.patterns.PyElementPattern;
|
||||
@@ -21,6 +22,8 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.jetbrains.python.codeInsight.template.PyHTMLInjectionControllerKt.looksLikeHTML;
|
||||
|
||||
/**
|
||||
* Provides patterns for literals, strings, arguments and function/method arguments of Python.
|
||||
* <p>
|
||||
@@ -69,6 +72,19 @@ public final class PythonPatterns extends PlatformPatterns {
|
||||
});
|
||||
}
|
||||
|
||||
public static @NotNull PyElementPattern.Capture<PyFormattedStringElement> templateOrFormattedStringContainsHTML() {
|
||||
return new PyElementPattern.Capture<>(PyFormattedStringElement.class) {
|
||||
@Override
|
||||
public boolean accepts(@Nullable Object o, ProcessingContext context) {
|
||||
if (o instanceof PyStringLiteralExpression stringLiteralExpression && stringLiteralExpression.getStringElements().size() == 1) {
|
||||
PyAstStringElement stringElement = stringLiteralExpression.getStringElements().get(0);
|
||||
return stringElement.isTemplate() && looksLikeHTML(stringElement.getContent());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static @NotNull PyElementPattern.Capture<PyExpression> pyModuleFunctionArgument(@Nullable String functionName,
|
||||
int index,
|
||||
@NotNull String moduleName) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.codeInsight.template
|
||||
|
||||
import com.intellij.xml.util.HtmlUtil
|
||||
|
||||
|
||||
private val OBVIOUS_HTML_TAGS = setOf("html", "div", "span", "body", "p", "h1", "h2", "h3", "img",
|
||||
"table", "ul", "ol", "li", "a", "img", "form", "script")
|
||||
|
||||
fun looksLikeHTML(content: String): Boolean {
|
||||
if (content.isEmpty()) return false
|
||||
|
||||
val cleanContent = content.trim().replace(Regex("[\n\r]"), "")
|
||||
|
||||
if (cleanContent.startsWith("<")) {
|
||||
|
||||
val tagNamePattern = "<(\\w+)".toRegex()
|
||||
val tagNameMatch = tagNamePattern.find(cleanContent)
|
||||
if (tagNameMatch != null) {
|
||||
val tagName = tagNameMatch.groupValues[1]
|
||||
if (OBVIOUS_HTML_TAGS.contains(tagName) ||
|
||||
HtmlUtil.isHtmlBlockTag(tagName, false) ||
|
||||
HtmlUtil.isTagWithOptionalEnd(tagName, false)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<caret>name = "User"
|
||||
greeting = t'Hi, {name}!'
|
||||
@@ -0,0 +1,2 @@
|
||||
username = "User"
|
||||
greeting = t'Hi, {username}!'
|
||||
@@ -0,0 +1,7 @@
|
||||
<caret>name = "User"
|
||||
greeting = t"""
|
||||
<div>
|
||||
<h2>Hi, {name}!</h2>
|
||||
<p>This is a t-string with both HTML and Python interpolation.</p>
|
||||
</div>
|
||||
"""
|
||||
@@ -0,0 +1,7 @@
|
||||
username = "User"
|
||||
greeting = t"""
|
||||
<div>
|
||||
<h2>Hi, {username}!</h2>
|
||||
<p>This is a t-string with both HTML and Python interpolation.</p>
|
||||
</div>
|
||||
"""
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.codeInsight.template;
|
||||
|
||||
import com.intellij.lang.Language;
|
||||
import com.intellij.lang.injection.InjectedLanguageManager;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiLanguageInjectionHost;
|
||||
import com.jetbrains.python.PythonFileType;
|
||||
import com.jetbrains.python.fixtures.PyTestCase;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tests for HTML language injection in Python template strings (t-strings) and f-strings.
|
||||
*/
|
||||
public class PyHtmlTemplateInjectionTest extends PyTestCase {
|
||||
|
||||
public void testSimpleHtmlInjection() {
|
||||
doTestHtmlInjected("x = t'<caret><html><body><h1>Hello, World!</h1></body></html>'");
|
||||
}
|
||||
|
||||
public void testSimpleMultiLineHtmlInjection() {
|
||||
doTestHtmlInjected("""
|
||||
html_content = t'''
|
||||
<caret><html>
|
||||
<head>
|
||||
<title>Test HTML Injection</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, World!</h1>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
""");
|
||||
}
|
||||
|
||||
public void testHtmlWithInterpolation() {
|
||||
doTestHtmlInjected("""
|
||||
name = "John"
|
||||
x = t'<div><h2>Welcome,<caret> {name}!</h2></div>'
|
||||
""");
|
||||
}
|
||||
|
||||
public void testSingleQuotedTString() {
|
||||
doTestHtmlInjected("x = t'<caret><span>This is a single-quoted t-string</span>'");
|
||||
}
|
||||
|
||||
public void testRawTString() {
|
||||
doTestHtmlInjected("x = tr'<caret><div class=\"raw\">Raw t-string with HTML</div>'");
|
||||
}
|
||||
|
||||
public void testNoInjectionInRegularString() {
|
||||
doTestNoInjection("x = \"<html><body><h1>Hello, World!</h1></body></html>\"");
|
||||
}
|
||||
|
||||
public void testNoInjectionInFString() {
|
||||
doTestNoInjection("x = f'<caret><span>This is an f-string</span>'");
|
||||
}
|
||||
|
||||
public void testNoInjectionInNonHtmlTString() {
|
||||
doTestNoInjection("x = t'<caret>plain string, nothing to see here'");
|
||||
}
|
||||
|
||||
public void testNoInjectionInNonHtmlTStringWithInterpolation() {
|
||||
doTestNoInjection("""
|
||||
name = "John"
|
||||
x = t'<caret>Hi, {name}!'
|
||||
""");
|
||||
}
|
||||
|
||||
public void testNoInjectionInEmptyTString() {
|
||||
doTestNoInjection("x = t'<caret>'");
|
||||
}
|
||||
|
||||
public void testNoInjectionInJsonTString() {
|
||||
doTestNoInjection("x = t'<caret>{\"name\": \"John\", \"age\": 30}'");
|
||||
}
|
||||
|
||||
public void testHtmlInjectedByScriptTag() {
|
||||
doTestHtmlInjected("""
|
||||
evil = t"<caret><script>alert('evil')</script>"
|
||||
""");
|
||||
}
|
||||
|
||||
public void testHtmlInjectedByImgTagWithAttributes() {
|
||||
doTestHtmlInjected("x = t'<caret><img src=\"shrubbery.jpg\" alt=\"looks nice\" />'");
|
||||
}
|
||||
|
||||
|
||||
public void testHtmlInjectedByImgTagWithInterpolation() {
|
||||
doTestHtmlInjected("""
|
||||
attributes = {"src": "shrubbery.jpg", "alt": "looks nice"}
|
||||
template = t"<caret><img {attributes} />"
|
||||
""");
|
||||
}
|
||||
|
||||
private void doTestNoInjection(@NotNull String text) {
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, text);
|
||||
InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
|
||||
PsiLanguageInjectionHost host = languageManager.getInjectionHost(getElementAtCaret());
|
||||
assertNull(host);
|
||||
}
|
||||
|
||||
private void doTestHtmlInjected(@NotNull String text) {
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, text);
|
||||
InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
|
||||
PsiElement elementAtCaret = getElementAtCaret();
|
||||
assertNotNull("Element under caret not found", elementAtCaret);
|
||||
|
||||
PsiLanguageInjectionHost host = languageManager.getInjectionHost(elementAtCaret);
|
||||
assertNotNull("Injection host is null", host);
|
||||
|
||||
List<Pair<PsiElement, TextRange>> injectedPsiFiles = languageManager.getInjectedPsiFiles(host);
|
||||
assertNotNull("No injected PSI files found", injectedPsiFiles);
|
||||
|
||||
assertFalse("No injected elements found", injectedPsiFiles.isEmpty());
|
||||
for (Pair<PsiElement, TextRange> pair : injectedPsiFiles) {
|
||||
assertEquals(pair.first.getLanguage(), Language.findLanguageByID("HTML"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.intellij.refactoring.BaseRefactoringProcessor;
|
||||
import com.intellij.refactoring.util.TextOccurrencesUtil;
|
||||
import com.intellij.testFramework.PlatformTestUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.jetbrains.python.PythonFileType;
|
||||
import com.jetbrains.python.PythonTestUtil;
|
||||
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
|
||||
import com.jetbrains.python.fixtures.PyTestCase;
|
||||
@@ -413,6 +414,16 @@ public class PyRenameTest extends PyTestCase {
|
||||
doTest("renamed");
|
||||
}
|
||||
|
||||
// PY-79967
|
||||
public void testRenameVariableInTStringWithHTMLInjection() {
|
||||
doTest("username");
|
||||
}
|
||||
|
||||
// PY-79967
|
||||
public void testRenameVariableInSimpleTemplateString() {
|
||||
doTest("username");
|
||||
}
|
||||
|
||||
private void renameWithDocStringFormat(DocStringFormat format, final String newName) {
|
||||
runWithDocStringFormat(format, () -> doTest(newName));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user