@ -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,17 +638,73 @@ public class OWSLinkPreview: MTLModel {
return false
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 )
}
// N O T E : i m a g e S i z e ( f o r F i l e P a t h : . . . ) w i l l c a l l o w s _ i s V a l i d I m a g e ( . . . ) .
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 )
}
}
// E x a m p l e :
// 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 : 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 " >
// < 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 ,
class func parse ( linkData : Data ) throws -> OWSLinkPreviewContents {
linkUrlString : String ) -> Promise < OWSLinkPreviewDraft > {
guard let linkText = String ( bytes : linkData , encoding : . utf8 ) else {
guard let linkText = String ( bytes : linkData , encoding : . utf8 ) else {
owsFailDebug ( " Could not parse link text. " )
owsFailDebug ( " Could not parse link text. " )
return Promise ( error : LinkPreviewError . invalidInput )
throw LinkPreviewError . invalidInput
}
}
Logger . verbose ( " linkText: \( linkText ) " )
var title : String ?
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 rawTitle = NSRegularExpression . parseFirstMatch ( pattern : " <meta \\ s+property \\ s*= \\ s* \" og:title \" \\ s+content \\ s*= \\ s* \" (.*?) \" \\ s*/?> " , text : linkText ) {
if let decodedTitle = decodeHTMLEntities ( inString : rawTitle ) {
if let decodedTitle = decodeHTMLEntities ( inString : rawTitle ) {
@ -653,63 +718,32 @@ public class OWSLinkPreview: MTLModel {
Logger . verbose ( " title: \( String ( describing : title ) ) " )
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 {
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 {
guard let imageUrlString = decodeHTMLEntities ( inString : rawImageUrlString ) ? . ows_stripped ( ) else {
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
return OWSLinkPreviewContents ( 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 ) )
}
}
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 )
}
// N O T E : i m a g e S i z e ( f o r F i l e P a t h : . . . ) w i l l c a l l o w s _ i s V a l i d I m a g e ( . . . ) .
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 OWSLinkPreviewContents ( title : title , imageUrl : imageUrlString )
return Promise . value ( linkPreviewDraft )
}
. recover ( on : DispatchQueue . global ( ) ) { ( _ ) -> Promise < OWSLinkPreviewDraft > in
return Promise . value ( OWSLinkPreviewDraft ( urlString : linkUrlString , title : title ) )
}
}
}
private class func fileExtension ( forImageUrl urlString : String ) -> String ? {
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