Skip to content
Open
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using DCL.UI;
using DCL.Utility;
using DCL.Web3;
using System;
Expand Down Expand Up @@ -84,19 +85,12 @@ private static string FormatUnits(BigInteger amount)
return fraction.Length == 0 ? whole.ToString() : $"{whole}.{fraction}";
}

// The copy is rich text by design, so a display name carrying "<size=0>" would hide the warning it
// sits in. The link id is escaped as an attribute because a '"' there would close it early.
private static string Highlight(string value) =>
$"<color={HIGHLIGHT_COLOR}>{EscapeRichText(value)}</color>";
$"<color={HIGHLIGHT_COLOR}>{RichTextSanitizer.Escape(value)}</color>";

private static string HighlightLink(string id, string label) =>
$"<link=\"{EscapeRichText(id)}\"><color={HIGHLIGHT_COLOR}><b>{EscapeRichText(label)}</b></color></link>";

/// <summary>
/// Swaps the characters TMP reads as markup for lookalikes that it does not. The copy is rich
/// text by design, so a display name carrying "&lt;size=0&gt;" would hide the warning it sits in.
/// </summary>
private static string EscapeRichText(string value) =>
value.Replace('<', '‹') // single left-pointing angle quotation mark
.Replace('>', '›') // single right-pointing angle quotation mark
.Replace('"', '”'); // right double quotation mark, closes the link attribute
$"<link=\"{RichTextSanitizer.EscapeAttribute(id)}\"><color={HIGHLIGHT_COLOR}><b>{RichTextSanitizer.Escape(label)}</b></color></link>";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
4 changes: 3 additions & 1 deletion Explorer/Assets/DCL/Chat/ChatEntryUsernameElement.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using DCL.UI;
using JetBrains.Annotations;
using System;
using TMPro;
Expand Down Expand Up @@ -29,7 +30,8 @@ private void UserNameClickDetected()

