Skip to content

Commit

Permalink
#2414 Show links to remote images (#2422)
Browse files Browse the repository at this point in the history
* show remote image link

* fix: added and fixed ui test

* fix: pr reviews

* temp: added web node

* Revert "temp: added web node"

This reverts commit bfd9a69.

* fix: clean up

* fix: web node

* fix: remove time delay when calculating web view height

* fix: keep style attributes

* ui test

* fix: unit test

* feat: added ability to inspect web view

* fix: pr reviews

* fix: pr reviews

* fix: pr reviews
  • Loading branch information
ioanmo226 authored Nov 24, 2023
1 parent 4fb8eff commit 086d22c
Show file tree
Hide file tree
Showing 23 changed files with 485 additions and 17 deletions.
6 changes: 6 additions & 0 deletions Core/source/mobile-interface/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export class Endpoints {
return fmtRes({}, encrypted);
};

public sanitizeHtml = async (uncheckedReq: unknown): Promise<EndpointRes> => {
const { html } = ValidateInput.sanitizeHtml(uncheckedReq);
const sanitizedHtml = Xss.htmlSanitizeKeepBasicTags(html);
return fmtRes({ sanitizedHtml });
};

public parseDecryptMsg = async (uncheckedReq: unknown, data: Buffers): Promise<EndpointRes> => {
const { keys: kisWithPp, msgPwd, isMime, verificationPubkeys } = ValidateInput.parseDecryptMsg(uncheckedReq);
const rawBlocks: MsgBlock[] = []; // contains parsed, unprocessed / possibly encrypted data
Expand Down
10 changes: 10 additions & 0 deletions Core/source/mobile-interface/validate-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export namespace NodeRequest {
export type composeEmail = ComposeEmailPlain | ComposeEmailEncrypted;
export type encryptMsg = { pubKeys: string[]; msgPwd?: string };
export type encryptFile = { pubKeys: string[]; name: string };
export type sanitizeHtml = {
html: string;
};
export type parseDecryptMsg = {
keys: PrvKeyInfo[];
msgPwd?: string;
Expand Down Expand Up @@ -154,6 +157,13 @@ export class ValidateInput {
throw new Error('Wrong request structure for NodeRequest.parseDecryptMsg');
};

public static sanitizeHtml = (v: unknown): NodeRequest.sanitizeHtml => {
if (isObj(v) && hasProp(v, 'html', 'string')) {
return v as NodeRequest.sanitizeHtml;
}
throw new Error('Wrong request structure for NodeRequest.sanitizeHtml');
};

public static encryptFile = (v: unknown): NodeRequest.encryptFile => {
if (isObj(v) && hasProp(v, 'pubKeys', 'string[]') && hasProp(v, 'name', 'string')) {
return v as NodeRequest.encryptFile;
Expand Down
15 changes: 14 additions & 1 deletion Core/source/platform/xss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare const dereq_sanitize_html: (
transformTags?: { [tagName: string]: string | Transformer };
allowedAttributes?: { [tag: string]: string[] };
allowedSchemes?: string[];
allowedStyles?: { [tag: string]: Record<string, string | RegExp[]> };
},
) => string;

Expand Down Expand Up @@ -69,17 +70,28 @@ export class Xss {

// eslint-disable-next-line @typescript-eslint/naming-convention
private static ALLOWED_ATTRS = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'*': ['style'],
a: ['href', 'name', 'target'],
img: ['src', 'width', 'height', 'alt'],
font: ['size', 'color', 'face'],
span: ['color'],
div: ['color'],
p: ['color'],
em: ['style'], // tests rely on this, could potentially remove
td: ['width', 'height'],
hr: ['color', 'height'],
};

// eslint-disable-next-line @typescript-eslint/naming-convention
private static ALLOWED_STYLES = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'*': {
// Existing rules...
// Exclude URLs in background property
background: [/^(?!.*url).+$/],
},
};

// eslint-disable-next-line @typescript-eslint/naming-convention
private static ALLOWED_SCHEMES = ['data', 'http', 'https', 'mailto'];

Expand Down Expand Up @@ -139,6 +151,7 @@ export class Xss {
allowedTags: Xss.ALLOWED_BASIC_TAGS,
allowedAttributes: Xss.ALLOWED_ATTRS,
allowedSchemes: Xss.ALLOWED_SCHEMES,
allowedStyles: Xss.ALLOWED_STYLES,
});
}
cleanHtml = cleanHtml.replace(
Expand Down
8 changes: 8 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
95473C1B297E61DE006C8957 /* SequenceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95473C1A297E61DE006C8957 /* SequenceExtension.swift */; };
9547EF212A5F106E00A048FF /* PassPhraseAlertNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */; };
9547EF242A5FBA2B00A048FF /* MenuSeparatorCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */; };
955475FB2B0650AC00F52076 /* WebNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955475FA2B0650AC00F52076 /* WebNode.swift */; };
955475FD2B06512800F52076 /* ThreadDetailWebNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955475FC2B06512800F52076 /* ThreadDetailWebNode.swift */; };
9577CEDD2AA7A4A40084AC62 /* PublicKeyDetailNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */; };
9582BC5A2A782DA700439728 /* pass_phrase_hint.html in Resources */ = {isa = PBXBuildFile; fileRef = 9582BC592A782DA700439728 /* pass_phrase_hint.html */; };
958566B72A6126DE001C84D3 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; };
Expand Down Expand Up @@ -635,6 +637,8 @@
95473C1A297E61DE006C8957 /* SequenceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceExtension.swift; sourceTree = "<group>"; };
9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseAlertNode.swift; sourceTree = "<group>"; };
9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSeparatorCellNode.swift; sourceTree = "<group>"; };
955475FA2B0650AC00F52076 /* WebNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebNode.swift; sourceTree = "<group>"; };
955475FC2B06512800F52076 /* ThreadDetailWebNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDetailWebNode.swift; sourceTree = "<group>"; };
9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyDetailNode.swift; sourceTree = "<group>"; };
9582BC592A782DA700439728 /* pass_phrase_hint.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = pass_phrase_hint.html; sourceTree = "<group>"; };
958566B82A612822001C84D3 /* ASButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASButtonNode.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2223,6 +2227,7 @@
75DCE69F2869EBC0003435F1 /* EmptyFolderCellNode.swift */,
9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */,
950F27052AB833EA0041595D /* ContactAddNode.swift */,
955475FC2B06512800F52076 /* ThreadDetailWebNode.swift */,
);
path = "Cell Nodes";
sourceTree = "<group>";
Expand Down Expand Up @@ -2252,6 +2257,7 @@
9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */,
95E014CE2A8BF27C00D4B4F5 /* AvatarCheckboxNode.swift */,
9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */,
955475FA2B0650AC00F52076 /* WebNode.swift */,
);
path = Nodes;
sourceTree = "<group>";
Expand Down Expand Up @@ -2871,6 +2877,7 @@
D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */,
5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */,
51C56BE82901867D00610D12 /* ENSideMenu.swift in Sources */,
955475FB2B0650AC00F52076 /* WebNode.swift in Sources */,
D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */,
511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */,
D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */,
Expand Down Expand Up @@ -2906,6 +2913,7 @@
D24ABA6023FDB26C002EE9DD /* Helpers.swift in Sources */,
9577CEDD2AA7A4A40084AC62 /* PublicKeyDetailNode.swift in Sources */,
D211CE7323FC35AC00D1CE38 /* TextViewCellNode.swift in Sources */,
955475FD2B06512800F52076 /* ThreadDetailWebNode.swift in Sources */,
D2A9CA392426197400E1D898 /* SigninButtonNode.swift in Sources */,
D211CE7423FC35AC00D1CE38 /* ButtonCellNode.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource {
else { return self.dividerNode(indexPath: indexPath) }

guard row > 1 else {
if processedMessage.text.isHTMLString() {
return ThreadDetailWebNode(input: .init(message: processedMessage.text, index: messageIndex))
}
return MessageTextSubjectNode(
input: .init(
message: processedMessage.attributedMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ extension ThreadDetailsViewController {
sender: input.rawMessage.sender,
subject: [quoteType.subjectPrefix, subject].joined(),
sentDate: input.rawMessage.date,
text: processedMessage.fullText,
text: processedMessage.fullText.removingHtmlTags() ?? "",
threadId: threadId,
replyToMsgId: replyToMsgId,
inReplyTo: input.rawMessage.inReplyTo,
Expand Down
18 changes: 18 additions & 0 deletions FlowCrypt/Core/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ class Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
).data
}

func sanitizeHtml(html: String) async throws -> String {
struct SanitizeHtmlRaw: Decodable {
let sanitizedHtml: String
}
let params: [String: String] = [
"html": html
]

let parsed = try await call(
"sanitizeHtml",
params: params
)

let res = try parsed.json.decodeJson(as: SanitizeHtmlRaw.self)

return res.sanitizedHtml
}

func parseDecryptMsg(
encrypted: Data,
keys: [Keypair],
Expand Down
4 changes: 4 additions & 0 deletions FlowCrypt/Extensions/UIColorExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public extension UIColor {
static var main: UIColor {
UIColor(r: 36, g: 156, b: 6, alpha: 1)
}

static var plainTextBorder: UIColor {
UIColor(hex: "777777") ?? .systemGray4
}

static var textColor: UIColor {
colorFor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ final class MessageHelper {
messageId: message.identifier
)
guard message.isPgp else {
return ProcessedMessage(message: message, keyDetails: keyDetails)
return try await ProcessedMessage(message: message, keyDetails: keyDetails)
}

return try await decryptAndProcess(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import FlowCryptCommon
import UIKit

struct ProcessedMessage {
Expand Down Expand Up @@ -106,10 +107,15 @@ extension ProcessedMessage {
self.signature = signature
}

init(message: Message, keyDetails: [KeyDetails] = []) {
init(message: Message, keyDetails: [KeyDetails] = []) async throws {
self.message = message
(self.text, self.quote) = Self.parseQuote(text: message.body.text)
self.type = .plain
if let html = message.body.html {
self.text = try await Core.shared.sanitizeHtml(html: html)
self.quote = nil
} else {
(self.text, self.quote) = Self.parseQuote(text: message.body.text)
}
self.attachments = message.attachments
self.signature = .unsigned
self.keyDetails = keyDetails
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Resources/generated/flowcrypt-ios-prod.js.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions FlowCryptCommon/Extensions/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//

import Foundation
import UIKit

public extension String {
var hasContent: Bool {
Expand Down
2 changes: 1 addition & 1 deletion FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public final class MessageTextSubjectNode: CellNode {
if let quote = input.quote {
setupTextNode(quoteNode, text: quote, accessibilityIdentifier: "aid-message-\(input.index)-quote")
}
addLeftBorder(width: .threadLeftBorderWidth, color: input.isEncrypted ? .main : UIColor(hex: "777777"))
addLeftBorder(width: .threadLeftBorderWidth, color: input.isEncrypted ? .main : .plainTextBorder)
}

private func setupTextNode(_ node: ASEditableTextNode, text: NSAttributedString?, accessibilityIdentifier: String) {
Expand Down
68 changes: 68 additions & 0 deletions FlowCryptUI/Cell Nodes/ThreadDetailWebNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// ThreadDetailWebNode.swift
// FlowCryptUI
//
// Created by Ioan Moldoan on 11/16/23
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import AsyncDisplayKit

public final class ThreadDetailWebNode: CellNode {
public struct Input {
let message: String?
let index: Int

public init(message: String?, index: Int) {
self.message = message
self.index = index
}
}

private let input: ThreadDetailWebNode.Input

private lazy var webViewNode: CustomWebViewNode = {
let node = CustomWebViewNode()
node.setAccessibilityIdentifier(accessibilityIdentifier: "aid-message-\(input.index)")
node.setHtml("""
<header>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<style>
* { font-family: -apple-system, "Helvetica Neue", sans-serif; }
:root { color-scheme: light dark; supported-color-schemes: light dark; }
@media (prefers-color-scheme: dark) {
:root {
background-color: #2D2C2E;
color: white;
}
a {
color: #1783FD;
}
}
html, body { padding: 0 !important; margin: 0 !important; }
</style>
</header>
\(input.message ?? "")
""")
node.style.flexGrow = 1.0
return node
}()

public init(input: ThreadDetailWebNode.Input) {
self.input = input

super.init()
addLeftBorder(width: .threadLeftBorderWidth, color: .plainTextBorder)
}

override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec {
let specChild: ASLayoutElement

specChild = webViewNode

return ASInsetLayoutSpec(
insets: .threadMessageInsets,
child: specChild
)
}
}
Loading

0 comments on commit 086d22c

Please sign in to comment.