Browse Source

redirect link preview to valid link

Michael Kirk 4 months ago
parent
commit
cfed9fbe0d

+ 13 - 3
Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m

@@ -105,6 +105,15 @@ const CGFloat kMaxTextViewHeight = 98;
     return self;
 }
 
+#pragma mark - Dependencies
+
+- (OWSLinkPreviewManager *)linkPreviewManager
+{
+    return SSKEnvironment.shared.linkPreviewManager;
+}
+
+#pragma mark -
+
 - (CGSize)intrinsicContentSize
 {
     // Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, we must specify
@@ -956,8 +965,9 @@ const CGFloat kMaxTextViewHeight = 98;
 
     // It's key that we use the *raw/unstripped* text, so we can reconcile cursor position with the
     // selectedRange.
-    NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForRawBodyText:self.inputTextView.text
-                                                                selectedRange:self.inputTextView.selectedRange];
+    NSString *_Nullable previewUrl =
+        [self.linkPreviewManager previewUrlForRawBodyText:self.inputTextView.text
+                                            selectedRange:self.inputTextView.selectedRange];
     if (previewUrl.length < 1) {
         [self clearLinkPreviewStateAndView];
         return;
@@ -975,7 +985,7 @@ const CGFloat kMaxTextViewHeight = 98;
     [self ensureLinkPreviewViewWithState:[LinkPreviewLoading new]];
 
     __weak ConversationInputToolbar *weakSelf = self;
-    [[OWSLinkPreview tryToBuildPreviewInfoObjcWithPreviewUrl:previewUrl]
+    [[self.linkPreviewManager tryToBuildPreviewInfoObjcWithPreviewUrl:previewUrl]
             .then(^(OWSLinkPreviewDraft *linkPreviewDraft) {
                 ConversationInputToolbar *_Nullable strongSelf = weakSelf;
                 if (!strongSelf) {

+ 2 - 0
SignalMessaging/environment/AppSetup.m

@@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
 
         TSNetworkManager *networkManager = [[TSNetworkManager alloc] initDefault];
         OWSContactsManager *contactsManager = [[OWSContactsManager alloc] initWithPrimaryStorage:primaryStorage];
+        OWSLinkPreviewManager *linkPreviewManager = [OWSLinkPreviewManager new];
         ContactsUpdater *contactsUpdater = [ContactsUpdater new];
         OWSMessageSender *messageSender = [[OWSMessageSender alloc] initWithPrimaryStorage:primaryStorage];
         SSKMessageSenderJobQueue *messageSenderJobQueue = [SSKMessageSenderJobQueue new];
@@ -101,6 +102,7 @@ NS_ASSUME_NONNULL_BEGIN
                                                            windowManager:windowManager]];
 
         [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithContactsManager:contactsManager
+                                                               linkPreviewManager:linkPreviewManager
                                                                     messageSender:messageSender
                                                             messageSenderJobQueue:messageSenderJobQueue
                                                                    profileManager:profileManager

+ 106 - 63
SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift

@@ -62,7 +62,7 @@ public class OWSLinkPreviewDraft: NSObject {
 
     @objc
     public func displayDomain() -> String? {
-        return OWSLinkPreview.displayDomain(forUrl: urlString)
+        return OWSLinkPreviewManager.displayDomain(forUrl: urlString)
     }
 }
 
@@ -134,13 +134,13 @@ public class OWSLinkPreview: MTLModel {
             Logger.error("Preview for message without body.")
             throw LinkPreviewError.invalidInput
         }
-        let previewUrls = allPreviewUrls(forMessageBodyText: body)
+        let previewUrls = OWSLinkPreviewManager.allPreviewUrls(forMessageBodyText: body)
         guard previewUrls.contains(urlString) else {
             Logger.error("URL not present in body.")
             throw LinkPreviewError.invalidInput
         }
 
-        guard isValidLink(url: url) else {
+        guard OWSLinkPreviewManager.isValidLink(url: url) else {
             Logger.verbose("Invalid link URL \(urlString).")
             Logger.error("Invalid link URL.")
             throw LinkPreviewError.invalidInput
@@ -148,7 +148,7 @@ public class OWSLinkPreview: MTLModel {
 
         var title: String?
         if let rawTitle = previewProto.title {
-            let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle)
+            let normalizedTitle = normalizeTitle(title: rawTitle)
             if normalizedTitle.count > 0 {
                 title = normalizedTitle
             }
@@ -182,7 +182,7 @@ public class OWSLinkPreview: MTLModel {
             throw LinkPreviewError.noPreview
         }
         let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(jpegImageData: info.jpegImageData,
-                                                                        transaction: transaction)
+                                                         transaction: transaction)
 
         let linkPreview = OWSLinkPreview(urlString: info.urlString, title: info.title, imageAttachmentId: imageAttachmentId)
 
@@ -251,22 +251,14 @@ public class OWSLinkPreview: MTLModel {
         attachment.remove(with: transaction)
     }
 
-    private class func normalizeTitle(title: String) -> String {
-        var result = title
-        // Truncate title after 2 lines of text.
-        let maxLineCount = 2
-        var components = result.components(separatedBy: .newlines)
-        if components.count > maxLineCount {
-            components = Array(components[0..<maxLineCount])
-            result =  components.joined(separator: "\n")
-        }
-        let maxCharacterCount = 2048
-        if result.count > maxCharacterCount {
-            let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount)
-            result = String(result[..<endIndex])
-        }
-        return result.filterStringForDisplay()
+    @objc
+    public func displayDomain() -> String? {
+        return OWSLinkPreviewManager.displayDomain(forUrl: urlString)
     }
+}
+
+@objc
+class OWSLinkPreviewManager: NSObject {
 
     // MARK: - Whitelists
 
@@ -335,11 +327,6 @@ public class OWSLinkPreview: MTLModel {
     ]
 
     @objc
-    public func displayDomain() -> String? {
-        return OWSLinkPreview.displayDomain(forUrl: urlString)
-    }
-
-    @objc
     public class func displayDomain(forUrl urlString: String?) -> String? {
         guard let urlString = urlString else {
             owsFailDebug("Missing url.")
@@ -350,7 +337,7 @@ public class OWSLinkPreview: MTLModel {
             return nil
         }
         guard let result = whitelistedDomain(forUrl: url,
-                                             domainWhitelist: OWSLinkPreview.linkDomainWhitelist,
+                                             domainWhitelist: linkDomainWhitelist,
                                              allowSubdomains: false) else {
                                                 Logger.error("Missing domain.")
                                                 return nil
@@ -361,14 +348,14 @@ public class OWSLinkPreview: MTLModel {
     @objc
     public class func isValidLink(url: URL) -> Bool {
         return whitelistedDomain(forUrl: url,
-                                 domainWhitelist: OWSLinkPreview.linkDomainWhitelist,
+                                 domainWhitelist: linkDomainWhitelist,
                                  allowSubdomains: false) != nil
     }
 
     @objc
     public class func isValidMedia(url: URL) -> Bool {
         return whitelistedDomain(forUrl: url,
-                                 domainWhitelist: OWSLinkPreview.mediaDomainWhitelist,
+                                 domainWhitelist: mediaDomainWhitelist,
                                  allowSubdomains: true) != nil
     }
 
@@ -401,9 +388,9 @@ public class OWSLinkPreview: MTLModel {
 
     // MARK: - Serial Queue
 
-    private static let serialQueue = DispatchQueue(label: "org.signal.linkPreview")
+    private let serialQueue = DispatchQueue(label: "org.signal.linkPreview")
 
-    private class func assertIsOnSerialQueue() {
+    private func assertIsOnSerialQueue() {
         if _isDebugAssertConfiguration(), #available(iOS 10.0, *) {
             assertOnQueue(serialQueue)
         }
@@ -412,14 +399,14 @@ public class OWSLinkPreview: MTLModel {
     // MARK: - Text Parsing
 
     // This cache should only be accessed on main thread.
-    private static var previewUrlCache: NSCache<NSString, NSString> = NSCache()
+    private var previewUrlCache: NSCache<NSString, NSString> = NSCache()
 
     @objc
-    public class func previewUrl(forRawBodyText body: String?, selectedRange: NSRange) -> String? {
+    public func previewUrl(forRawBodyText body: String?, selectedRange: NSRange) -> String? {
         return previewUrl(forMessageBodyText: body, selectedRange: selectedRange)
     }
 
-    public class func previewUrl(forMessageBodyText body: String?, selectedRange: NSRange?) -> String? {
+    public func previewUrl(forMessageBodyText body: String?, selectedRange: NSRange?) -> String? {
         AssertIsOnMainThread()
 
         // Exit early if link previews are not enabled in order to avoid
@@ -439,7 +426,7 @@ public class OWSLinkPreview: MTLModel {
             }
             return cachedUrl
         }
-        let previewUrlMatches = allPreviewUrlMatches(forMessageBodyText: body)
+        let previewUrlMatches = OWSLinkPreviewManager.allPreviewUrlMatches(forMessageBodyText: body)
         guard let urlMatch = previewUrlMatches.first else {
             // Use empty string to indicate "no preview URL" in the cache.
             previewUrlCache.setObject("", forKey: body as NSString)
@@ -505,9 +492,9 @@ public class OWSLinkPreview: MTLModel {
     // This cache should only be accessed on serialQueue.
     //
     // We should only maintain a "cache" of the last known draft.
-    private static var linkPreviewDraftCache: OWSLinkPreviewDraft?
+    private var linkPreviewDraftCache: OWSLinkPreviewDraft?
 
-    private class func cachedLinkPreview(forPreviewUrl previewUrl: String) -> OWSLinkPreviewDraft? {
+    private func cachedLinkPreview(forPreviewUrl previewUrl: String) -> OWSLinkPreviewDraft? {
         return serialQueue.sync {
             guard let linkPreviewDraft = linkPreviewDraftCache,
                 linkPreviewDraft.urlString == previewUrl else {
@@ -519,7 +506,7 @@ public class OWSLinkPreview: MTLModel {
         }
     }
 
-    private class func setCachedLinkPreview(_ linkPreviewDraft: OWSLinkPreviewDraft,
+    private func setCachedLinkPreview(_ linkPreviewDraft: OWSLinkPreviewDraft,
                                             forPreviewUrl previewUrl: String) {
         assert(previewUrl == linkPreviewDraft.urlString)
 
@@ -535,11 +522,11 @@ public class OWSLinkPreview: MTLModel {
     }
 
     @objc
-    public class func tryToBuildPreviewInfoObjc(previewUrl: String?) -> AnyPromise {
+    public func tryToBuildPreviewInfoObjc(previewUrl: String?) -> AnyPromise {
         return AnyPromise(tryToBuildPreviewInfo(previewUrl: previewUrl))
     }
 
-    public class func tryToBuildPreviewInfo(previewUrl: String?) -> Promise<OWSLinkPreviewDraft> {
+    public func tryToBuildPreviewInfo(previewUrl: String?) -> Promise<OWSLinkPreviewDraft> {
         guard SSKPreferences.areLinkPreviewsEnabled else {
             return Promise(error: LinkPreviewError.featureDisabled)
         }
@@ -552,18 +539,27 @@ public class OWSLinkPreview: MTLModel {
         }
         return downloadLink(url: previewUrl)
             .then(on: DispatchQueue.global()) { (data) -> Promise<OWSLinkPreviewDraft> in
-                return parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl)
-            }.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise<OWSLinkPreviewDraft> in
+                return self.parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl)
+            }.map(on: DispatchQueue.global()) { (linkPreviewDraft) -> OWSLinkPreviewDraft in
                 guard linkPreviewDraft.isValid() else {
                     throw LinkPreviewError.noPreview
                 }
-                setCachedLinkPreview(linkPreviewDraft, forPreviewUrl: previewUrl)
+                self.setCachedLinkPreview(linkPreviewDraft, forPreviewUrl: previewUrl)
 
-                return Promise.value(linkPreviewDraft)
+                return linkPreviewDraft
         }
     }
 
-    class func downloadLink(url urlString: String,
+    // MARK: - Downloads
+
+    lazy var proxiedContentDownloader: ProxiedContentDownloader = {
+        let proxiedContentDownloader = ProxiedContentDownloader(downloadFolderName: "LinkPreviews")
+        proxiedContentDownloader.delegate = self
+
+        return proxiedContentDownloader
+    }()
+
+    func downloadLink(url urlString: String,
                             remainingRetries: UInt = 3) -> Promise<Data> {
 
         Logger.verbose("url: \(urlString)")
@@ -578,6 +574,21 @@ public class OWSLinkPreview: MTLModel {
                                                   sessionConfiguration: sessionConfiguration)
         sessionManager.requestSerializer = AFHTTPRequestSerializer()
         sessionManager.responseSerializer = AFHTTPResponseSerializer()
+        let redirectionBlock = { (session: URLSession, task: URLSessionTask, response: URLResponse, request: URLRequest) -> URLRequest? in
+            guard let redirectURL = request.url else {
+                owsFailDebug("redirectURL was unexpectedly nil")
+                return nil
+            }
+
+            guard OWSLinkPreviewManager.isValidLink(url: redirectURL) else {
+                Logger.debug("Ignoring redirect to non-whitelisted URL: \(redirectURL)")
+                return nil
+            }
+
+            Logger.debug("link preview redirected: \(String(describing: response.url)) -> \(redirectURL)")
+            return request
+        }
+        sessionManager.setTaskWillPerformHTTPRedirectionBlock(redirectionBlock)
 
         guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else {
             owsFailDebug("Could not configure url: \(urlString).")
@@ -617,7 +628,7 @@ public class OWSLinkPreview: MTLModel {
                            failure: { _, error in
                             Logger.verbose("Error: \(error)")
 
-                            guard isRetryable(error: error) else {
+                            guard self.isRetryable(error: error) else {
                                 Logger.warn("Error is not retryable.")
                                 resolver.reject(LinkPreviewError.couldNotDownload)
                                 return
@@ -628,7 +639,7 @@ public class OWSLinkPreview: MTLModel {
                                 resolver.reject(LinkPreviewError.couldNotDownload)
                                 return
                             }
-                            OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1)
+                            self.downloadLink(url: urlString, remainingRetries: remainingRetries - 1)
                             .done(on: DispatchQueue.global()) { (data) in
                                 resolver.fulfill(data)
                             }.catch(on: DispatchQueue.global()) { (error) in
@@ -638,7 +649,7 @@ public class OWSLinkPreview: MTLModel {
         return promise
     }
 
-    private class func downloadImage(url urlString: String, imageMimeType: String) -> Promise<Data> {
+    private func downloadImage(url urlString: String, imageMimeType: String) -> Promise<Data> {
 
         Logger.verbose("url: \(urlString)")
 
@@ -653,13 +664,14 @@ public class OWSLinkPreview: MTLModel {
         }
         let (promise, resolver) = Promise<ProxiedContentAsset>.pending()
         DispatchQueue.main.async {
-            _ = ProxiedContentDownloader.defaultDownloader.requestAsset(assetDescription: assetDescription,
-                                                                        priority: .high,
-                                                                        success: { (_, asset) in
-                                                                            resolver.fulfill(asset)
-            }, failure: { (_) in
-                Logger.warn("Error downloading asset")
-                resolver.reject(LinkPreviewError.couldNotDownload)
+            _ = self.proxiedContentDownloader.requestAsset(assetDescription: assetDescription,
+                                                      priority: .high,
+                                                      success: { (_, asset) in
+                                                        resolver.fulfill(asset)
+            },
+                                                      failure: { (_) in
+                                                        Logger.warn("Error downloading asset")
+                                                        resolver.reject(LinkPreviewError.couldNotDownload)
             })
         }
         return promise.then(on: DispatchQueue.global()) { (asset: ProxiedContentAsset) -> Promise<Data> in
@@ -702,7 +714,7 @@ public class OWSLinkPreview: MTLModel {
         }
     }
 
-    private class func isRetryable(error: Error) -> Bool {
+    private func isRetryable(error: Error) -> Bool {
         let nsError = error as NSError
         if nsError.domain == kCFErrorDomainCFNetwork as String {
             // Network failures are retried.
@@ -711,8 +723,8 @@ public class OWSLinkPreview: MTLModel {
         return false
     }
 
-    class func parseLinkDataAndBuildDraft(linkData: Data,
-                                          linkUrlString: String) -> Promise<OWSLinkPreviewDraft> {
+    func parseLinkDataAndBuildDraft(linkData: Data,
+                                    linkUrlString: String) -> Promise<OWSLinkPreviewDraft> {
         do {
             let contents = try parse(linkData: linkData)
 
@@ -724,7 +736,7 @@ public class OWSLinkPreview: MTLModel {
                 Logger.error("Invalid imageUrlString.")
                 return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
             }
-            guard isValidMedia(url: imageUrl) else {
+            guard OWSLinkPreviewManager.isValidMedia(url: imageUrl) else {
                 Logger.error("Invalid image URL.")
                 return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title))
             }
@@ -756,7 +768,7 @@ public class OWSLinkPreview: MTLModel {
     //
     //    <meta property="og:title" content="Randomness is Random - Numberphile">
     //    <meta property="og:image" content="https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg">
-    class func parse(linkData: Data) throws -> OWSLinkPreviewContents {
+    func parse(linkData: Data) throws -> OWSLinkPreviewContents {
         guard let linkText = String(bytes: linkData, encoding: .utf8) else {
             owsFailDebug("Could not parse link text.")
             throw LinkPreviewError.invalidInput
@@ -767,7 +779,7 @@ public class OWSLinkPreview: MTLModel {
                                                               text: linkText,
                                                               options: .dotMatchesLineSeparators) {
             if let decodedTitle = decodeHTMLEntities(inString: rawTitle) {
-                let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle)
+                let normalizedTitle = normalizeTitle(title: decodedTitle)
                 if normalizedTitle.count > 0 {
                     title = normalizedTitle
                 }
@@ -786,7 +798,7 @@ public class OWSLinkPreview: MTLModel {
         return OWSLinkPreviewContents(title: title, imageUrl: imageUrlString)
     }
 
-    class func fileExtension(forImageUrl urlString: String) -> String? {
+    func fileExtension(forImageUrl urlString: String) -> String? {
         guard let imageUrl = URL(string: urlString) else {
             Logger.error("Could not parse image URL.")
             return nil
@@ -799,7 +811,7 @@ public class OWSLinkPreview: MTLModel {
         return imageFileExtension
     }
 
-    class func mimetype(forImageFileExtension imageFileExtension: String) -> String? {
+    func mimetype(forImageFileExtension imageFileExtension: String) -> String? {
         guard imageFileExtension.count > 0 else {
             return nil
         }
@@ -818,7 +830,7 @@ public class OWSLinkPreview: MTLModel {
         return imageMimeType
     }
 
-    private class func decodeHTMLEntities(inString value: String) -> String? {
+    private func decodeHTMLEntities(inString value: String) -> String? {
         guard let data = value.data(using: .utf8) else {
             return nil
         }
@@ -835,3 +847,34 @@ public class OWSLinkPreview: MTLModel {
         return attributedString.string
     }
 }
+
+extension OWSLinkPreviewManager: ProxiedContentDownloaderDelegate {
+    func proxiedContentDownloader(willPerformHTTPRedirection response: HTTPURLResponse, newRequest: URLRequest) -> URLRequest? {
+        guard let url = newRequest.url else {
+            return nil
+        }
+
+        guard OWSLinkPreviewManager.isValidMedia(url: url) else {
+            return nil
+        }
+
+        return newRequest
+    }
+}
+
+private func normalizeTitle(title: String) -> String {
+    var result = title
+    // Truncate title after 2 lines of text.
+    let maxLineCount = 2
+    var components = result.components(separatedBy: .newlines)
+    if components.count > maxLineCount {
+        components = Array(components[0..<maxLineCount])
+        result =  components.joined(separator: "\n")
+    }
+    let maxCharacterCount = 2048
+    if result.count > maxCharacterCount {
+        let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount)
+        result = String(result[..<endIndex])
+    }
+    return result.filterStringForDisplay()
+}

+ 23 - 3
SignalServiceKit/src/Network/ProxiedContentDownloader.swift

@@ -10,6 +10,12 @@ public enum ProxiedContentRequestPriority {
     case low, high
 }
 
+protocol ProxiedContentDownloaderDelegate: AnyObject {
+    /// uses the same semantics as:
+    /// URLSessionDelegate#URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler;
+    func proxiedContentDownloader(willPerformHTTPRedirection response: HTTPURLResponse, newRequest: URLRequest) -> URLRequest?
+}
+
 // MARK: -
 
 @objc
@@ -445,9 +451,6 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
 
     // MARK: - Properties
 
-    @objc
-    public static let defaultDownloader = ProxiedContentDownloader(downloadFolderName: "proxiedContent")
-
     private let downloadFolderName: String
 
     private var downloadFolderPath: String?
@@ -734,6 +737,12 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
             self.assetRequestDidFail(assetRequest: assetRequest)
             return
         }
+        guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
+            Logger.warn("invalid httpResponse.statusCode: \(httpResponse.statusCode)")
+            assetRequest.state = .failed
+            self.assetRequestDidFail(assetRequest: assetRequest)
+            return
+        }
         var firstContentRangeString: String?
         for header in httpResponse.allHeaderFields.keys {
             guard let headerString = header as? String else {
@@ -898,6 +907,17 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
         segmentRequestDidSucceed(assetRequest: assetRequest, assetSegment: assetSegment)
     }
 
+    weak var delegate: ProxiedContentDownloaderDelegate?
+    public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
+        guard let delegate = delegate else {
+            completionHandler(request)
+            return
+        }
+
+        let delegateRequest = delegate.proxiedContentDownloader(willPerformHTTPRedirection: response, newRequest: request)
+        completionHandler(delegateRequest)
+    }
+
     // MARK: Temp Directory
 
     public func ensureDownloadFolder() {

+ 3 - 0
SignalServiceKit/src/SSKEnvironment.h

@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
 @class OWSBlockingManager;
 @class OWSDisappearingMessagesJob;
 @class OWSIdentityManager;
+@class OWSLinkPreviewManager;
 @class OWSMessageDecrypter;
 @class OWSMessageManager;
 @class OWSMessageReceiver;
@@ -39,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
 @interface SSKEnvironment : NSObject
 
 - (instancetype)initWithContactsManager:(id<ContactsManagerProtocol>)contactsManager
+                     linkPreviewManager:(OWSLinkPreviewManager *)linkPreviewManager
                           messageSender:(OWSMessageSender *)messageSender
                   messageSenderJobQueue:(SSKMessageSenderJobQueue *)messageSenderJobQueue
                          profileManager:(id<ProfileManagerProtocol>)profileManager
@@ -76,6 +78,7 @@ NS_ASSUME_NONNULL_BEGIN
 #endif
 
 @property (nonatomic, readonly) id<ContactsManagerProtocol> contactsManager;
+@property (nonatomic, readonly) OWSLinkPreviewManager *linkPreviewManager;
 @property (nonatomic, readonly) OWSMessageSender *messageSender;
 @property (nonatomic, readonly) SSKMessageSenderJobQueue *messageSenderJobQueue;
 @property (nonatomic, readonly) id<ProfileManagerProtocol> profileManager;

+ 3 - 0
SignalServiceKit/src/SSKEnvironment.m

@@ -51,6 +51,7 @@ static SSKEnvironment *sharedSSKEnvironment;
 @synthesize analyticsDBConnection = _analyticsDBConnection;
 
 - (instancetype)initWithContactsManager:(id<ContactsManagerProtocol>)contactsManager
+                     linkPreviewManager:(OWSLinkPreviewManager *)linkPreviewManager
                           messageSender:(OWSMessageSender *)messageSender
                   messageSenderJobQueue:(SSKMessageSenderJobQueue *)messageSenderJobQueue
                          profileManager:(id<ProfileManagerProtocol>)profileManager
@@ -82,6 +83,7 @@ static SSKEnvironment *sharedSSKEnvironment;
     }
 
     OWSAssertDebug(contactsManager);
+    OWSAssertDebug(linkPreviewManager);
     OWSAssertDebug(messageSender);
     OWSAssertDebug(messageSenderJobQueue);
     OWSAssertDebug(profileManager);
@@ -108,6 +110,7 @@ static SSKEnvironment *sharedSSKEnvironment;
     OWSAssertDebug(attachmentDownloads);
 
     _contactsManager = contactsManager;
+    _linkPreviewManager = linkPreviewManager;
     _messageSender = messageSender;
     _messageSenderJobQueue = messageSenderJobQueue;
     _profileManager = profileManager;

+ 2 - 0
SignalServiceKit/src/TestUtils/MockSSKEnvironment.m

@@ -51,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
 {
     OWSPrimaryStorage *primaryStorage = [MockSSKEnvironment createPrimaryStorageForTests];
     id<ContactsManagerProtocol> contactsManager = [OWSFakeContactsManager new];
+    OWSLinkPreviewManager *linkPreviewManager = [OWSLinkPreviewManager new];
     TSNetworkManager *networkManager = [OWSFakeNetworkManager new];
     OWSMessageSender *messageSender = [OWSFakeMessageSender new];
     SSKMessageSenderJobQueue *messageSenderJobQueue = [SSKMessageSenderJobQueue new];
@@ -78,6 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
     OWSAttachmentDownloads *attachmentDownloads = [[OWSAttachmentDownloads alloc] init];
 
     self = [super initWithContactsManager:contactsManager
+                       linkPreviewManager:linkPreviewManager
                             messageSender:messageSender
                     messageSenderJobQueue:messageSenderJobQueue
                            profileManager:[OWSFakeProfileManager new]