Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ webauthn-help-text=Use your Passkey to sign in.
webauthn-passwordless-display-name=Passkey
webauthn-passwordless-help-text=Use your Passkey for passwordless sign in.
passwordless=Passwordless
error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at most {{2}} value(s).
error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} {2,choice,0#values|1#value|1<values}.
recovery-authn-code=My recovery authentication codes
recovery-authn-codes-display-name=Recovery authentication codes
recovery-authn-codes-help-text=These codes can be used to regain your access in case your other 2FA means are not available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1252,7 +1252,7 @@ onDragCancel=已取消拖动。列表未更改。
removeUser=移除用户
ownerManagedAccess=启用用户管理访问
userModelAttributeNameHelp=从 LDAP 导入用户时要添加的模型属性的名称
templateHelp=用于格式化要导入的用户名的模板。替换包含在 ${} 中。例如:'${ALIAS}.${CLAIM.sub}'。ALIAS 是供应商别名。CLAIM.<NAME > 引用 ID 或访问令牌声明。可以通过将 |uppercase 或 |lowercase 附加到替换值来将替换转换为大写或小写,例如“${CLAIM.sub | lowercase}”。
templateHelp=用于格式化要导入的用户名的模板。替换包含在 ${} 中。例如:'${ALIAS}.${CLAIM.sub}'。ALIAS 是供应商别名。CLAIM.<NAME> 引用 ID 或访问令牌声明。可以通过将 |uppercase 或 |lowercase 附加到替换值来将替换转换为大写或小写,例如“${CLAIM.sub | lowercase}”。
permissions=权限
emptyExecutionInstructions=您可以通过添加子流程或执行器来开始定义此流程
offlineSessionSettings=离线会话设置
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3133,7 +3133,7 @@ bruteForceMode.PermanentLockout=Lockout permanently
bruteForceMode.TemporaryLockout=Lockout temporarily
bruteForceMode.PermanentAfterTemporaryLockout=Lockout permanently after temporary lockout
bruteForceMode=Brute Force Mode
error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at most {{2}} value(s).
error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} {2,choice,0#values|1#value|1<values}.
multivalued=Multivalued
multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation.
to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
Comment on lines 3138 to 3139

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Broken properties entry: orphan line without key (parsing bug).

Line 3139 starts with plain text (“to the attribute...”) and will be parsed as key “to” with a long value, corrupting the bundle at runtime. Merge it with line 3138.

Apply this diff:

- multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation.
-to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
+ multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.

Run this repo-wide check for malformed .properties lines (missing '=' or ':' and not comments/blanks):


🏁 Script executed:

#!/bin/bash
# Find suspicious lines in .properties (not blank, not comment, without '=' or ':')
fd -t f -e properties \
| while read -r f; do
  awk '!/^\s*(#|!)|^\s*$/{ if ($0 !~ /=|:/) { printf("%s:%d:%s\n", FILENAME, NR, $0) } }' "$f"
done

Length of output: 4613


🏁 Script executed:

#!/bin/bash
# Verify the exact content and context around the reported malformed line
echo "=== messages_en.properties lines 3137-3140 ==="
sed -n '3137,3140p' js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties

echo ""
echo "=== messages_es.properties lines 3143-3146 ==="
sed -n '3143,3146p' js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties

Length of output: 368


🏁 Script executed:

#!/bin/bash
# Find the actual file paths
echo "=== Finding messages_en.properties ==="
fd -t f -n "messages_en.properties"

echo ""
echo "=== Finding messages_es.properties ==="
fd -t f -n "messages_es.properties"

Length of output: 540


🏁 Script executed:

#!/bin/bash
# Find the actual file paths (corrected syntax)
echo "=== Finding messages_en.properties ==="
fd -t f "messages_en.properties"

echo ""
echo "=== Finding messages_es.properties ==="
fd -t f "messages_es.properties"

Length of output: 1255


🏁 Script executed:

#!/bin/bash
# Verify the exact content and context around the reported malformed line
echo "=== messages_en.properties lines 3137-3140 ==="
sed -n '3137,3140p' js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties | cat -A

echo ""
echo "=== messages_es.properties lines 3143-3146 ==="
sed -n '3143,3146p' js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties | cat -A

Length of output: 787


Confirmed: Critical parsing bug in line 3139 needs merging with line 3138.

The verification shows that line 3139 in js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties lacks a key-value separator and contains orphaned text that belongs to the previous line's value. The .properties parser will treat "to" as a separate key, corrupting the message bundle. The proposed diff correctly merges the two lines into a single property entry.

The same issue exists in the Spanish translation at line 3145 of messages_es.properties.

🤖 Prompt for AI Agents
In
js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
around lines 3138-3139, line 3139 is orphaned text ("to the attribute...") that
should be part of the value for the previous key (multivaluedHelp); merge line
3139 into the value of line 3138 (ensure a single space between sentences),
removing the stray newline so the property remains a single key=value entry;
apply the same fix to the Spanish file messages_es.properties at the reported
line (line ~3145) by joining the orphaned line into the preceding key's value to
prevent parser errors.

Expand Down
11 changes: 11 additions & 0 deletions misc/theme-verifier/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20240325.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.13.0</version>
<scope>compile</scope>
</dependency>
Comment on lines +75 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Validate sanitizer and commons-text versions against your platform baseline

Adding owasp-java-html-sanitizer and commons-text as compile dependencies makes sense for the new verification logic. Please ensure these versions align with your broader Keycloak/platform BOM and pass your security/compliance checks (CVEs, license policy, etc.), as the plugin will carry them transitively where it’s used.


🌐 Web query:

Check whether com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1 and org.apache.commons:commons-text:1.13.0 are current recommended versions and if there are any known high/critical CVEs affecting them.

💡 Result:

Summary (checked Nov 22, 2025):

  • com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1

    • 20240325.1 is the project's current release tag on GitHub (released Mar 25, 2024). [1]
    • Known past high/critical issues (CVE-2021-42575, CVSS 9.8) affected versions prior to 20211018.1 and were fixed; I found no public high/critical CVE that applies to the 20240325.1 release itself. [2][3]
  • org.apache.commons:commons-text:1.13.0

    • 1.13.0 was released Dec 10, 2024, but Commons Text has newer releases after that (1.13.1 and 1.14.0), so 1.13.0 is not the latest recommended release as of Jul 20, 2025 (1.14.0). Upgrading to the latest stable release is recommended. [4]
    • The well-known critical “Text4Shell” issue CVE-2022-42889 (RCE via StringSubstitutor defaults) affects commons-text versions 1.5–1.9 and was fixed in 1.10.0; it does not affect 1.13.0. I found no other public high/critical CVE explicitly affecting 1.13.0. [5][6]

Recommendations

  • OWASP sanitizer: 20240325.1 is current upstream and has known prior issues fixed; no high/critical CVE found against that tag. If you depend on an OS/distribution package, check your distro advisory for backported fixes. [1][2]
  • Commons Text: upgrade to the latest stable Commons Text (1.14.0 as of Jul 20, 2025) to stay current; 1.13.0 is not vulnerable to CVE-2022-42889 but using the newest release is recommended. [4][5]

Sources

  1. OWASP java-html-sanitizer releases (GitHub) — release 20240325.1. [GitHub release page]. (github.com)
  2. CVE-2021-42575 details and advisories (affects versions <20211018.1). (wiz.io)
  3. OWASP project page. (owasp.org)
  4. Apache Commons Text changes / releases (shows 1.13.0 and later 1.14.0 released Jul 20, 2025). (commons.apache.org)
  5. NVD / advisories for CVE-2022-42889 (“Text4Shell”) — affects 1.5–1.9; fixed in 1.10.0. (nvd.nist.gov)

If you want, I can: (a) check your project’s dependency tree for transitive versions, or (b) fetch distro-specific advisories (e.g., Red Hat, Debian) for these artifacts.

Citations:


Upgrade commons-text to 1.14.0 for latest stability

owasp-java-html-sanitizer:20240325.1 is the current upstream release, with no public high/critical CVE found for that version.

However, commons-text:1.13.0 is not the latest stable release; 1.14.0 was released in July 2025. While 1.13.0 is not vulnerable to CVE-2022-42889 (Text4Shell), upgrading to 1.14.0 aligns with best practices for dependency currency.

🤖 Prompt for AI Agents
In misc/theme-verifier/pom.xml around lines 75 to 85, the pom declares
commons-text version 1.13.0; update that dependency version to 1.14.0 to align
with the current stable release. Edit the <version> element for the
org.apache.commons:commons-text dependency to 1.14.0, save the pom, then run a
quick build (mvn -DskipTests package or mvn -U test) to ensure no compatibility
issues and commit the change.

</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@
package org.keycloak.themeverifier;

import org.apache.maven.plugin.MojoExecutionException;
import org.owasp.html.PolicyFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class VerifyMessageProperties {

Expand All @@ -41,12 +48,129 @@ public List<String> verify() throws MojoExecutionException {
try {
String contents = Files.readString(file.toPath());
verifyNoDuplicateKeys(contents);
verifySafeHtml();
} catch (IOException e) {
throw new MojoExecutionException("Can not read file " + file, e);
}
return messages;
}

PolicyFactory POLICY_SOME_HTML = new org.owasp.html.HtmlPolicyBuilder()
.allowElements(
"br", "p", "strong", "b"
).toFactory();

PolicyFactory POLICY_NO_HTML = new org.owasp.html.HtmlPolicyBuilder().toFactory();

private void verifySafeHtml() {
PropertyResourceBundle bundle;
try (FileInputStream fis = new FileInputStream(file)) {
bundle = new PropertyResourceBundle(fis);
} catch (IOException e) {
throw new RuntimeException("unable to read file " + file, e);
}

PropertyResourceBundle bundleEnglish;
String englishFile = file.getAbsolutePath().replaceAll("resources-community", "resources")
.replaceAll("_[a-zA-Z-_]*\\.properties", "_en.properties");
try (FileInputStream fis = new FileInputStream(englishFile)) {
bundleEnglish = new PropertyResourceBundle(fis);
} catch (IOException e) {
throw new RuntimeException("unable to read file " + englishFile, e);
}

Comment on lines +65 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

English-bundle resolution is convention-based; ensure it matches all target layouts

verifySafeHtml() derives the English source file via:

  • Replacing resources-community with resources.
  • Rewriting _[a-zA-Z-_]*.properties to _en.properties.

This works for the tests and for conventional <base>_<locale>.properties layouts, but will throw a RuntimeException if the computed English file is missing or moved. If the plugin may run over files that don’t follow this convention (or where English is not _en), consider either:

  • Guarding the lookup and falling back to “no HTML allowed” when the English bundle is absent, or
  • Making the naming convention configurable and failing with a more explicit MojoExecutionException.

🏁 Script executed:

#!/bin/bash
# List all *.properties files this module normally verifies to check that
# each non-en file has a matching *_en.properties alongside it.

fd '\.properties$' -t f |
  gawk '
    /_([A-Za-z-]+)\.properties$/ {
      base=$0; sub(/_[A-Za-z-]+\.properties$/, "", base);
      en=base "_en.properties";
      if (system("[ -f \"" en "\" ]") != 0) {
        print "Missing English counterpart for: " $0 " (expected " en ")"
      }
    }
  '

Length of output: 36611


Fix English-bundle lookup to handle non-standard naming conventions

The verification script confirms that verifySafeHtml() will fail with RuntimeException for a significant number of properties files across the codebase. The regex-based English filename resolution produces non-existent paths for:

  • Compound locale files: messages_pt_BR.properties → expects messages_pt_en.properties (missing)
  • Regional variants: messages_de_AT.properties → expects messages_de_en.properties (missing)
  • Language-only files: en_US.properties → expects en_en.properties (missing)

Over 130+ localized properties files in themes/, js/apps/, and testsuite/ directories do not follow the assumed _en.properties naming pattern. At minimum, add a fallback:

try (FileInputStream fis = new FileInputStream(englishFile)) {
    bundleEnglish = new PropertyResourceBundle(fis);
} catch (IOException e) {
    // Fall back to current bundle if English variant is unavailable
    bundleEnglish = bundle;
}

Or make the convention configurable and fail explicitly with MojoExecutionException rather than a generic RuntimeException.

🤖 Prompt for AI Agents
In
misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java
around lines 65 to 81, the current regex-based lookup for the English bundle
produces non-existent paths for many locale variants and throws a
RuntimeException; replace the strict resolution with a resilient strategy:
attempt to open the computed englishFile as before, but on IOException fall back
to using the original bundle (bundleEnglish = bundle) so verification can
proceed when an _en.properties variant is absent, or alternatively make the
english file pattern configurable and throw a MojoExecutionException with a
clear message if you prefer failing explicitly; implement the fallback in the
catch block (or switch to the configurable approach) and ensure exceptions
include the file path in their message.

bundle.getKeys().asIterator().forEachRemaining(key -> {
String value = bundle.getString(key);
value = normalizeValue(key, value);
String englishValue = getEnglishValue(key, bundleEnglish);
englishValue = normalizeValue(key, englishValue);

value = santizeAnchors(key, value, englishValue);

// Only if the English source string contains HTML we also allow HTML in the translation
PolicyFactory policy = containsHtml(englishValue) ? POLICY_SOME_HTML : POLICY_NO_HTML;
String sanitized = policy.sanitize(value);

// Sanitizer will escape HTML entities for quotes and also for numberic tags like '<1>'
sanitized = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(sanitized);
// Sanitizer will add them when there are double curly braces
sanitized = sanitized.replace("<!-- -->", "");

if (!Objects.equals(sanitized, value)) {

// Strip identical characters from the beginning and the end to show where the difference is
int start = 0;
while (start < sanitized.length() && start < value.length() && value.charAt(start) == sanitized.charAt(start)) {
start++;
}
int end = 0;
while (end < sanitized.length() && end < value.length() && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) {
end++;
}

messages.add("Illegal HTML in key " + key + " for file " + file + ": '" + value.substring(start, value.length() - end) + "' vs. '" + sanitized.substring(start, sanitized.length() - end) + "'");
}

});
}

private String normalizeValue(String key, String value) {
if (key.equals("templateHelp")) {
// Allow "CLAIM.<NAME>" here
value = value.replaceAll("CLAIM\\.<[A-Z]*>", "");
} else if (key.equals("optimizeLookupHelp")) {
// Allow "<Extensions>" here
value = value.replaceAll("<Extensions>", "");
} else if (key.startsWith("linkExpirationFormatter.timePeriodUnit") || key.equals("error-invalid-multivalued-size")) {
// The problem is the "<" that appears in the choice
value = value.replaceAll("\\{[0-9]+,choice,[^}]*}", "...");
}

// Unescape HTML entities, as we later also unescape HTML entities in the sanitized value
value = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(value);

if (file.getAbsolutePath().contains("email")) {
// TODO: move the RTL information for emails
value = value.replaceAll(Pattern.quote(" style=\"direction: rtl;\""), "");
}
return value;
}

Pattern HTML_TAGS = Pattern.compile("<[a-z]+[^>]*>");

private boolean containsHtml(String englishValue) {
return HTML_TAGS.matcher(englishValue).find();
}

private static final Pattern ANCHOR_PATTERN = Pattern.compile("</?a[^>]*>");

/**
* Allow only those anchor tags from the source key to also appear in the target key.
*/
private String santizeAnchors(String key, String value, String englishValue) {
Matcher matcher = ANCHOR_PATTERN.matcher(value);
Matcher englishMatcher = ANCHOR_PATTERN.matcher(englishValue);
while (matcher.find()) {
if (englishMatcher.find() && Objects.equals(matcher.group(), englishMatcher.group())) {
value = value.replaceFirst(Pattern.quote(englishMatcher.group()), "");
} else {
messages.add("Didn't find anchor tag " + matcher.group() + " in original string");
break;
}
}
return value;
}
Comment on lines +145 to +162

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Anchor sanitization logic works; improve naming and diagnostics

The santizeAnchors method correctly:

  • Walks anchor tags in the translation and English source.
  • Flags any extra or changed anchors ("Didn't find anchor tag … in original string").
  • Removes matching anchors from the value before sanitizer comparison.

Two minor polish suggestions:

  • Rename santizeAnchorssanitizeAnchors for readability.
  • Include key and/or file path in the “Didn’t find anchor tag” message (similar to Illegal HTML in key … for file …) to make it easier to track down the offending entry in large bundles.

Behavior need not change; this is about debuggability.


🌐 Web query:

Check OWASP Java HTML Sanitizer and similar tools for any built-in patterns for safe anchor handling, to see if your custom anchor comparison aligns with common practices.

💡 Result:

Short answer: Yes — OWASP Java HTML Sanitizer provides built-in handling for safe anchors (via its LINKS sanitizer and HtmlPolicyBuilder allowUrlProtocols/requireRelNofollow options), and common practices used by OWASP and other sanitizers match the usual checks your custom anchor comparison should do: whitelist schemes, normalize/percent-decode and compare schemes/hosts, block dangerous schemes (javascript:, data:), and add rel="nofollow"/rel="noopener" for external/target=_blank links. See sources below for the exact APIs and behaviour.

Key points to align your comparator with common practice:

  • Whitelist allowed URL protocols (e.g., http, https, mailto); OWASP exposes allowUrlProtocols(...) and Sanitizers.LINKS for that. [1]
  • Normalize and percent-decode before comparing (case-insensitive scheme, canonical host) so comparisons aren’t bypassed by encoding or case tricks. (OWASP examples show using URL/protocol checks when allowing hrefs.) [1][2]
  • Explicitly block or strip dangerous schemes like javascript:, data:, vbscript:. DOMPurify and OWASP both validate URI schemes. [3][1]
  • Treat relative URLs as allowed (or resolve them to a base) and compare resolved origins when you mean “same-origin.” Many policies allow relative hrefs by default when protocols are safe. [1][3]
  • Add/require rel="nofollow" and for target="_blank" ensure rel includes noopener,noreferrer to avoid window.opener attacks; OWASP has requireRelNofollowOnLinks() and examples of adding rel. [1][2]

Sources:
[1] OWASP Java HTML Sanitizer — project page and examples (HtmlPolicyBuilder, Sanitizers.LINKS, allowUrlProtocols, requireRelNofollow).
[2] Baeldung / examples using OWASP sanitizer (shows LINKS behavior and rel handling).
[3] DOMPurify docs — URI-scheme validation and blocking javascript: / data: schemes.

If you want, paste your custom anchor-comparison code and I’ll check it line-by-line against these practices.


I need to understand the actual purpose of this method within the codebase to determine if the OWASP best practices are relevant. Let me check the usage context.


🏁 Script executed:

#!/bin/bash
# Find method calls to santizeAnchors to understand its usage context
rg -nP '\bsantizeAnchors\s*\(' misc/theme-verifier/src/main/java/org/keycloak/themeverifier/ -A 3 -B 3

Length of output: 2058


🏁 Script executed:

#!/bin/bash
# Check class-level documentation and overall structure
cat misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java | head -100

Length of output: 3871


🏁 Script executed:

#!/bin/bash
# Search for other sanitization methods in the file to understand scope
rg -nP 'private.*String.*\(' misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java -A 2

Length of output: 534


Fix typo in method name and enhance error diagnostics

The santizeAnchors method correctly validates that anchor tags in translations match the English source structure. The actual HTML security is handled by the downstream OWASP PolicyFactory.sanitize() call, so OWASP URI/scheme validation does not apply to this method's scope.

Two fixes remain valid:

  • Rename santizeAnchorssanitizeAnchors (typo).
  • Include key in the "Didn't find anchor tag" message (line 157) for consistency with other error messages like "Illegal HTML in key … for file …", improving debuggability in large translation bundles.
🤖 Prompt for AI Agents
In
misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java
around lines 145-162, rename the method from santizeAnchors to sanitizeAnchors
(fix the typo) and update its diagnostic message so that when an anchor tag is
missing it includes the message key (e.g., "Didn't find anchor tag <...> in
original string for key <key>") to match other errors; also update any call
sites to use the new method name. Ensure only the method name and the message
string are changed, preserving existing logic and return behavior.


private static String getEnglishValue(String key, PropertyResourceBundle bundleEnglish) {
String englishValue;
try {
englishValue = bundleEnglish.getString(key);
} catch (MissingResourceException ex) {
englishValue = "";
}
return englishValue;
}

private void verifyNoDuplicateKeys(String contents) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new StringReader(contents));
String line;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,26 @@ class VerifyMessagePropertiesTest {

@Test
void verifyDuplicateKeysDetected() throws MojoExecutionException {
List<String> verify = getFile("duplicate_keys.properties").verify();
MatcherAssert.assertThat(verify, Matchers.contains(Matchers.containsString("Duplicate keys in file")));
List<String> verify = getFile("duplicateKeys_en.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Duplicate keys in file")));
}

@Test
void verifyIllegalHtmlTagDetected() throws MojoExecutionException {
List<String> verify = getFile("illegalHtmlTag_en.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Illegal HTML")));
}

