diff --git a/Session/Signal/LinkPreviewView.swift b/Session/Signal/LinkPreviewView.swift
index 4a95fd4ed..d8b3b0abe 100644
--- a/Session/Signal/LinkPreviewView.swift
+++ b/Session/Signal/LinkPreviewView.swift
@@ -657,7 +657,7 @@ public class LinkPreviewView: UIStackView {
cancelButton.tintColor = Theme.secondaryColor
cancelButton.setContentHuggingHigh()
cancelButton.setCompressionResistanceHigh()
- cancelButton.isHidden = true
+ cancelButton.isHidden = false
cancelStack.addArrangedSubview(cancelButton)
rightStack.addArrangedSubview(cancelStack)
diff --git a/SessionMessagingKit/Sending & Receiving/Link Previews/HTMLMetadata.swift b/SessionMessagingKit/Sending & Receiving/Link Previews/HTMLMetadata.swift
new file mode 100644
index 000000000..5037faaa9
--- /dev/null
+++ b/SessionMessagingKit/Sending & Receiving/Link Previews/HTMLMetadata.swift
@@ -0,0 +1,119 @@
+import Foundation
+
+public struct HTMLMetadata: Equatable {
+ /// Parsed from
+ var titleTag: String?
+ /// Parsed from
+ var faviconUrlString: String?
+ /// Parsed from
+ var description: String?
+ /// Parsed from the og:title meta property
+ var ogTitle: String?
+ /// Parsed from the og:description meta property
+ var ogDescription: String?
+ /// Parsed from the og:image or og:image:url meta property
+ var ogImageUrlString: String?
+ /// Parsed from the og:published_time meta property
+ var ogPublishDateString: String?
+ /// Parsed from article:published_time meta property
+ var articlePublishDateString: String?
+ /// Parsed from the og:modified_time meta property
+ var ogModifiedDateString: String?
+ /// Parsed from the article:modified_time meta property
+ var articleModifiedDateString: String?
+
+ static func construct(parsing rawHTML: String) -> HTMLMetadata {
+ let metaPropertyTags = Self.parseMetaProperties(in: rawHTML)
+ return HTMLMetadata(
+ titleTag: Self.parseTitleTag(in: rawHTML),
+ faviconUrlString: Self.parseFaviconUrlString(in: rawHTML),
+ description: Self.parseDescriptionTag(in: rawHTML),
+ ogTitle: metaPropertyTags["og:title"],
+ ogDescription: metaPropertyTags["og:description"],
+ ogImageUrlString: (metaPropertyTags["og:image"] ?? metaPropertyTags["og:image:url"]),
+ ogPublishDateString: metaPropertyTags["og:published_time"],
+ articlePublishDateString: metaPropertyTags["article:published_time"],
+ ogModifiedDateString: metaPropertyTags["og:modified_time"],
+ articleModifiedDateString: metaPropertyTags["article:modified_time"]
+ )
+ }
+}
+
+// MARK: - Parsing
+extension HTMLMetadata {
+
+ private static func parseTitleTag(in rawHTML: String) -> String? {
+ titleRegex
+ .firstMatchSet(in: rawHTML)?
+ .group(idx: 0)
+ .flatMap { decodeHTMLEntities(in: String($0)) }
+ }
+
+ private static func parseFaviconUrlString(in rawHTML: String) -> String? {
+ guard let matchedTag = faviconRegex
+ .firstMatchSet(in: rawHTML)
+ .map({ String($0.fullString) }) else { return nil }
+
+ return faviconUrlRegex
+ .parseFirstMatch(inText: matchedTag)
+ .flatMap { decodeHTMLEntities(in: String($0)) }
+ }
+
+ private static func parseDescriptionTag(in rawHTML: String) -> String? {
+ guard let matchedTag = metaDescriptionRegex
+ .firstMatchSet(in: rawHTML)
+ .map({ String($0.fullString) }) else { return nil }
+
+ return metaContentRegex
+ .parseFirstMatch(inText: matchedTag)
+ .flatMap { decodeHTMLEntities(in: String($0)) }
+ }
+
+ private static func parseMetaProperties(in rawHTML: String) -> [String: String] {
+ metaPropertyRegex
+ .allMatchSets(in: rawHTML)
+ .reduce(into: [:]) { (builder, matchSet) in
+ guard let ogTypeSubstring = matchSet.group(idx: 0) else { return }
+ let ogType = String(ogTypeSubstring)
+ let fullTag = String(matchSet.fullString)
+
+ // Exit early if we've already found a tag of this type
+ guard builder[ogType] == nil else { return }
+ guard let content = metaContentRegex.parseFirstMatch(inText: fullTag) else { return }
+
+ builder[ogType] = decodeHTMLEntities(in: content)
+ }
+ }
+
+ private static func decodeHTMLEntities(in string: String) -> String? {
+ guard let data = string.data(using: .utf8) else {
+ return nil
+ }
+
+ let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
+ .documentType: NSAttributedString.DocumentType.html,
+ .characterEncoding: String.Encoding.utf8.rawValue
+ ]
+
+ guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else {
+ return nil
+ }
+ return attributedString.string
+ }
+}
+
+ // MARK: - Regular Expressions
+extension HTMLMetadata {
+ static let titleRegex = regex(pattern: "<\\s*title[^>]*>(.*?)<\\s*/title[^>]*>")
+ static let faviconRegex = regex(pattern: "<\\s*link[^>]*rel\\s*=\\s*\"\\s*(shortcut\\s+)?icon\\s*\"[^>]*>")
+ static let faviconUrlRegex = regex(pattern: "href\\s*=\\s*\"([^\"]*)\"")
+ static let metaDescriptionRegex = regex(pattern: "<\\s*meta[^>]*name\\s*=\\s*\"\\s*description[^\"]*\"[^>]*>")
+ static let metaPropertyRegex = regex(pattern: "<\\s*meta[^>]*property\\s*=\\s*\"\\s*([^\"]+?)\"[^>]*>")
+ static let metaContentRegex = regex(pattern: "content\\s*=\\s*\"([^\"]*?)\"")
+
+ static private func regex(pattern: String) -> NSRegularExpression {
+ try! NSRegularExpression(
+ pattern: pattern,
+ options: [.dotMatchesLineSeparators, .caseInsensitive])
+ }
+}
diff --git a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift
index f3e1f697a..0d3f81f54 100644
--- a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift
+++ b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift
@@ -277,83 +277,6 @@ public class OWSLinkPreview: MTLModel {
return result.filterStringForDisplay()
}
- // MARK: - Whitelists
-
- // For link domains, we require an exact match - no subdomains allowed.
- //
- // Note that order matters in this whitelist since the logic for determining
- // how to render link preview domains in displayDomain(...) uses the first match.
- // We should list TLDs first and subdomains later.
- private static let linkDomainWhitelist = [
- // YouTube
- "youtube.com",
- "www.youtube.com",
- "m.youtube.com",
- "youtu.be",
-
- // Reddit
- "reddit.com",
- "www.reddit.com",
- "m.reddit.com",
- // NOTE: We don't use redd.it.
-
- // Imgur
- //
- // NOTE: Subdomains are also used for content.
- //
- // For example, you can access "user/member" pages: https://sillygoose2.imgur.com/
- // A different member page can be accessed without a subdomain: https://imgur.com/user/SillyGoose2
- //
- // I'm not sure we need to support these subdomains; they don't appear to be core functionality.
- "imgur.com",
- "www.imgur.com",
- "m.imgur.com",
-
- // Instagram
- "instagram.com",
- "www.instagram.com",
- "m.instagram.com",
-
- // Pinterest
- "pinterest.com",
- "www.pinterest.com",
- "pin.it",
-
- // Giphy
- "giphy.com",
- "media.giphy.com",
- "media1.giphy.com",
- "media2.giphy.com",
- "media3.giphy.com",
- "gph.is"
- ]
-
- // For media domains, we DO NOT require an exact match - subdomains are allowed.
- private static let mediaDomainWhitelist = [
- // YouTube
- "ytimg.com",
-
- // Reddit
- "redd.it",
-
- // Imgur
- "imgur.com",
-
- // Instagram
- "cdninstagram.com",
- "fbcdn.net",
-
- // Pinterest
- "pinimg.com",
-
- // Giphy
- "giphy.com"
- ]
-
- private static let protocolWhitelist = [
- "https"
- ]
-
@objc
public func displayDomain() -> String? {
return OWSLinkPreview.displayDomain(forUrl: urlString)
@@ -367,12 +290,7 @@ public class OWSLinkPreview: MTLModel {
guard let url = URL(string: urlString) else {
return nil
}
- guard let result = whitelistedDomain(forUrl: url,
- domainWhitelist: OWSLinkPreview.linkDomainWhitelist,
- allowSubdomains: false) else {
- return nil
- }
- return result
+ return url.host
}
@objc
@@ -380,9 +298,7 @@ public class OWSLinkPreview: MTLModel {
guard let url = URL(string: urlString) else {
return false
}
- return whitelistedDomain(forUrl: url,
- domainWhitelist: OWSLinkPreview.linkDomainWhitelist,
- allowSubdomains: false) != nil
+ return true
}
@objc
@@ -390,36 +306,7 @@ public class OWSLinkPreview: MTLModel {
guard let url = URL(string: urlString) else {
return false
}
- return whitelistedDomain(forUrl: url,
- domainWhitelist: OWSLinkPreview.mediaDomainWhitelist,
- allowSubdomains: true) != nil
- }
-
- private class func whitelistedDomain(forUrl url: URL, domainWhitelist: [String], allowSubdomains: Bool) -> String? {
- guard let urlProtocol = url.scheme?.lowercased() else {
- return nil
- }
- guard protocolWhitelist.contains(urlProtocol) else {
- return nil
- }
- guard let domain = url.host?.lowercased() else {
- return nil
- }
- guard url.path.count > 1 else {
- // URL must have non-empty path.
- return nil
- }
-
- for whitelistedDomain in domainWhitelist {
- if domain == whitelistedDomain.lowercased() {
- return whitelistedDomain
- }
- if allowSubdomains,
- domain.hasSuffix("." + whitelistedDomain.lowercased()) {
- return whitelistedDomain
- }
- }
- return nil
+ return true
}
// MARK: - Serial Queue
@@ -577,8 +464,8 @@ public class OWSLinkPreview: MTLModel {
return Promise.value(cachedInfo)
}
return downloadLink(url: previewUrl)
- .then(on: DispatchQueue.global()) { (data) -> Promise in
- return parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl)
+ .then(on: DispatchQueue.global()) { (data, response) -> Promise in
+ return parseLinkDataAndBuildDraft(linkData: data, response: response, linkUrlString: previewUrl)
}.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise in
guard linkPreviewDraft.isValid() else {
throw LinkPreviewError.noPreview
@@ -588,9 +475,17 @@ public class OWSLinkPreview: MTLModel {
return Promise.value(linkPreviewDraft)
}
}
+
+ // Twitter doesn't return OpenGraph tags to Signal
+ // `curl -A Signal "https://twitter.com/signalapp/status/1280166087577997312?s=20"`
+ // If this ever changes, we can switch back to our default User-Agent
+ private static let userAgentString = "WhatsApp"
class func downloadLink(url urlString: String,
- remainingRetries: UInt = 3) -> Promise {
+ remainingRetries: UInt = 3) -> Promise<(Data, URLResponse)> {
+
+ Logger.verbose("url: \(urlString)")
+
// let sessionConfiguration = ContentProxy.sessionConfiguration() // Loki: Signal's proxy appears to have been banned by YouTube
let sessionConfiguration = URLSessionConfiguration.ephemeral
@@ -606,8 +501,10 @@ public class OWSLinkPreview: MTLModel {
guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else {
return Promise(error: LinkPreviewError.assertionFailure)
}
+
+ sessionManager.requestSerializer.setValue(self.userAgentString, forHTTPHeaderField: "User-Agent")
- let (promise, resolver) = Promise.pending()
+ let (promise, resolver) = Promise<(Data, URLResponse)>.pending()
sessionManager.get(urlString,
parameters: [String: AnyObject](),
headers: nil,
@@ -632,7 +529,7 @@ public class OWSLinkPreview: MTLModel {
resolver.reject(LinkPreviewError.invalidContent)
return
}
- resolver.fulfill(data)
+ resolver.fulfill((data, response))
},
failure: { _, error in
guard isRetryable(error: error) else {
@@ -645,8 +542,8 @@ public class OWSLinkPreview: MTLModel {
return
}
OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1)
- .done(on: DispatchQueue.global()) { (data) in
- resolver.fulfill(data)
+ .done(on: DispatchQueue.global()) { (data, response) in
+ resolver.fulfill((data, response))
}.catch(on: DispatchQueue.global()) { (error) in
resolver.reject(error)
}.retainUntilComplete()
@@ -670,7 +567,7 @@ public class OWSLinkPreview: MTLModel {
resolver.fulfill(asset)
}, failure: { (_) in
resolver.reject(LinkPreviewError.couldNotDownload)
- })
+ }, shouldIgnoreSignalProxy: true)
}
return promise.then(on: DispatchQueue.global()) { (asset: ProxiedContentAsset) -> Promise in
do {
@@ -719,9 +616,10 @@ public class OWSLinkPreview: MTLModel {
}
class func parseLinkDataAndBuildDraft(linkData: Data,
+ response: URLResponse,
linkUrlString: String) -> Promise {
do {
- let contents = try parse(linkData: linkData)
+ let contents = try parse(linkData: linkData, response: response)
let title = contents.title
guard let imageUrl = contents.imageUrl else {
@@ -752,28 +650,26 @@ public class OWSLinkPreview: MTLModel {
}
}
- // Example:
- //
- //
- //
- class func parse(linkData: Data) throws -> OWSLinkPreviewContents {
- guard let linkText = String(bytes: linkData, encoding: .utf8) else {
+ class func parse(linkData: Data, response: URLResponse) throws -> OWSLinkPreviewContents {
+ guard let linkText = String(data: linkData, urlResponse: response) else {
+ print("Could not parse link text.")
throw LinkPreviewError.invalidInput
}
+
+ let content = HTMLMetadata.construct(parsing: linkText)
var title: String?
- if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "]*content\\s*=\\s*\"(.*?)\"\\s*[^>]*/?>",
- text: linkText,
- options: .dotMatchesLineSeparators) {
- if let decodedTitle = decodeHTMLEntities(inString: rawTitle) {
- let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
- if normalizedTitle.count > 0 {
- title = normalizedTitle
- }
+ let rawTitle = content.ogTitle ?? content.titleTag
+ if let decodedTitle = decodeHTMLEntities(inString: rawTitle ?? "") {
+ let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
+ if normalizedTitle.count > 0 {
+ title = normalizedTitle
}
}
- guard let rawImageUrlString = NSRegularExpression.parseFirstMatch(pattern: "]*content\\s*=\\s*\"(.*?)\"[^>]*/?>", text: linkText) else {
+ Logger.verbose("title: \(String(describing: title))")
+
+ guard let rawImageUrlString = content.ogImageUrlString ?? content.faviconUrlString else {
return OWSLinkPreviewContents(title: title)
}
guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else {
@@ -790,7 +686,8 @@ public class OWSLinkPreview: MTLModel {
let imageFilename = imageUrl.lastPathComponent
let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased()
guard imageFileExtension.count > 0 else {
- return nil
+ // TODO: For those links don't have a file extension, we should figure out a way to know the image mime type
+ return "png"
}
return imageFileExtension
}
diff --git a/SessionUtilitiesKit/NSRegularExpression+SSK.swift b/SessionUtilitiesKit/NSRegularExpression+SSK.swift
index 0704a86cb..c8eb33d21 100644
--- a/SessionUtilitiesKit/NSRegularExpression+SSK.swift
+++ b/SessionUtilitiesKit/NSRegularExpression+SSK.swift
@@ -5,23 +5,19 @@
import Foundation
@objc
-public extension NSRegularExpression {
+extension NSRegularExpression {
@objc
- func hasMatch(input: String) -> Bool {
+ public func hasMatch(input: String) -> Bool {
return self.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) != nil
}
@objc
- class func parseFirstMatch(pattern: String,
- text: String,
- options: NSRegularExpression.Options = []) -> String? {
+ public class func parseFirstMatch(pattern: String, text: String, options: NSRegularExpression.Options = []) -> String? {
do {
let regex = try NSRegularExpression(pattern: pattern, options: options)
- guard let match = regex.firstMatch(in: text,
- options: [],
- range: NSRange(location: 0, length: text.utf16.count)) else {
- return nil
+ guard let match = regex.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) else {
+ return nil
}
let matchRange = match.range(at: 1)
guard let textRange = Range(matchRange, in: text) else {
@@ -35,8 +31,7 @@ public extension NSRegularExpression {
}
@objc
- func parseFirstMatch(inText text: String,
- options: NSRegularExpression.Options = []) -> String? {
+ public func parseFirstMatch(inText text: String, options: NSRegularExpression.Options = []) -> String? {
guard let match = self.firstMatch(in: text,
options: [],
range: NSRange(location: 0, length: text.utf16.count)) else {
@@ -49,4 +44,58 @@ public extension NSRegularExpression {
let substring = String(text[textRange])
return substring
}
+
+ @nonobjc
+ public func firstMatchSet(in searchString: String) -> MatchSet? {
+ firstMatch(in: searchString, options: [], range: searchString.completeNSRange)?.createMatchSet(originalSearchString: searchString)
+ }
+
+ @nonobjc
+ public func allMatchSets(in searchString: String) -> [MatchSet] {
+ matches(in: searchString, options: [], range: searchString.completeNSRange).compactMap { $0.createMatchSet(originalSearchString: searchString) }
+ }
+
+}
+
+public struct MatchSet {
+ public let fullString: Substring
+ public let matchedGroups: [Substring?]
+
+ public func group(idx: Int) -> Substring? {
+ guard idx < matchedGroups.count else { return nil }
+ return matchedGroups[idx]
+ }
}
+
+extension String {
+
+ public subscript(_ nsRange: NSRange) -> Substring? {
+ guard let swiftRange = Range(nsRange, in: self) else { return nil }
+ return self[swiftRange]
+ }
+
+ public var completeRange: Range {
+ startIndex.. MatchSet? {
+ guard numberOfRanges > 0 else { return nil }
+ let substrings = (0.. Void)?
private var failure: ((ProxiedContentAssetRequest) -> Void)?
-
+
+ var shouldIgnoreSignalProxy = false
var wasCancelled = false
// This property is an internal implementation detail of the download process.
var assetFilePath: String?
@@ -438,6 +439,19 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
delegateQueue: nil)
return session
}()
+
+ private lazy var downloadSessionWithoutProxy: URLSession = {
+ let configuration = URLSessionConfiguration.ephemeral
+ // Don't use any caching to protect privacy of these requests.
+ configuration.urlCache = nil
+ configuration.requestCachePolicy = .reloadIgnoringCacheData
+
+ configuration.httpMaximumConnectionsPerHost = 10
+ let session = URLSession(configuration: configuration,
+ delegate: self,
+ delegateQueue: nil)
+ return session
+ }()
// 100 entries of which at least half will probably be stills.
// Actual animated GIFs will usually be less than 3 MB so the
@@ -458,7 +472,8 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
public func requestAsset(assetDescription: ProxiedContentAssetDescription,
priority: ProxiedContentRequestPriority,
success:@escaping ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void),
- failure:@escaping ((ProxiedContentAssetRequest) -> Void)) -> ProxiedContentAssetRequest? {
+ failure:@escaping ((ProxiedContentAssetRequest) -> Void),
+ shouldIgnoreSignalProxy: Bool = false) -> ProxiedContentAssetRequest? {
if let asset = assetMap.get(key: assetDescription.url) {
// Synchronous cache hit.
success(nil, asset)
@@ -472,6 +487,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
priority: priority,
success: success,
failure: failure)
+ assetRequest.shouldIgnoreSignalProxy = shouldIgnoreSignalProxy
assetRequestQueue.append(assetRequest)
// Process the queue (which may start this request)
// asynchronously so that the caller has time to store
@@ -614,10 +630,17 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
processRequestQueueSync()
return
}
-
- let task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in
- self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error)
- })
+
+ var task: URLSessionDataTask
+ if (assetRequest.shouldIgnoreSignalProxy) {
+ task = downloadSessionWithoutProxy.dataTask(with: request, completionHandler: { data, response, error -> Void in
+ self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error)
+ })
+ } else {
+ task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in
+ self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error)
+ })
+ }
assetRequest.contentLengthTask = task
task.resume()
@@ -625,6 +648,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
// Start a download task.
guard let assetSegment = assetRequest.firstWaitingSegment() else {
+ print("queued asset request does not have a waiting segment.")
return
}
assetSegment.state = .downloading
@@ -641,7 +665,12 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
return
}
- let task: URLSessionDataTask = downloadSession.dataTask(with: request)
+ var task: URLSessionDataTask
+ if (assetRequest.shouldIgnoreSignalProxy) {
+ task = downloadSessionWithoutProxy.dataTask(with: request)
+ } else {
+ task = downloadSession.dataTask(with: request)
+ }
task.assetRequest = assetRequest
task.assetSegment = assetSegment
assetSegment.task = task
@@ -660,11 +689,13 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
}
guard let data = data,
data.count > 0 else {
+ print("Asset size response missing data.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest: assetRequest)
return
}
guard let httpResponse = response as? HTTPURLResponse else {
+ print("Asset size response is invalid.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest: assetRequest)
return
@@ -672,6 +703,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
var firstContentRangeString: String?
for header in httpResponse.allHeaderFields.keys {
guard let headerString = header as? String else {
+ print("Invalid header: \(header)")
continue
}
if headerString.lowercased() == "content-range" {
@@ -679,6 +711,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
}
}
guard let contentRangeString = firstContentRangeString else {
+ print("Asset size response is missing content range.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest: assetRequest)
return
@@ -693,11 +726,13 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
}
guard contentLengthString.count > 0,
let contentLength = Int(contentLengthString) else {
+ print("Asset size response has unparsable content length.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest: assetRequest)
return
}
guard contentLength > 0 else {
+ print("Asset size response has invalid content length.")
assetRequest.state = .failed
self.assetRequestDidFail(assetRequest: assetRequest)
return
diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj
index 258d07ee6..0ab4cf7e2 100644
--- a/Signal.xcodeproj/project.pbxproj
+++ b/Signal.xcodeproj/project.pbxproj
@@ -283,6 +283,7 @@
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */; };
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
+ B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; };
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; };
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; };
@@ -1392,6 +1393,7 @@
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; };
B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairMigrationSuccessSheet.swift; sourceTree = ""; };
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = ""; };
+ B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = ""; };
B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; };
B8BB829F238F322400BA5194 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; };
B8BB82A1238F356100BA5194 /* Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = ""; };
@@ -2896,6 +2898,7 @@
C32C5D22256DD496003C73A2 /* Link Previews */ = {
isa = PBXGroup;
children = (
+ B8B320B6258C30D70020074B /* HTMLMetadata.swift */,
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */,
B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */,
);
@@ -5228,6 +5231,7 @@
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
B8856D34256F1192001CE70E /* Environment.m in Sources */,
+ B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */,
C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */,
C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */,
C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,