@ -17,6 +17,21 @@ public enum LinkPreviewError: Int, Error {
// MARK: - O W S L i n k P r e v i e w D r a f t
// MARK: - O W S L i n k P r e v i e w D r a f t
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 ( )
}
}
// T h i s c o n t a i n s t h e i n f o f o r a l i n k p r e v i e w " d r a f t " .
// T h i s c o n t a i n s t h e i n f o f o r a l i n k p r e v i e w " d r a f t " .
public class OWSLinkPreviewDraft : NSObject {
public class OWSLinkPreviewDraft : NSObject {
@objc
@objc
@ -310,10 +325,6 @@ public class OWSLinkPreview: MTLModel {
owsFailDebug ( " Invalid url. " )
owsFailDebug ( " Invalid url. " )
return nil
return nil
}
}
guard url . path . count > 0 else {
owsFailDebug ( " Invalid url (empty path). " )
return nil
}
guard let result = whitelistedDomain ( forUrl : url ,
guard let result = whitelistedDomain ( forUrl : url ,
domainWhitelist : OWSLinkPreview . linkDomainWhitelist ) else {
domainWhitelist : OWSLinkPreview . linkDomainWhitelist ) else {
owsFailDebug ( " Missing domain. " )
owsFailDebug ( " Missing domain. " )
@ -360,13 +371,11 @@ public class OWSLinkPreview: MTLModel {
guard let domain = url . host ? . lowercased ( ) else {
guard let domain = url . host ? . lowercased ( ) else {
return nil
return nil
}
}
// TODO: W e n e e d t o v e r i f y :
guard url . path . count > 1 else {
//
// U R L m u s t h a v e n o n - e m p t y p a t h .
// * T h e f i n a l d o m a i n w h i t e l i s t .
return nil
// * T h e r e l a t i o n s h i p b e t w e e n t h e " l i n k " w h i t e l i s t a n d t h e " m e d i a " w h i t e l i s t .
}
// * E x a c t m a t c h o r s u f f i x - b a s e d ?
// * C a s e - i n s e n s i t i v e ?
// * P r o t o c o l ?
for whitelistedDomain in domainWhitelist {
for whitelistedDomain in domainWhitelist {
if domain = = whitelistedDomain . lowercased ( ) ||
if domain = = whitelistedDomain . lowercased ( ) ||
domain . hasSuffix ( " . " + whitelistedDomain . lowercased ( ) ) {
domain . hasSuffix ( " . " + whitelistedDomain . lowercased ( ) ) {
@ -491,7 +500,7 @@ public class OWSLinkPreview: MTLModel {
}
}
return downloadLink ( url : previewUrl )
return downloadLink ( url : previewUrl )
. then ( on : DispatchQueue . global ( ) ) { ( data ) -> Promise < OWSLinkPreviewDraft > in
. then ( on : DispatchQueue . global ( ) ) { ( data ) -> Promise < OWSLinkPreviewDraft > in
return parse ( linkData : data , linkUrlString : previewUrl )
return parse LinkDataAndBuildDraft ( linkData : data , linkUrlString : previewUrl )
. then ( on : DispatchQueue . global ( ) ) { ( linkPreviewDraft ) -> Promise < OWSLinkPreviewDraft > in
. then ( on : DispatchQueue . global ( ) ) { ( linkPreviewDraft ) -> Promise < OWSLinkPreviewDraft > in
guard linkPreviewDraft . isValid ( ) else {
guard linkPreviewDraft . isValid ( ) else {
return Promise ( error : LinkPreviewError . noPreview )
return Promise ( error : LinkPreviewError . noPreview )
@ -629,49 +638,30 @@ public class OWSLinkPreview: MTLModel {
return false
return false
}
}
// E x a m p l e :
class func parseLinkDataAndBuildDraft ( linkData : Data ,
//
// < m e t a p r o p e r t y = " o g : t i t l e " c o n t e n t = " R a n d o m n e s s i s R a n d o m - N u m b e r p h i l e " >
// < m e t a p r o p e r t y = " o g : i m a g e " c o n t e n t = " h t t p s : / / i . y t i m g . c o m / v i / t P - I p s a t 9 0 c / m a x r e s d e f a u l t . j p g " >
private class func parse ( linkData : Data ,
linkUrlString : String ) -> Promise < OWSLinkPreviewDraft > {
linkUrlString : String ) -> Promise < OWSLinkPreviewDraft > {
guard let linkText = String ( bytes : linkData , encoding : . utf8 ) else {
do {
owsFailDebug ( " Could not parse link text. " )
let contents = try parse ( linkData : linkData )
return Promise ( error : LinkPreviewError . invalidInput )
}
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 ) {
let normalizedTitle = OWSLinkPreview . normalizeTitle ( title : decodedTitle )
if normalizedTitle . count > 0 {
title = normalizedTitle
}
}
}
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 {
let title = contents . title
guard let imageUrl = contents . imageUrl else {
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
}
}
guard let imageUrlString = decodeHTMLEntities ( inString : rawImageUrlString ) ? . ows_stripped ( ) else {
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
guard isValidMediaUrl ( imageUrl ) else {
}
guard isValidMediaUrl ( imageUrlString ) else {
Logger . error ( " Invalid image URL. " )
Logger . error ( " Invalid image URL. " )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
}
}
guard let imageFileExtension = fileExtension ( forImageUrl : imageUrl String ) else {
guard let imageFileExtension = fileExtension ( forImageUrl : imageUrl ) else {
Logger . error ( " Image URL has unknown or invalid file extension: \( imageUrl String ) . " )
Logger . error ( " Image URL has unknown or invalid file extension: \( imageUrl ) . " )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
}
}
guard let imageMimeType = mimetype ( forImageFileExtension : imageFileExtension ) else {
guard let imageMimeType = mimetype ( forImageFileExtension : imageFileExtension ) else {
Logger . error ( " Image URL has unknown or invalid content type: \( imageUrl String ) . " )
Logger . error ( " Image URL has unknown or invalid content type: \( imageUrl ) . " )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
}
}
return downloadImage ( url : imageUrl String , imageMimeType : imageMimeType )
return downloadImage ( url : imageUrl , imageMimeType : imageMimeType )
. then ( on : DispatchQueue . global ( ) ) { ( imageData : Data ) -> Promise < OWSLinkPreviewDraft > in
. then ( on : DispatchQueue . global ( ) ) { ( imageData : Data ) -> Promise < OWSLinkPreviewDraft > in
let imageFilePath = OWSFileSystem . temporaryFilePath ( withFileExtension : imageFileExtension )
let imageFilePath = OWSFileSystem . temporaryFilePath ( withFileExtension : imageFileExtension )
do {
do {
@ -697,19 +687,63 @@ public class OWSLinkPreview: MTLModel {
. recover ( on : DispatchQueue . global ( ) ) { ( _ ) -> Promise < OWSLinkPreviewDraft > in
. recover ( on : DispatchQueue . global ( ) ) { ( _ ) -> Promise < OWSLinkPreviewDraft > in
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
}
}
} catch {
owsFailDebug ( " Could not parse link data: \( error ) . " )
return Promise ( error : error )
}
}
// E x a m p l e :
//
// < m e t a p r o p e r t y = " o g : t i t l e " c o n t e n t = " R a n d o m n e s s i s R a n d o m - N u m b e r p h i l e " >
// < m e t a p r o p e r t y = " o g : i m a g e " c o n t e n t = " h t t p s : / / i . y t i m g . c o m / v i / t P - I p s a t 9 0 c / m a x r e s d e f a u l t . j p g " >
class func parse ( linkData : Data ) throws -> OWSLinkPreviewContents {
guard let linkText = String ( bytes : linkData , encoding : . utf8 ) else {
owsFailDebug ( " Could not parse link text. " )
throw LinkPreviewError . invalidInput
}
}
private class func fileExtension ( forImageUrl urlString : String ) -> String ? {
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 ) {
let normalizedTitle = OWSLinkPreview . normalizeTitle ( title : decodedTitle )
if normalizedTitle . count > 0 {
title = normalizedTitle
}
}
}
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 OWSLinkPreviewContents ( title : title )
}
guard let imageUrlString = decodeHTMLEntities ( inString : rawImageUrlString ) ? . ows_stripped ( ) else {
return OWSLinkPreviewContents ( title : title )
}
return OWSLinkPreviewContents ( title : title , imageUrl : imageUrlString )
}
class func fileExtension ( forImageUrl urlString : String ) -> String ? {
guard let imageUrl = URL ( string : urlString ) else {
guard let imageUrl = URL ( string : urlString ) else {
Logger . error ( " Could not parse image URL. " )
Logger . error ( " Could not parse image URL. " )
return nil
return nil
}
}
let imageFilename = imageUrl . lastPathComponent
let imageFilename = imageUrl . lastPathComponent
let imageFileExtension = ( imageFilename as NSString ) . pathExtension . lowercased ( )
let imageFileExtension = ( imageFilename as NSString ) . pathExtension . lowercased ( )
guard imageFileExtension . count > 0 else {
return nil
}
return imageFileExtension
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 {
guard let imageMimeType = MIMETypeUtil . mimeType ( forFileExtension : imageFileExtension ) else {
Logger . error ( " Image URL has unknown content type: \( imageFileExtension ) . " )
Logger . error ( " Image URL has unknown content type: \( imageFileExtension ) . " )
return nil
return nil