@Test
void verifyNoHtmlAllowed() throws MojoExecutionException {
List<String> verify = getFile("noHtml_de.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Illegal HTML")));
}

@Test
void verifyNoChangedAnchors() throws MojoExecutionException {
List<String> verify = getFile("changedAnchor_de.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Didn't find anchor tag")));
}

private static VerifyMessageProperties getFile(String fixture) {
Expand Down
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/changedAnchor_de.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <a href="http://malicious.com">link</a>
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/changedAnchor_en.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <a href="http://example.com">link</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <div>tag</div
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/noHtml_de.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <b>HTML</b>
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/noHtml_en.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=No HTML
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ revoke=Kumoa oikeudet

configureAuthenticators=Konfiguroitu kaksivaiheinen kirjautuminen
mobile=Mobiili
totpStep1=Asenna <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> tai Google Authenticator ohjelma laiteellesi. Kummatkin sovellukset ovat saatavilla <a href="https://play.google.com">Google Play</a> ja Apple App Store kaupoissa.
totpStep1=Asenna jokin seuraavista sovelluksista matkapuhelimeesi:
totpStep2=Avaa sovellus ja skannaa QR-koodi tai kirjoita avain.
totpStep3=Täytä saamasi kertaluontoinen koodisi allaolevaan kenttään ja paina Tallenna.
totpStep3DeviceName=Anna laitteelle nimi, jotta voit hallinnoida OTP-laitteitasi.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ revoke=Atšaukti įgaliojimą

configureAuthenticators=Sukonfigūruotas autentifikatorius
mobile=Mobilus
totpStep1=Įdiekite <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> arba Google Authenticator savo įrenginyje. Programėlės prieinamos <a href="https://play.google.com">Google Play</a> ir Apple App Store.
totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix language mismatch in Lithuanian localization. Line 101 contains Italian text ("Installa una delle seguenti applicazioni sul tuo cellulare:") instead of Lithuanian. This breaks the Lithuanian localization for TOTP setup. Correct this to the appropriate Lithuanian translation to match the file's language and align with the intent shown in lines 102–103.

Verify the correct Lithuanian translation for the TOTP setup instruction. The current value should be replaced with a Lithuanian translation (e.g., something similar to the Norwegian or Swedish equivalents in other files).


I'll verify the review comment by examining the actual file and comparing translations across language files.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and verify the Lithuanian properties file
find . -name "messages_lt.properties" -type f

Length of output: 555


🏁 Script executed:

#!/bin/bash
# Extract the content around line 101 from the Lithuanian file
sed -n '98,105p' "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties"

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Find all messages_*.properties files to compare translations
find themes/src/main/resources-community/theme/base/account/messages -name "messages_*.properties" -type f | sort

Length of output: 2618


🏁 Script executed:

#!/bin/bash
# Extract the totpStep1 line from multiple language files to compare
for file in $(find themes/src/main/resources-community/theme/base/account/messages -name "messages_*.properties" -type f | sort); do
  echo "=== $(basename $file) ==="
  grep -n "^totpStep1=" "$file" || echo "NOT FOUND"
done

Length of output: 2988


🏁 Script executed:

#!/bin/bash
# Check other Lithuanian message files for the totpStep1 key to see if there's a correct translation elsewhere
for file in $(find . -name "messages_lt.properties" -type f); do
  echo "=== $(echo $file | sed 's|.*/||') ==="
  grep -n "totpStep1=" "$file" || echo "NOT FOUND"
done

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Check if there are any other Lithuanian files with TOTP-related content
for file in $(find . -name "*_lt.properties" -o -name "*_lt*.properties" 2>/dev/null | grep -i totp); do
  echo "=== $(echo $file | sed 's|.*/||') ===" 
  grep -i "totp" "$file" | head -5
done

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any Italian content in Lithuanian files that shouldn't be there
grep -l "Installa una delle seguenti" themes/src/main/resources-community/theme/base/**/*_lt.properties 2>/dev/null

Length of output: 0


Fix language mismatch in Lithuanian localization at line 101 of themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties. The value contains Italian text ("Installa una delle seguenti applicazioni sul tuo cellulare:") instead of a Lithuanian translation. This breaks the Lithuanian TOTP setup workflow. Replace with the correct Lithuanian translation (lines 102–103 are properly Lithuanian and can serve as a reference for the expected language).

🤖 Prompt for AI Agents
In
themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties
around line 101, the value for totpStep1 is in Italian; replace "Installa una
delle seguenti applicazioni sul tuo cellulare:" with the proper Lithuanian
translation — e.g. "Įdiekite vieną iš šių programėlių savo telefone:" — save the
file with UTF-8 encoding and run the i18n/l10n checks to ensure the key now
matches the Lithuanian locale and renders correctly.

totpStep2=Atidarykite programėlę ir nuskenuokite barkodą arba įveskite kodą.
totpStep3=Įveskite programėlėje sugeneruotą vieną kartą galiojantį kodą ir paspauskite Saugoti norėdami prisijungti.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ revoke=Zrušiť oprávnenie

configureAuthenticators=Nakonfigurované autentifikátory
mobile=Mobilný
totpStep1=Nainštalujte vo svojom zariadení <a href="https://freeotp.github.io/" target="_blank"> FreeOTP </a> alebo Google Authenticator. Obidve aplikácie sú k dispozícii v <a href="https://play.google.com"> Google Play </a> a Apple App Store.
totpStep1=Nainštalujte si do mobilu jednu z nasledujúcich aplikácií:
totpStep2=Otvorte aplikáciu a naskenujte čiarový kód alebo zadajte kľúč.
totpStep3=Zadajte jednorazový kód poskytnutý aplikáciou a kliknutím na tlačidlo Uložiť dokončíte nastavenie.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ revoke=Upphäv rättighet

configureAuthenticators=Konfigurerade autentiserare
mobile=Mobil
totpStep1=Installera <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> eller Google Authenticator på din enhet. Båda applikationerna finns tillgängliga på <a href="https://play.google.com">Google Play</a> och Apple App Store.
totpStep1=Installera en av följande applikationer på din mobil:
totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ revoke=收回授权

configureAuthenticators=配置的认证者
mobile=手机
totpStep1=在你的设备上安装 <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> 或者 Google Authenticator.两个应用可以从 <a href="https://play.google.com">Google Play</a> 和 Apple App Store下载。
totpStep1=在您的手機上安裝以下應用程式之一:
totpStep2=打开应用扫描二维码输入验证码
totpStep3=输入应用提供的一次性验证码单击保存

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ emailTestBody=Dette er en test besked
emailTestBodyHtml=<p>Dette er en test besked</p>
identityProviderLinkSubject=Link {0}
identityProviderLinkBody=Nogen vil forbinde din "{1}" konto med "{0}" kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti\n\n{3}\n\nDette link vil udløbe efter {5}.\n\nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.
identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.
identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>
passwordResetSubject=Gendan adgangskode
passwordResetBody=Nogen har forsøgt at nulstille adgangskoden til {2}. Hvis dette var dig, bedes du klikke på linket herunder for at nulstille adgangskoden.\n\n{0}\n\nDette link og kode vil udløbe efter {3}.\n\nHvis du ikke ønsker at nulstille din adgangskode, kan du se bort fra denne besked.
passwordResetBodyHtml=<p>Nogen har forsøgt at nulstille adgangskoden til {2}. Hvis dette var dig, bedes du klikke på linket herunder for at nulstille adgangskoden.</p><p><a href="{0}">Nulstil adgangskode</a></p><p>Dette link og kode vil udløbe efter {3}.</p><p>Hvis du ikke ønsker at nulstille din adgangskode, kan du se bort fra denne besked.</p>
Expand Down
Loading