public void SetUsername(string username, string? walletId, bool isOfficial)
{
userName.text = username;
// The label's richText is off in the prefab, so another user's name only needs bounding here.
userName.text = RichTextSanitizer.Truncate(username, RichTextSanitizer.DEFAULT_NAME_LENGTH);
walletIdText.text = walletId;

bool hasWalletId = !string.IsNullOrEmpty(walletId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,11 @@ private void OnGoToStreamButtonClicked()
public void SetCommunityData(string id, string title, string owner, string description, bool isMember)
{
CommunityId = id;
communityTitle.text = title;
// Title, owner and description are all written by whoever owns the community.
communityTitle.text = RichTextSanitizer.EscapeAndTruncate(title, RichTextSanitizer.DEFAULT_NAME_LENGTH);
currentCommunityName = title;
Comment thread
mikhail-dcl marked this conversation as resolved.
communityOwner.text = owner;
communityDescription.text = description;
communityOwner.text = RichTextSanitizer.EscapeAndTruncate(owner, RichTextSanitizer.DEFAULT_NAME_LENGTH);
communityDescription.text = RichTextSanitizer.Truncate(description, RichTextSanitizer.DEFAULT_BODY_LENGTH);
this.isMember = isMember;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3791,7 +3791,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DCL.Communities.CommunitiesDataProvider.DTOs;
using DCL.Diagnostics;
using DCL.FeatureFlags;
using DCL.UI;
using DCL.UI.ConfirmationDialog.Opener;
using DCL.UI.ProfileElements;
using DCL.UI.Profiles.Helpers;
Expand Down Expand Up @@ -82,8 +83,14 @@ public void Configure(CommunityPost announcementInfo, ProfileRepositoryWrapper p
{
currentAnnouncementId = announcementInfo.id;

announcementContent.text = announcementInfo.content;
authorName.text = announcementInfo.Profile.Name;
// Capped but not escaped: this label's richText is off in the prefab, so markup in the body is
// already inert and escaping would only mangle honest prose like "5 < 10".
announcementContent.text = RichTextSanitizer.Truncate(announcementInfo.content, RichTextSanitizer.DEFAULT_BODY_LENGTH);

// The author name renders through a label shared with other panels that has to stay rich text, so
// escaping is what keeps an author-chosen name from being read as markup. ValidatedName rather than
// Name because it keeps only alphanumerics, which removes the escape sequences too.
authorName.text = RichTextSanitizer.EscapeAndTruncate(announcementInfo.Profile.ValidatedName, RichTextSanitizer.DEFAULT_NAME_LENGTH);
profileTag.text = $"#{announcementInfo.authorAddress[^4..]}";
profileTag.gameObject.SetActive(!announcementInfo.Profile.HasClaimedName);
verifiedMark.SetActive(announcementInfo.Profile.HasClaimedName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ async UniTask ShowDeleteInvitationConfirmationDialogAsync(CancellationToken ct)
elementsSpacing: contextMenuSettings.ElementsSpacing,
anchorPoint: ContextMenuOpenDirection.BottomLeft)
.AddControl(communityNotificationsContextMenuElement = new GenericContextMenuElement(
communityNotificationsContextMenuControlSettings = new ToggleWithIconContextMenuControlSettings(contextMenuSettings.CommunityNotificationsSprite, contextMenuSettings.CommunityNotificationsText, OnToggleCommunityNotifications, null, 10)))
communityNotificationsContextMenuControlSettings = new ToggleWithIconContextMenuControlSettings(contextMenuSettings.CommunityNotificationsSprite, contextMenuSettings.CommunityNotificationsText, OnToggleCommunityNotifications, horizontalLayoutSpacing: 10)))
.AddControl(communityNotificationsSeparatorContextMenuElement = new GenericContextMenuElement(
new SeparatorContextMenuControlSettings(contextMenuSettings.CommunityNotificationsSeparatorHeight, -contextMenuSettings.VerticalPadding.left, -contextMenuSettings.VerticalPadding.right)))
.AddControl(copyLinkContextMenuElement = new GenericContextMenuElement(
Expand Down Expand Up @@ -391,9 +391,12 @@ public void UpdateMemberCount(GetCommunityResponse.CommunityData communityData)
public void ConfigureCommunity(GetCommunityResponse.CommunityData communityData,
ThumbnailLoader thumbnailLoader)
{
communityName.text = communityData.name;
// Name and description are written by whoever owns the community. The description label's richText is
// off in the prefab so it only needs a cap; the name is escaped as well, since a bracket is never
// legitimate in one and its label is shared with panels this view does not control.
communityName.text = RichTextSanitizer.EscapeAndTruncate(communityData.name, RichTextSanitizer.DEFAULT_NAME_LENGTH);
UpdateMemberCount(communityData);
communityDescription.text = communityData.description;
communityDescription.text = RichTextSanitizer.Truncate(communityData.description, RichTextSanitizer.DEFAULT_BODY_LENGTH);
communityPrivacyText.text = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(communityData.privacy.ToString());
UnlistedMark.SetActive(communityData.visibility == CommunityVisibility.unlisted);
UnlistedSeparator.SetActive(communityData.visibility == CommunityVisibility.unlisted);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DCL.Communities.CommunitiesDataProvider.DTOs;
using DCL.UI;
using DCL.UI.ProfileElements;
using DCL.UI.Profiles.Helpers;
using System;
Expand Down Expand Up @@ -103,7 +104,9 @@ public void Configure(ICommunityMemberData memberProfile, MembersListView.Member

Color userColor = memberProfile.GetUserNameColor();

userName.text = memberProfile.Name;
// Rich text is off on this label and the colour below is applied as a property rather than as
// markup, so the escape is belt-and-braces; the cap bounds the layout pass.
userName.text = RichTextSanitizer.EscapeAndTruncate(memberProfile.Name, RichTextSanitizer.DEFAULT_NAME_LENGTH);
userName.color = userColor;
userNameTag.text = $"#{memberProfile.Address[^4..]}";
userNameTag.gameObject.SetActive(!memberProfile.HasClaimedName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5218,7 +5218,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3767,7 +3767,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down Expand Up @@ -1823,7 +1823,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ protected override void OnViewInstantiated()
protected override void OnViewShow()
{
if (inputData.Uri == null)
{
// Refused by the http(s) scheme policy (SEC-008). Blank the fields so the previous prompt's
// destination is not left on screen, and drop the callback so the buttons cannot re-approve it.
resultCallback = null;
viewInstance!.DomainText.text = string.Empty;
viewInstance.UrlText.text = string.Empty;
return;
}

Uri uri = inputData.Uri;

if (ExternalUrlPolicy.TryGetTrustKey(uri, out string trustKey) && trustedKeys.Contains(trustKey))
if (IsTrusted(uri))
{
webBrowser.OpenUrlMainThreadOnly(uri.OriginalString);
viewInstance!.CloseButton.OnClickAsync(CancellationToken.None).Forget();
Expand All @@ -67,13 +74,13 @@ protected override void OnViewShow()

protected override UniTask WaitForCloseIntentAsync(CancellationToken ct)
{
if (inputData.Uri != null
&& ExternalUrlPolicy.TryGetTrustKey(inputData.Uri, out string trustKey)
&& trustedKeys.Contains(trustKey))
// Nothing left to consent to: the URL was refused by the scheme policy, or its (scheme, host) is
// already trusted and was opened in OnViewShow. Either way, close instead of showing the dialog.
if (inputData.Uri == null || IsTrusted(inputData.Uri))
return UniTask.CompletedTask;

return UniTask.WhenAny(
viewInstance.CloseButton.OnClickAsync(ct),
viewInstance!.CloseButton.OnClickAsync(ct),
viewInstance.CancelButton.OnClickAsync(ct),
viewInstance.ContinueButton.OnClickAsync(ct));
}
Expand All @@ -83,11 +90,18 @@ public override void Dispose()
trustedKeys.Clear();
}

private bool IsTrusted(Uri uri) =>
ExternalUrlPolicy.TryGetTrustKey(uri, out string trustKey) && trustedKeys.Contains(trustKey);

private void RequestOpenUrl(Uri uri, Action<ExternalUrlPromptResultType> result)
{
resultCallback = result;
viewInstance!.DomainText.text = uri.Host;
viewInstance.UrlText.text = uri.OriginalString;

// AbsoluteUri, not OriginalString: it is the canonical form UnityAppWebBrowser hands to
// Application.OpenURL, so the user consents to exactly the string that gets opened, and its
// percent-escaping is a second barrier against markup smuggled into the raw URL (SEC-008).
viewInstance.UrlText.text = uri.AbsoluteUri;
viewInstance.TrustToggle.isOn = false;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using DCL.FeatureFlags;
using DCL.Profiles;
using DCL.UI;
using DCL.UI.Profiles.Helpers;
using DCL.UI.ProfileElements;
using System;
Expand All @@ -15,17 +16,17 @@ public class FriendPanelUserView : MonoBehaviour, IPointerEnterHandler, IPointer
{
protected readonly List<Button> buttons = new ();

[field: SerializeField] public Image Background { get; private set; }
[field: SerializeField] public Image Background { get; private set; } = null!;
[field: SerializeField] public Color NormalColor { get; private set; }
[field: SerializeField] public Color HoveredColor { get; private set; }
[field: SerializeField] public Button MainButton { get; private set; }
[field: SerializeField] public Button MainButton { get; private set; } = null!;

[field: Header("User")]
[field: SerializeField] public TMP_Text UserName { get; private set; }
[field: SerializeField] public TMP_Text UserNameTag { get; private set; }
[field: SerializeField] public GameObject VerifiedIcon { get; private set; }
[field: SerializeField] public GameObject OfficialIcon { get; private set; }
[field: SerializeField] public ProfilePictureView ProfilePicture { get; private set; }
[field: SerializeField] public TMP_Text UserName { get; private set; } = null!;
[field: SerializeField] public TMP_Text UserNameTag { get; private set; } = null!;
[field: SerializeField] public GameObject VerifiedIcon { get; private set; } = null!;
[field: SerializeField] public GameObject OfficialIcon { get; private set; } = null!;
[field: SerializeField] public ProfilePictureView ProfilePicture { get; private set; } = null!;

private bool canUnHover = true;

Expand Down Expand Up @@ -71,7 +72,7 @@ public virtual void Configure(Profile.CompactInfo friendProfile, ProfileReposito

Color userColor = friendProfile.UserNameColor;

UserName.text = friendProfile.Name;
UserName.text = RichTextSanitizer.EscapeAndTruncate(friendProfile.ValidatedNameOrRaw, RichTextSanitizer.DEFAULT_NAME_LENGTH);
UserName.color = userColor;
UserNameTag.text = friendProfile.WalletId;
UserNameTag.gameObject.SetActive(!friendProfile.HasClaimedName);
Expand Down
4 changes: 2 additions & 2 deletions Explorer/Assets/DCL/Friends/UI/Prefabs/NameContainer.prefab
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down Expand Up @@ -330,7 +330,7 @@ MonoBehaviour:
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_isRichText: 0
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DCL.Audio;
using DCL.FeatureFlags;
using DCL.Profiles;
using DCL.UI;
using DCL.UI.Profiles.Helpers;
using DCL.UI.ProfileElements;
using DG.Tweening;
Expand All @@ -14,12 +15,12 @@ namespace DCL.Friends.UI.PushNotifications
{
public class FriendPushNotificationView : ViewBase, IView
{
[field: SerializeField] public ProfilePictureView ProfilePictureView { get; private set; }
[field: SerializeField] public TMP_Text UserNameText { get; private set; }
[field: SerializeField] public TMP_Text UserAddressText { get; private set; }
[field: SerializeField] public GameObject VerifiedIcon { get; private set; }
[field: SerializeField] public GameObject OfficialIcon { get; private set; }
[field: SerializeField] public CanvasGroup PanelCanvasGroup { get; private set; }
[field: SerializeField] public ProfilePictureView ProfilePictureView { get; private set; } = null!;
[field: SerializeField] public TMP_Text UserNameText { get; private set; } = null!;
[field: SerializeField] public TMP_Text UserAddressText { get; private set; } = null!;
[field: SerializeField] public GameObject VerifiedIcon { get; private set; } = null!;
[field: SerializeField] public GameObject OfficialIcon { get; private set; } = null!;
[field: SerializeField] public CanvasGroup PanelCanvasGroup { get; private set; } = null!;

[field:Header("Toast Animation")]
[field: SerializeField] public float toastFadeInDuration = 0.3f;
Expand All @@ -43,7 +44,7 @@ internal void ConfigureForFriend(Profile.CompactInfo friendProfile, ProfileRepos
{
Color userColor = friendProfile.UserNameColor;
UserNameText.color = userColor;
UserNameText.text = friendProfile.Name;
UserNameText.text = RichTextSanitizer.EscapeAndTruncate(friendProfile.ValidatedNameOrRaw, RichTextSanitizer.DEFAULT_NAME_LENGTH);
UserAddressText.text = $"#{friendProfile.Address.ToString()[^4..]}";
UserAddressText.gameObject.SetActive(!friendProfile.HasClaimedName);
VerifiedIcon.SetActive(friendProfile.HasClaimedName);
Expand Down
Loading
Loading