// // QuickLook.m // WebCore // // Copyright 2009 Apple Inc. All rights reserved. // #import "config.h" #import "QuickLook.h" #if USE(QUICK_LOOK) #import "FileSystemIOS.h" #import "KURL.h" #import "Logging.h" #import "ResourceHandle.h" #import "RuntimeApplicationChecksIOS.h" #import "SoftLinking.h" #import "SubstituteData.h" #import "WebCoreURLResponseIOS.h" #import <Foundation/Foundation.h> #import <Foundation/NSFileManager_NSURLExtras.h> #import <QuickLook/QLPreviewConverter.h> #import <QuickLook/QuickLookPrivate.h> #import <wtf/PassOwnPtr.h> #import <wtf/RetainPtr.h> #import <wtf/StdLibExtras.h> #import <wtf/Threading.h> #import <wtf/Vector.h> #import <wtf/text/WTFString.h> #if USE(CFNETWORK) #import <CFNetwork/CFURLConnection.h> @interface NSURLResponse (QuickLookDetails) +(NSURLResponse *)_responseWithCFURLResponse:(CFURLResponseRef)response; -(CFURLResponseRef)_CFURLResponse; @end #endif SOFT_LINK_FRAMEWORK_OPTIONAL(QuickLook) SOFT_LINK_CLASS(QuickLook, QLPreviewConverter) SOFT_LINK_MAY_FAIL(QuickLook, QLPreviewGetSupportedMIMETypes, NSSet *, (), ()) SOFT_LINK_MAY_FAIL(QuickLook, QLTypeCopyBestMimeTypeForFileNameAndMimeType, NSString *, (NSString *fileName, NSString *mimeType), (fileName, mimeType)) SOFT_LINK_MAY_FAIL(QuickLook, QLTypeCopyBestMimeTypeForURLAndMimeType, NSString *, (NSURL *url, NSString *mimeType), (url, mimeType)) SOFT_LINK_MAY_FAIL(QuickLook, QLTypeCopyUTIForURLAndMimeType, NSString *, (NSURL *url, NSString *mimeType), (url, mimeType)) SOFT_LINK_CONSTANT_MAY_FAIL(QuickLook, QLPreviewScheme, NSString *) namespace WebCore { NSString *QLTypeCopyUTIForURLAndMimeType(NSURL *url, NSString *mimeType); } using namespace WebCore; Class WebCore::QLPreviewConverterClass() { #define QLPreviewConverter getQLPreviewConverterClass() return QLPreviewConverter; #undef QLPreviewConverter } NSString *WebCore::QLTypeCopyBestMimeTypeForFileNameAndMimeType(NSString *fileName, NSString *mimeType) { if (!canLoadQLTypeCopyBestMimeTypeForFileNameAndMimeType()) return nil; return ::QLTypeCopyBestMimeTypeForFileNameAndMimeType(fileName, mimeType); } NSString *WebCore::QLTypeCopyBestMimeTypeForURLAndMimeType(NSURL *url, NSString *mimeType) { if (!canLoadQLTypeCopyBestMimeTypeForURLAndMimeType()) return nil; return ::QLTypeCopyBestMimeTypeForURLAndMimeType(url, mimeType); } NSSet *WebCore::QLPreviewGetSupportedMIMETypesSet() { if (!canLoadQLPreviewGetSupportedMIMETypes()) return nil; DEFINE_STATIC_LOCAL(RetainPtr<NSSet>, set, (::QLPreviewGetSupportedMIMETypes())); return set.get(); } NSString *WebCore::QLTypeCopyUTIForURLAndMimeType(NSURL *url, NSString *mimeType) { if (!canLoadQLTypeCopyUTIForURLAndMimeType()) return nil; return ::QLTypeCopyUTIForURLAndMimeType(url, mimeType); } NSDictionary *WebCore::QLFileAttributes() { // set file perms to owner read/write only NSNumber *filePOSIXPermissions = [NSNumber numberWithInteger:(WEB_UREAD | WEB_UWRITE)]; DEFINE_STATIC_LOCAL(RetainPtr<NSDictionary>, dictionary, ([[NSDictionary alloc] initWithObjectsAndKeys: NSUserName(), NSFileOwnerAccountName, filePOSIXPermissions, NSFilePosixPermissions, NULL])); return dictionary.get(); } NSDictionary *WebCore::QLDirectoryAttributes() { // set file perms to owner read/write/execute only NSNumber *directoryPOSIXPermissions = [NSNumber numberWithInteger:(WEB_UREAD | WEB_UWRITE | WEB_UEXEC)]; DEFINE_STATIC_LOCAL(RetainPtr<NSDictionary>, dictionary, ([[NSDictionary alloc] initWithObjectsAndKeys: NSUserName(), NSFileOwnerAccountName, directoryPOSIXPermissions, NSFilePosixPermissions, NULL ])); return dictionary.get(); } static Mutex& qlPreviewConverterDictionaryMutex() { DEFINE_STATIC_LOCAL(Mutex, mutex, ()); return mutex; } static NSMutableDictionary *QLPreviewConverterDictionary() { DEFINE_STATIC_LOCAL(RetainPtr<NSMutableDictionary>, dictionary, ([NSMutableDictionary dictionary])); return dictionary.get(); } static NSMutableDictionary *QLContentDictionary() { DEFINE_STATIC_LOCAL(RetainPtr<NSMutableDictionary>, contentDictionary, ([NSMutableDictionary dictionary])); return contentDictionary.get(); } void WebCore::addQLPreviewConverterWithFileForURL(NSURL *url, id converter, NSString *fileName) { ASSERT(url); ASSERT(converter); MutexLocker lock(qlPreviewConverterDictionaryMutex()); [QLPreviewConverterDictionary() setObject:converter forKey:url]; [QLContentDictionary() setObject:(fileName ? fileName : @"") forKey:url]; } NSString *WebCore::qlPreviewConverterFileNameForURL(NSURL *url) { return [QLContentDictionary() objectForKey:url]; } NSString *WebCore::qlPreviewConverterUTIForURL(NSURL *url) { id converter = nil; { MutexLocker lock(qlPreviewConverterDictionaryMutex()); converter = [QLPreviewConverterDictionary() objectForKey:url]; } if (!converter) return nil; return [converter previewUTI]; } void WebCore::removeQLPreviewConverterForURL(NSURL *url) { MutexLocker lock(qlPreviewConverterDictionaryMutex()); [QLPreviewConverterDictionary() removeObjectForKey:url]; // Delete the file when we remove the preview converter NSString *filename = qlPreviewConverterFileNameForURL(url); if ([filename length]) [[NSFileManager defaultManager] _web_removeFileOnlyAtPath:filename]; [QLContentDictionary() removeObjectForKey:url]; } PassOwnPtr<ResourceRequest> WebCore::registerQLPreviewConverterIfNeeded(NSURL *url, NSString *mimeType, NSData *data) { RetainPtr<NSString> updatedMIMEType = adoptNS(WebCore::QLTypeCopyBestMimeTypeForURLAndMimeType(url, mimeType)); if ([WebCore::QLPreviewGetSupportedMIMETypesSet() containsObject:updatedMIMEType.get()]) { RetainPtr<NSString> uti = adoptNS(WebCore::QLTypeCopyUTIForURLAndMimeType(url, updatedMIMEType.get())); RetainPtr<id> converter = adoptNS([[QLPreviewConverterClass() alloc] initWithData:data name:nil uti:uti.get() options:nil]); NSURLRequest *request = [converter.get() previewRequest]; // We use [request URL] here instead of url since it will be // the URL that the WebDataSource will see during -dealloc. addQLPreviewConverterWithFileForURL([request URL], converter.get(), nil); return adoptPtr(new ResourceRequest(request)); } return nullptr; } const KURL WebCore::safeQLURLForDocumentURLAndResourceURL(const KURL& documentURL, const String& resourceURL) { id converter = nil; NSURL *nsDocumentURL = documentURL; { MutexLocker lock(qlPreviewConverterDictionaryMutex()); converter = [QLPreviewConverterDictionary() objectForKey:nsDocumentURL]; } if (!converter) return KURL(ParsedURLString, resourceURL); NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:resourceURL]]; NSURLRequest *safeRequest = [converter safeRequestForRequest:request]; [request release]; return [safeRequest URL]; } static Vector<char> createQLPreviewProtocol() { Vector<char> previewProtocol; #define QLPreviewScheme getQLPreviewScheme() const char* qlPreviewScheme = [QLPreviewScheme UTF8String]; #undef QLPreviewScheme previewProtocol.append(qlPreviewScheme, strlen(qlPreviewScheme) + 1); return previewProtocol; } const char* WebCore::QLPreviewProtocol() { if (!canLoadQLPreviewScheme()) return ""; DEFINE_STATIC_LOCAL(Vector<char>, previewProtocol, (createQLPreviewProtocol())); return previewProtocol.data(); } #if USE(CFNETWORK) /* * The way QuickLook works is we pass it an NSURLConnectionDelegate callback object at creation * time. Then we pass it all the data as we receive it. Once we've downloaded the full URL, * QuickLook turns around and send us, through this delegate, the HTML version of the file which we * pass on to WebCore. The flag m_finishedLoadingDataIntoConverter in QuickLookHandle decides * whether to pass the data to QuickLook or WebCore. * * This works fine when using NS APIs, but when using CFNetwork, we don't have a * NSURLConnectionDelegate. So we create WebQuickLookHandleAsDelegate as an intermediate delegate * object and pass it to QLPreviewConverter. The proxy delegate then forwards the messages on to the * CFNetwork code. */ @interface WebQuickLookHandleAsDelegate : NSObject <NSURLConnectionDelegate> { CFURLConnectionRef m_connection; ResourceHandle* m_handle; } - (id)initWithConnection:(CFURLConnectionRef)connection handle:(ResourceHandle*)handle; - (void)clearHandle; @end @implementation WebQuickLookHandleAsDelegate - (id)initWithConnection:(CFURLConnectionRef)connection handle:(ResourceHandle*)handle { self = [super init]; if (!self) return nil; m_connection = connection; m_handle = handle; return self; } - (void)connection:(NSURLConnection *)connection didReceiveDataArray:(NSArray *)dataArray { UNUSED_PARAM(connection); if (!m_handle) return; LOG(Network, "WebQuickLookHandleAsDelegate::didReceiveDataArray()"); ResourceHandle::connectionClientCallbacks()->didReceiveDataArray(m_connection, reinterpret_cast<CFArrayRef>(dataArray), m_handle); } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data lengthReceived:(long long)lengthReceived { UNUSED_PARAM(connection); if (!m_handle) return; LOG(Network, "WebQuickLookHandleAsDelegate::didReceiveData() - data length = %ld", (long)[data length]); // QuickLook code sends us a nil data at times. The check below is the same as the one in // ResourceHandleMac.cpp added for a different bug. if (![data length]) return; ResourceHandle::connectionClientCallbacks()->didReceiveData(m_connection, reinterpret_cast<CFDataRef>(data), static_cast<int>(lengthReceived), m_handle); } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { UNUSED_PARAM(connection); if (!m_handle) return; LOG(Network, "WebQuickLookHandleAsDelegate::didFinishLoading()"); ResourceHandle::connectionClientCallbacks()->didFinishLoading(m_connection, m_handle); } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { UNUSED_PARAM(connection); if (!m_handle) return; LOG(Network, "WebQuickLookHandleAsDelegate::didFail()"); ResourceHandle::connectionClientCallbacks()->didFail(m_connection, reinterpret_cast<CFErrorRef>(error), m_handle); } - (void)clearHandle { m_handle = 0; } @end #endif namespace WebCore { static NSString *createTemporaryFileForQuickLook(NSString *fileName) { NSString *downloadDirectory = createTemporaryDirectory(@"QuickLookContent"); if (!downloadDirectory) return nil; NSString *contentPath = [downloadDirectory stringByAppendingPathComponent:fileName]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *uniqueContentPath = [fileManager _web_pathWithUniqueFilenameForPath:contentPath]; BOOL success = [fileManager _web_createFileAtPathWithIntermediateDirectories:uniqueContentPath contents:nil attributes:QLFileAttributes() directoryAttributes:QLDirectoryAttributes()]; return success ? uniqueContentPath : nil; } QuickLookHandle::QuickLookHandle(ResourceHandle* handle, NSURLConnection *connection, NSURLResponse *nsResponse, id delegate) : m_handle(handle) , m_converter(AdoptNS, [[QLPreviewConverterClass() alloc] initWithConnection:connection delegate:delegate response:nsResponse options:nil]) , m_delegate(AdoptNS, [delegate retain]) , m_finishedLoadingDataIntoConverter(false) , m_quicklookFileHandle(0) , m_nsResponse([m_converter.get() previewResponse]) { NSURL *previewRequestURL = [[m_converter.get() previewRequest] URL]; if (!applicationIsMobileSafari()) { // This keeps the QLPreviewConverter alive to serve any subresource requests. // It is removed by -[WebDataSource dealloc]. addQLPreviewConverterWithFileForURL(previewRequestURL, m_converter.get(), nil); return; } // QL consumes the incoming data, we need to store it so that it // can be opened in the handling application. NSString *quicklookContentPath = createTemporaryFileForQuickLook([m_converter.get() previewFileName]); LOG(Network, "QuickLookHandle::QuickLookHandle() - quicklookContentPath: %s", [quicklookContentPath UTF8String]); if (quicklookContentPath) { m_quicklookFileHandle = adoptNS([[NSFileHandle fileHandleForWritingAtPath:quicklookContentPath] retain]); // We must use the generated URL from m_converter's NSURLRequest object // so that it matches the URL removed from -[WebDataSource dealloc]. addQLPreviewConverterWithFileForURL(previewRequestURL, m_converter.get(), quicklookContentPath); } } PassOwnPtr<QuickLookHandle> QuickLookHandle::create(ResourceHandle* handle, NSURLConnection *connection, NSURLResponse *nsResponse, id delegate) { if (handle->firstRequest().isMainResourceRequest() && [WebCore::QLPreviewGetSupportedMIMETypesSet() containsObject:[nsResponse MIMEType]]) return adoptPtr(new QuickLookHandle(handle, connection, nsResponse, delegate)); return nullptr; } #if USE(CFNETWORK) PassOwnPtr<QuickLookHandle> QuickLookHandle::create(ResourceHandle* handle, CFURLConnectionRef connection, CFURLResponseRef cfResponse) { if (handle->firstRequest().isMainResourceRequest() && [WebCore::QLPreviewGetSupportedMIMETypesSet() containsObject:(NSString *)CFURLResponseGetMIMEType(cfResponse)]) { NSURLResponse *nsResponse = [NSURLResponse _responseWithCFURLResponse:cfResponse]; WebQuickLookHandleAsDelegate *delegate = [[[WebQuickLookHandleAsDelegate alloc] initWithConnection:connection handle:handle] autorelease]; return adoptPtr(new QuickLookHandle(handle, nil, nsResponse, delegate)); } return nullptr; } CFURLResponseRef QuickLookHandle::cfResponse() { return [m_nsResponse _CFURLResponse]; } #endif NSURLResponse *QuickLookHandle::nsResponse() { return m_nsResponse; } bool QuickLookHandle::didReceiveDataArray(CFArrayRef cfDataArray) { NSArray * const dataArray = (NSArray *)cfDataArray; if (m_finishedLoadingDataIntoConverter) return false; LOG(Network, "QuickLookHandle::didReceiveDataArray()"); [m_converter.get() appendDataArray:dataArray]; if (m_quicklookFileHandle) { for (NSData *data in dataArray) [m_quicklookFileHandle.get() writeData:data]; } return true; } bool QuickLookHandle::didReceiveData(CFDataRef cfData) { NSData * const data = (NSData *)cfData; if (m_finishedLoadingDataIntoConverter) return false; LOG(Network, "QuickLookHandle::didReceiveData()"); [m_converter.get() appendData:data]; if (m_quicklookFileHandle) [m_quicklookFileHandle.get() writeData:data]; return true; } bool QuickLookHandle::didFinishLoading() { if (m_finishedLoadingDataIntoConverter) return false; LOG(Network, "QuickLookHandle::didFinishLoading()"); m_finishedLoadingDataIntoConverter = YES; [m_converter.get() finishedAppendingData]; if (m_quicklookFileHandle) [m_quicklookFileHandle.get() closeFile]; return true; } void QuickLookHandle::didFail() { LOG(Network, "QuickLookHandle::didFail()"); m_quicklookFileHandle = nullptr; // removeQLPreviewConverterForURL deletes the temporary file created. removeQLPreviewConverterForURL([m_handle->firstRequest().nsURLRequest(DoNotUpdateHTTPBody) URL]); [m_converter.get() finishConverting]; m_converter = nullptr; } QuickLookHandle::~QuickLookHandle() { LOG(Network, "QuickLookHandle::~QuickLookHandle()"); if (m_quicklookFileHandle) { m_quicklookFileHandle = nullptr; removeQLPreviewConverterForURL([m_handle->firstRequest().nsURLRequest(DoNotUpdateHTTPBody) URL]); } m_converter = nullptr; [m_delegate.get() clearHandle]; } } #endif // USE(QUICK_LOOK)