Elaborate the link preview tests.

pull/1/head
Matthew Chen 6 years ago
parent 4e1098475b
commit 2e9f2e6154

@ -17,6 +17,21 @@ public enum LinkPreviewError: Int, Error {
// MARK: - OWSLinkPreviewDraft
public class OWSLinkPreviewContents: NSObject {
@objc
public var title: String?
@objc
public var imageUrl: String?
public init(title: String?, imageUrl: String? = nil) {
self.title = title
self.imageUrl = imageUrl
super.init()
}
}
// This contains the info for a link preview "draft".
public class OWSLinkPreviewDraft: NSObject {
@objc
@ -310,10 +325,6 @@ public class OWSLinkPreview: MTLModel {
owsFailDebug("Invalid url.")
return nil
}
guard url.path.count > 0 else {
owsFailDebug("Invalid url (empty path).")
return nil
}
guard let result = whitelistedDomain(forUrl: url,
domainWhitelist: OWSLinkPreview.linkDomainWhitelist) else {
owsFailDebug("Missing domain.")
@ -360,13 +371,11 @@ public class OWSLinkPreview: MTLModel {
guard let domain = url.host?.lowercased() else {
return nil
}
// TODO: We need to verify:
//
// * The final domain whitelist.
// * The relationship between the "link" whitelist and the "media" whitelist.
// * Exact match or suffix-based?
// * Case-insensitive?
// * Protocol?
guard url.path.count > 1 else {
// URL must have non-empty path.
return nil
}
for whitelistedDomain in domainWhitelist {
if domain == whitelistedDomain.lowercased() ||
domain.hasSuffix("." + whitelistedDomain.lowercased()) {
@ -491,7 +500,7 @@ public class OWSLinkPreview: MTLModel {
}
return downloadLink(url: previewUrl)
.then(on: DispatchQueue.global()) { (data) -> Promise<OWSLinkPreviewDraft> in
return parse(linkData: data, linkUrlString: previewUrl)
return parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl)
.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise<OWSLinkPreviewDraft> in
guard linkPreviewDraft.isValid() else {
return Promise(error: LinkPreviewError.noPreview)
@ -629,17 +638,73 @@ public class OWSLinkPreview: MTLModel {
return false
}
class func parseLinkDataAndBuildDraft(linkData: Data,
linkUrlString: String) -> Promise<OWSLinkPreviewDraft> {
do {
let contents = try parse(linkData: linkData)
let title = contents.title
guard let imageUrl = contents.imageUrl else {
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
guard isValidMediaUrl(imageUrl) else {
Logger.error("Invalid image URL.")
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
guard let imageFileExtension = fileExtension(forImageUrl: imageUrl) else {
Logger.error("Image URL has unknown or invalid file extension: \(imageUrl).")
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
guard let imageMimeType = mimetype(forImageFileExtension: imageFileExtension) else {
Logger.error("Image URL has unknown or invalid content type: \(imageUrl).")
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
return downloadImage(url: imageUrl, imageMimeType: imageMimeType)
.then(on: DispatchQueue.global()) { (imageData: Data) -> Promise<OWSLinkPreviewDraft> in
let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: imageFileExtension)
do {
try imageData.write(to: NSURL.fileURL(withPath: imageFilePath), options: .atomicWrite)
} catch let error as NSError {
owsFailDebug("file write failed: \(imageFilePath), \(error)")
return Promise(error: LinkPreviewError.assertionFailure)
}
// NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...).
let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType)
let kMaxImageSize: CGFloat = 2048
guard imageSize.width > 0,
imageSize.height > 0,
imageSize.width < kMaxImageSize,
imageSize.height < kMaxImageSize else {
Logger.error("Image has invalid size: \(imageSize).")
return Promise(error: LinkPreviewError.assertionFailure)
}
let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, imageFilePath: imageFilePath)
return Promise.value(linkPreviewDraft)
}
.recover(on: DispatchQueue.global()) { (_) -> Promise<OWSLinkPreviewDraft> in
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
} catch {
owsFailDebug("Could not parse link data: \(error).")
return Promise(error: error)
}
}
// Example:
//
// <meta property="og:title" content="Randomness is Random - Numberphile">
// <meta property="og:image" content="https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg">
private class func parse(linkData: Data,
linkUrlString: String) -> Promise<OWSLinkPreviewDraft> {
class func parse(linkData: Data) throws -> OWSLinkPreviewContents {
guard let linkText = String(bytes: linkData, encoding: .utf8) else {
owsFailDebug("Could not parse link text.")
return Promise(error: LinkPreviewError.invalidInput)
throw LinkPreviewError.invalidInput
}
Logger.verbose("linkText: \(linkText)")
var title: String?
if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property\\s*=\\s*\"og:title\"\\s+content\\s*=\\s*\"(.*?)\"\\s*/?>", text: linkText) {
if let decodedTitle = decodeHTMLEntities(inString: rawTitle) {
@ -653,63 +718,32 @@ public class OWSLinkPreview: MTLModel {
Logger.verbose("title: \(String(describing: title))")
guard let rawImageUrlString = NSRegularExpression.parseFirstMatch(pattern: "<meta\\s+property\\s*=\\s*\"og:image\"\\s+content\\s*=\\s*\"(.*?)\"\\s*/?>", text: linkText) else {
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
return OWSLinkPreviewContents(title: title)
}
guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else {
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
guard isValidMediaUrl(imageUrlString) else {
Logger.error("Invalid image URL.")
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
guard let imageFileExtension = fileExtension(forImageUrl: imageUrlString) else {
Logger.error("Image URL has unknown or invalid file extension: \(imageUrlString).")
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
return OWSLinkPreviewContents(title: title)
}
guard let imageMimeType = mimetype(forImageFileExtension: imageFileExtension) else {
Logger.error("Image URL has unknown or invalid content type: \(imageUrlString).")
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
return downloadImage(url: imageUrlString, imageMimeType: imageMimeType)
.then(on: DispatchQueue.global()) { (imageData: Data) -> Promise<OWSLinkPreviewDraft> in
let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: imageFileExtension)
do {
try imageData.write(to: NSURL.fileURL(withPath: imageFilePath), options: .atomicWrite)
} catch let error as NSError {
owsFailDebug("file write failed: \(imageFilePath), \(error)")
return Promise(error: LinkPreviewError.assertionFailure)
}
// NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...).
let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType)
let kMaxImageSize: CGFloat = 2048
guard imageSize.width > 0,
imageSize.height > 0,
imageSize.width < kMaxImageSize,
imageSize.height < kMaxImageSize else {
Logger.error("Image has invalid size: \(imageSize).")
return Promise(error: LinkPreviewError.assertionFailure)
}
let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, imageFilePath: imageFilePath)
return Promise.value(linkPreviewDraft)
}
.recover(on: DispatchQueue.global()) { (_) -> Promise<OWSLinkPreviewDraft> in
return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
}
return OWSLinkPreviewContents(title: title, imageUrl: imageUrlString)
}
private class func fileExtension(forImageUrl urlString: String) -> String? {
class func fileExtension(forImageUrl urlString: String) -> String? {
guard let imageUrl = URL(string: urlString) else {
Logger.error("Could not parse image URL.")
return nil
}
let imageFilename = imageUrl.lastPathComponent
let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased()
guard imageFileExtension.count > 0 else {
return nil
}
return imageFileExtension
}
private class func mimetype(forImageFileExtension imageFileExtension: String) -> String? {
class func mimetype(forImageFileExtension imageFileExtension: String) -> String? {
guard imageFileExtension.count > 0 else {
return nil
}
guard let imageMimeType = MIMETypeUtil.mimeType(forFileExtension: imageFileExtension) else {
Logger.error("Image URL has unknown content type: \(imageFileExtension).")
return nil

@ -3,7 +3,7 @@
//
import Foundation
import SignalServiceKit
@testable import SignalServiceKit
import XCTest
class OWSLinkPreviewTest: SSKBaseTestSwift {
@ -92,7 +92,9 @@ class OWSLinkPreviewTest: SSKBaseTestSwift {
func testIsValidLinkUrl() {
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.youtube.com/watch?v=tP-Ipsat90c"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://youtube.com/watch?v=tP-Ipsat90c"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.youtube.com"))
// Case shouldn't matter.
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://WWW.YOUTUBE.COM/watch?v=tP-Ipsat90c"))
// Allow arbitrary subdomains.
XCTAssertTrue(OWSLinkPreview.isValidMediaUrl("https://some.random.subdomain.youtube.com/watch?v=tP-Ipsat90c"))
@ -112,12 +114,30 @@ class OWSLinkPreviewTest: SSKBaseTestSwift {
// Don't allow media domains.
XCTAssertFalse(OWSLinkPreview.isValidLinkUrl("https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg"))
// Allow all whitelisted domains.
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.youtube.com/watch?v=tP-Ipsat90c"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://youtu.be/tP-Ipsat90c"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.reddit.com/r/androiddev/comments/a7gctz/androidx_release_notes_this_is_the_first_release/"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.reddit.com/r/WhitePeopleTwitter/comments/a7j3mm/why/"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://imgur.com/gallery/KFCL8fm"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://imgur.com/gallery/FMdwTiV"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.instagram.com/p/BrgpsUjF9Jo/?utm_source=ig_web_button_share_sheet"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://www.instagram.com/p/BrgpsUjF9Jo/?utm_source=ig_share_sheet&igshid=94c7ihqjfmbm"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://imgur.com/gallery/igHOwDM"))
// Strip trailing commas.
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://imgur.com/gallery/igHOwDM,"))
// Ignore URLs with an empty path.
XCTAssertFalse(OWSLinkPreview.isValidLinkUrl("https://imgur.com"))
XCTAssertFalse(OWSLinkPreview.isValidLinkUrl("https://imgur.com/"))
XCTAssertTrue(OWSLinkPreview.isValidLinkUrl("https://imgur.com/X"))
}
func testIsValidMediaUrl() {
XCTAssertTrue(OWSLinkPreview.isValidMediaUrl("https://www.youtube.com/watch?v=tP-Ipsat90c"))
XCTAssertTrue(OWSLinkPreview.isValidMediaUrl("https://youtube.com/watch?v=tP-Ipsat90c"))
XCTAssertTrue(OWSLinkPreview.isValidMediaUrl("https://www.youtube.com"))
// Allow arbitrary subdomains.
XCTAssertTrue(OWSLinkPreview.isValidMediaUrl("https://some.random.subdomain.youtube.com/watch?v=tP-Ipsat90c"))
@ -154,4 +174,66 @@ class OWSLinkPreviewTest: SSKBaseTestSwift {
XCTAssertEqual(OWSLinkPreview.previewUrl(forMessageBodyText: "alice bob https://www.youtube.com/watch?v=tP-Ipsat90c jim https://www.youtube.com/watch?v=other-url carol"),
"https://www.youtube.com/watch?v=tP-Ipsat90c")
}
func testUtils() {
XCTAssertNil(OWSLinkPreview.fileExtension(forImageUrl: ""))
XCTAssertNil(OWSLinkPreview.fileExtension(forImageUrl: "https://www.some.host/path/imagename"))
XCTAssertNil(OWSLinkPreview.fileExtension(forImageUrl: "https://www.some.host/path/imagename."))
XCTAssertEqual(OWSLinkPreview.fileExtension(forImageUrl: "https://www.some.host/path/imagename.jpg"), "jpg")
XCTAssertEqual(OWSLinkPreview.fileExtension(forImageUrl: "https://www.some.host/path/imagename.gif"), "gif")
XCTAssertEqual(OWSLinkPreview.fileExtension(forImageUrl: "https://www.some.host/path/imagename.png"), "png")
XCTAssertEqual(OWSLinkPreview.fileExtension(forImageUrl: "https://www.some.host/path/imagename.boink"), "boink")
XCTAssertNil(OWSLinkPreview.mimetype(forImageFileExtension: ""))
XCTAssertNil(OWSLinkPreview.mimetype(forImageFileExtension: "boink"))
XCTAssertNil(OWSLinkPreview.mimetype(forImageFileExtension: "tiff"))
XCTAssertNil(OWSLinkPreview.mimetype(forImageFileExtension: "gif"))
XCTAssertEqual(OWSLinkPreview.mimetype(forImageFileExtension: "jpg"), OWSMimeTypeImageJpeg)
XCTAssertEqual(OWSLinkPreview.mimetype(forImageFileExtension: "png"), OWSMimeTypeImagePng)
}
func testLinkDownloadAndParsing() {
let expectation = self.expectation(description: "link download and parsing")
OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: "https://www.youtube.com/watch?v=tP-Ipsat90c")
.done { (draft) in
XCTAssertNotNil(draft)
XCTAssertEqual(draft.title, "Randomness is Random - Numberphile")
XCTAssertNotNil(draft.imageFilePath)
expectation.fulfill()
}.catch { (error) in
Logger.error("error: \(error)")
XCTFail("Unexpected error: \(error)")
expectation.fulfill()
}.retainUntilComplete()
self.waitForExpectations(timeout: 5.0, handler: nil)
}
func testLinkDataParsing_Empty() {
let linkText = ""
let linkData = linkText.data(using: .utf8)!
let content = try! OWSLinkPreview.parse(linkData: linkData)
XCTAssertNotNil(content)
XCTAssertNil(content.title)
XCTAssertNil(content.imageUrl)
}
func testLinkDataParsing() {
let linkText = ("<meta property=\"og:title\" content=\"Randomness is Random - Numberphile\">" +
"<meta property=\"og:image\" content=\"https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg\">")
let linkData = linkText.data(using: .utf8)!
let content = try! OWSLinkPreview.parse(linkData: linkData)
XCTAssertNotNil(content)
XCTAssertEqual(content.title, "Randomness is Random - Numberphile")
XCTAssertEqual(content.imageUrl, "https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg")
}
}

Loading…
Cancel
Save