Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Text shared via the ShareExtension is sanitized and added to a new draft #1371

Merged
merged 5 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions .package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/swift-concurrency",
"state" : {
"revision" : "1c2e7ee0bb93203c865904494b0aa2aedbc57713",
"version" : "0.0.4"
"revision" : "02960fd5d2cf57c7ba38d13bbbf580d7f6ac7102",
"version" : "0.0.5"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions Mail/Views/New Message/Attachments/AttachmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ import SwiftUI
await worker.completeUploadedAttachments()
}

func processTextAttachments(_ attachments: [TextAttachable]) async {
await worker.processTextAttachments(attachments)
func processHtmlAttachments(_ attachments: [HTMLAttachable]) async {
valentinperignon marked this conversation as resolved.
Show resolved Hide resolved
await worker.processHtmlAttachments(attachments)
}

func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask {
Expand Down
4 changes: 2 additions & 2 deletions Mail/Views/New Message/ComposeMessageIntentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct ComposeMessageIntentView: View, IntentViewable {
}

let composeMessageIntent: ComposeMessageIntent
var textAttachments: [TextAttachable] = []
var htmlAttachments: [HTMLAttachable] = []
var attachments: [Attachable] = []

var body: some View {
Expand All @@ -50,7 +50,7 @@ struct ComposeMessageIntentView: View, IntentViewable {
mailboxManager: resolvedIntent.mailboxManager,
messageReply: resolvedIntent.messageReply,
attachments: attachments,
textAttachments: textAttachments
htmlAttachments: htmlAttachments
)
.environmentObject(resolvedIntent.mailboxManager)
} else {
Expand Down
8 changes: 4 additions & 4 deletions Mail/Views/New Message/ComposeMessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct ComposeMessageView: View {
private let messageReply: MessageReply?
private let draftContentManager: DraftContentManager
private let mailboxManager: MailboxManager
private let textAttachments: [TextAttachable]
private let htmlAttachments: [HTMLAttachable]

private var isSendButtonDisabled: Bool {
let disabledState = draft.identityId == nil
Expand All @@ -118,10 +118,10 @@ struct ComposeMessageView: View {
mailboxManager: MailboxManager,
messageReply: MessageReply? = nil,
attachments: [Attachable] = [],
textAttachments: [TextAttachable] = []
htmlAttachments: [HTMLAttachable] = []
) {
self.messageReply = messageReply
self.textAttachments = textAttachments
self.htmlAttachments = htmlAttachments

_draft = ObservedRealmObject(wrappedValue: draft)

Expand Down Expand Up @@ -223,7 +223,7 @@ struct ComposeMessageView: View {
currentSignature = try await draftContentManager.prepareCompleteDraft()

async let _ = attachmentsManager.completeUploadedAttachments()
async let _ = attachmentsManager.processTextAttachments(textAttachments)
async let _ = attachmentsManager.processHtmlAttachments(htmlAttachments)

isLoadingContent = false
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,22 +370,14 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable {
await updateDelegate?.contentWillChange()
}

public func processTextAttachments(_ attachments: [TextAttachable]) async {
// Process all text attachments
let textAttachments = await attachments.concurrentMap { attachment in
await attachment.textAttachment
}

public func processHtmlAttachments(_ htmlAttachments: [HTMLAttachable]) async {
adrien-coye marked this conversation as resolved.
Show resolved Hide resolved
// Get first usable title
let anyUsableTitle = anyUsableTitle(in: textAttachments)

// Get all URLs
let allURLs = allURLs(in: textAttachments)
let anyUsableTitle = await anyUsableTitle(in: htmlAttachments)

// Render all URLs as HTML code, if any after a minimalistic input sanitising
let formattedBodyUrls = formattedBodyUrls(allURLs: allURLs)
// Get all the sanitized HTML we can fetch
let allSanitizedHtmlString = await allSanitizedHtml(in: htmlAttachments).reduce("", +)

// mutate Draft
// Mutate Draft
await backgroundRealm.execute { realm in
try? realm.write {
guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: self.draftLocalUUID) else {
Expand All @@ -400,8 +392,8 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable {
modified = true
}

if !formattedBodyUrls.isEmpty {
draftInContext.body = formattedBodyUrls + draftInContext.body
if !allSanitizedHtmlString.isEmpty {
draftInContext.body = allSanitizedHtmlString + draftInContext.body
modified = true
}

Expand All @@ -414,34 +406,26 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable {
}
}

private func anyUsableTitle(in textAttachments: [TextAttachment]) -> String {
textAttachments.first { $0.title?.isEmpty == false }?.title ?? ""
private func anyUsableTitle(in textAttachments: [TextAttachable]) async -> String {
let textAttachments = await textAttachments.asyncMap { attachment in
await attachment.textAttachment
}

let title = textAttachments.first { $0.title?.isEmpty == false }?.title ?? ""
return title
}

private func allURLs(in textAttachments: [TextAttachment]) -> [String] {
textAttachments.compactMap { attachment in
guard let body = attachment.body,
!body.isEmpty else {
private func allSanitizedHtml(in htmlAttachments: [HTMLAttachable]) async -> [String] {
let allSanitizedHtml: [String] = await htmlAttachments.asyncCompactMap { attachment in
guard let renderedHTML = await attachment.renderedHTML,
!renderedHTML.isEmpty else {
return nil
}

return body
return renderedHTML
}
}

private func formattedBodyUrls(allURLs: [String]) -> String {
allURLs.reduce("") { partialResult, urlString in
guard let bodyUrl = URL(string: urlString) else {
return partialResult
}

let bodyAbsoluteUrl = bodyUrl.absoluteString
guard !bodyAbsoluteUrl.isEmpty else {
return partialResult
}

return partialResult + "<div><a href=\"\(bodyAbsoluteUrl)\">" + bodyAbsoluteUrl + "</a></div>"
}
return allSanitizedHtml
}

@MainActor public func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask {
Expand Down
8 changes: 8 additions & 0 deletions MailCore/Cache/Attachments/TextAttachable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public protocol TextAttachable {
var textAttachment: TextAttachment { get async }
}

/// Something that can be rendered into HTML that can be added to a Mail Draft safely.
///
/// Inherits from `TextAttachable`
public protocol HTMLAttachable: TextAttachable {
/// Provides a sanitised HTML that can be added to a Mail body.
var renderedHTML: String? { get async }
}

extension NSItemProvider: TextAttachable {
static let nilAttachment: TextAttachment = (nil, nil)

Expand Down
55 changes: 55 additions & 0 deletions MailShareExtension/Attachment/SafariKeyValueToHTMLAttachment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Infomaniak Mail - iOS App
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import MailCore

struct SafariKeyValueToHTMLAttachment: HTMLAttachable {
let item: TextAttachable

init(wrapping item: TextAttachable) {
self.item = item
}

// MARK: TextAttachable protocol

var textAttachment: MailCore.TextAttachment {
get async {
await item.textAttachment
}
}

// MARK: HTMLAttachable protocol

var renderedHTML: String? {
get async {
guard let urlString = await textAttachment.body,
let bodyUrl = URL(string: urlString) else {
return nil
}

let bodyAbsoluteUrl = bodyUrl.absoluteString
guard !bodyAbsoluteUrl.isEmpty else {
return nil
}

let finalHTML = "<div class=\"renderedHTML\"><a href=\"\(bodyAbsoluteUrl)\">" + bodyAbsoluteUrl + "</a></div>"
return finalHTML
}
}
}
73 changes: 73 additions & 0 deletions MailShareExtension/Attachment/TxtToHTMLAttachment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Infomaniak Mail - iOS App
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import MailCore

/// A wrapping type that can read an NSItemProvider that renders as a`.txt` on the fly and provide the content thanks to the
/// `TextAttachable` protocol
struct TxtToTextAttachment: HTMLAttachable {
let item: NSItemProvider

init?(wrapping item: NSItemProvider) {
guard item.underlyingType == .isText else {
return nil
}

self.item = item
}

// MARK: TextAttachable protocol

var textAttachment: MailCore.TextAttachment {
get async {
guard let textAttachment = try? await item.writeToTemporaryURL() else {
return (nil, nil)
}

guard let textData = NSData(contentsOf: textAttachment.url) else {
return (nil, nil)
}

let textString = String(decoding: textData, as: UTF8.self)

/// The `txt` file name is generated, so not useful for an email subject, discarding it
return TextAttachment(title: nil, body: textString)
}
}

// MARK: HTMLAttachable protocol

var renderedHTML: String? {
get async {
guard let textString = await textAttachment.body else {
return nil
}

/// Minimalist HTML sanitisation and support, non HTML text will work too.
guard let document = try? SwiftSoupUtils(fromHTMLFragment: textString),
let cleanDocument = try? await document.cleanBody(),
let cleanHTML = try? cleanDocument.outerHtml() else {
return nil
}

let finalHTML = "<div class=\"renderedHTML\">" + cleanHTML + "</div>"
return finalHTML
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import MailCore

/// A wrapping type that can read an NSItemProvider that renders as a`.webloc` on the fly and provide the content thanks to the
/// `TextAttachable` protocol
struct WeblocToTextAttachment: TextAttachable {
struct WeblocToTextAttachment: HTMLAttachable {
let item: NSItemProvider

init?(wrapping item: NSItemProvider) {
Expand All @@ -40,20 +40,39 @@ struct WeblocToTextAttachment: TextAttachable {
return (nil, nil)
}

guard let weblocData = NSData(contentsOf: webloc.url) else {
guard let weblocData = try? Data(contentsOf: webloc.url) else {
return (nil, nil)
}

guard let parsedWebloc = try? PropertyListSerialization.propertyList(from: weblocData as Data,
guard let parsedWebloc = try? PropertyListSerialization.propertyList(from: weblocData,
options: [],
format: nil) as? NSDictionary else {
return (nil, nil)
}

let parsedURL = parsedWebloc["URL"] as? String

/// The `webloc` title is not useful for an email subject, discarding it
/// The `webloc` file name is generated, so not useful for an email subject, discarding it
return TextAttachment(title: nil, body: parsedURL)
}
}

// MARK: HTMLAttachable protocol

var renderedHTML: String? {
get async {
guard let urlString = await textAttachment.body,
let bodyUrl = URL(string: urlString) else {
return nil
}

let bodyAbsoluteUrl = bodyUrl.absoluteString
guard !bodyAbsoluteUrl.isEmpty else {
return nil
}

let finalHTML = "<div class=\"renderedHTML\"><a href=\"\(bodyAbsoluteUrl)\">" + bodyAbsoluteUrl + "</a></div>"
adrien-coye marked this conversation as resolved.
Show resolved Hide resolved
return finalHTML
}
}
}
Loading
Loading