ClipboardMac.mm revision cac0f67c402d107cdb10971b95719e2ff9c7c76b
1/* 2 * Copyright (C) 2004, 2005, 2006, 2008 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26#import "config.h" 27#import "ClipboardMac.h" 28 29#import "DOMElementInternal.h" 30#import "DragClient.h" 31#import "DragController.h" 32#import "Editor.h" 33#import "FoundationExtras.h" 34#import "FileList.h" 35#import "Frame.h" 36#import "Image.h" 37#import "Page.h" 38#import "Pasteboard.h" 39#import "RenderImage.h" 40#import "SecurityOrigin.h" 41#import "WebCoreSystemInterface.h" 42 43#ifdef BUILDING_ON_TIGER 44typedef unsigned NSUInteger; 45#endif 46 47namespace WebCore { 48 49ClipboardMac::ClipboardMac(bool forDragging, NSPasteboard *pasteboard, ClipboardAccessPolicy policy, Frame *frame) 50 : Clipboard(policy, forDragging) 51 , m_pasteboard(pasteboard) 52 , m_frame(frame) 53{ 54 m_changeCount = [m_pasteboard.get() changeCount]; 55} 56 57ClipboardMac::~ClipboardMac() 58{ 59} 60 61bool ClipboardMac::hasData() 62{ 63 return m_pasteboard && [m_pasteboard.get() types] && [[m_pasteboard.get() types] count] > 0; 64} 65 66static NSString *cocoaTypeFromHTMLClipboardType(const String& type) 67{ 68 String qType = type.stripWhiteSpace(); 69 70 // two special cases for IE compatibility 71 if (qType == "Text") 72 return NSStringPboardType; 73 if (qType == "URL") 74 return NSURLPboardType; 75 76 // Ignore any trailing charset - JS strings are Unicode, which encapsulates the charset issue 77 if (qType == "text/plain" || qType.startsWith("text/plain;")) 78 return NSStringPboardType; 79 if (qType == "text/uri-list") 80 // special case because UTI doesn't work with Cocoa's URL type 81 return NSURLPboardType; // note special case in getData to read NSFilenamesType 82 83 // Try UTI now 84 NSString *mimeType = qType; 85 RetainPtr<CFStringRef> utiType(AdoptCF, UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (CFStringRef)mimeType, NULL)); 86 if (utiType) { 87 CFStringRef pbType = UTTypeCopyPreferredTagWithClass(utiType.get(), kUTTagClassNSPboardType); 88 if (pbType) 89 return HardAutorelease(pbType); 90 } 91 92 // No mapping, just pass the whole string though 93 return qType; 94} 95 96static String utiTypeFromCocoaType(NSString *type) 97{ 98 RetainPtr<CFStringRef> utiType(AdoptCF, UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, (CFStringRef)type, NULL)); 99 if (utiType) { 100 RetainPtr<CFStringRef> mimeType(AdoptCF, UTTypeCopyPreferredTagWithClass(utiType.get(), kUTTagClassMIMEType)); 101 if (mimeType) 102 return String(mimeType.get()); 103 } 104 return String(); 105} 106 107static void addHTMLClipboardTypesForCocoaType(HashSet<String>& resultTypes, NSString *cocoaType, NSPasteboard *pasteboard) 108{ 109 // UTI may not do these right, so make sure we get the right, predictable result 110 if ([cocoaType isEqualToString:NSStringPboardType]) { 111 resultTypes.add("text/plain"); 112 return; 113 } 114 if ([cocoaType isEqualToString:NSURLPboardType]) { 115 resultTypes.add("text/uri-list"); 116 return; 117 } 118 if ([cocoaType isEqualToString:NSFilenamesPboardType]) { 119 // If file list is empty, add nothing. 120 // Note that there is a chance that the file list count could have changed since we grabbed the types array. 121 // However, this is not really an issue for us doing a sanity check here. 122 NSArray *fileList = [pasteboard propertyListForType:NSFilenamesPboardType]; 123 if ([fileList count]) { 124 // It is unknown if NSFilenamesPboardType always implies NSURLPboardType in Cocoa, 125 // but NSFilenamesPboardType should imply both 'text/uri-list' and 'Files' 126 resultTypes.add("text/uri-list"); 127 resultTypes.add("Files"); 128 } 129 return; 130 } 131 String utiType = utiTypeFromCocoaType(cocoaType); 132 if (!utiType.isEmpty()) { 133 resultTypes.add(utiType); 134 return; 135 } 136 // No mapping, just pass the whole string though 137 resultTypes.add(cocoaType); 138} 139 140void ClipboardMac::clearData(const String& type) 141{ 142 if (policy() != ClipboardWritable) 143 return; 144 145 // note NSPasteboard enforces changeCount itself on writing - can't write if not the owner 146 147 NSString *cocoaType = cocoaTypeFromHTMLClipboardType(type); 148 if (cocoaType) 149 [m_pasteboard.get() setString:@"" forType:cocoaType]; 150} 151 152void ClipboardMac::clearAllData() 153{ 154 if (policy() != ClipboardWritable) 155 return; 156 157 // note NSPasteboard enforces changeCount itself on writing - can't write if not the owner 158 159 [m_pasteboard.get() declareTypes:[NSArray array] owner:nil]; 160} 161 162static NSArray *absoluteURLsFromPasteboardFilenames(NSPasteboard* pasteboard, bool onlyFirstURL = false) 163{ 164 NSArray *fileList = [pasteboard propertyListForType:NSFilenamesPboardType]; 165 166 // FIXME: Why does this code need to guard against bad values on the pasteboard? 167 ASSERT(!fileList || [fileList isKindOfClass:[NSArray class]]); 168 if (!fileList || ![fileList isKindOfClass:[NSArray class]] || ![fileList count]) 169 return nil; 170 171 NSUInteger count = onlyFirstURL ? 1 : [fileList count]; 172 NSMutableArray *urls = [NSMutableArray array]; 173 for (NSUInteger i = 0; i < count; i++) { 174 NSString *string = [fileList objectAtIndex:i]; 175 176 ASSERT([string isKindOfClass:[NSString class]]); // Added to understand why this if code is here 177 if (![string isKindOfClass:[NSString class]]) 178 return nil; // Non-string object in the list, bail out! FIXME: When can this happen? 179 180 NSURL *url = [NSURL fileURLWithPath:string]; 181 [urls addObject:[url absoluteString]]; 182 } 183 return urls; 184} 185 186static NSArray *absoluteURLsFromPasteboard(NSPasteboard* pasteboard, bool onlyFirstURL = false) 187{ 188 // NOTE: We must always check [availableTypes containsObject:] before accessing pasteboard data 189 // or CoreFoundation will printf when there is not data of the corresponding type. 190 NSArray *availableTypes = [pasteboard types]; 191 192 // Try NSFilenamesPboardType because it contains a list 193 if ([availableTypes containsObject:NSFilenamesPboardType]) { 194 if (NSArray* absoluteURLs = absoluteURLsFromPasteboardFilenames(pasteboard, onlyFirstURL)) 195 return absoluteURLs; 196 } 197 198 // Fallback to NSURLPboardType (which is a single URL) 199 if ([availableTypes containsObject:NSURLPboardType]) { 200 if (NSURL *url = [NSURL URLFromPasteboard:pasteboard]) 201 return [NSArray arrayWithObject:[url absoluteString]]; 202 } 203 204 // No file paths on the pasteboard, return nil 205 return nil; 206} 207 208String ClipboardMac::getData(const String& type, bool& success) const 209{ 210 success = false; 211 if (policy() != ClipboardReadable) 212 return String(); 213 214 NSString *cocoaType = cocoaTypeFromHTMLClipboardType(type); 215 NSString *cocoaValue = nil; 216 217 // Grab the value off the pasteboard corresponding to the cocoaType 218 if ([cocoaType isEqualToString:NSURLPboardType]) { 219 // "URL" and "text/url-list" both map to NSURLPboardType in cocoaTypeFromHTMLClipboardType(), "URL" only wants the first URL 220 bool onlyFirstURL = (type == "URL"); 221 NSArray *absoluteURLs = absoluteURLsFromPasteboard(m_pasteboard.get(), onlyFirstURL); 222 cocoaValue = [absoluteURLs componentsJoinedByString:@"\n"]; 223 } else if ([cocoaType isEqualToString:NSStringPboardType]) { 224 cocoaValue = [[m_pasteboard.get() stringForType:cocoaType] precomposedStringWithCanonicalMapping]; 225 } else if (cocoaType) 226 cocoaValue = [m_pasteboard.get() stringForType:cocoaType]; 227 228 // Enforce changeCount ourselves for security. We check after reading instead of before to be 229 // sure it doesn't change between our testing the change count and accessing the data. 230 if (cocoaValue && m_changeCount == [m_pasteboard.get() changeCount]) { 231 success = true; 232 return cocoaValue; 233 } 234 235 return String(); 236} 237 238bool ClipboardMac::setData(const String &type, const String &data) 239{ 240 if (policy() != ClipboardWritable) 241 return false; 242 // note NSPasteboard enforces changeCount itself on writing - can't write if not the owner 243 244 NSString *cocoaType = cocoaTypeFromHTMLClipboardType(type); 245 NSString *cocoaData = data; 246 247 if ([cocoaType isEqualToString:NSURLPboardType]) { 248 [m_pasteboard.get() addTypes:[NSArray arrayWithObject:NSURLPboardType] owner:nil]; 249 NSURL *url = [[NSURL alloc] initWithString:cocoaData]; 250 [url writeToPasteboard:m_pasteboard.get()]; 251 252 if ([url isFileURL] && m_frame->document()->securityOrigin()->canLoadLocalResources()) { 253 [m_pasteboard.get() addTypes:[NSArray arrayWithObject:NSFilenamesPboardType] owner:nil]; 254 NSArray *fileList = [NSArray arrayWithObject:[url path]]; 255 [m_pasteboard.get() setPropertyList:fileList forType:NSFilenamesPboardType]; 256 } 257 258 [url release]; 259 return true; 260 } 261 262 if (cocoaType) { 263 // everything else we know of goes on the pboard as a string 264 [m_pasteboard.get() addTypes:[NSArray arrayWithObject:cocoaType] owner:nil]; 265 return [m_pasteboard.get() setString:cocoaData forType:cocoaType]; 266 } 267 268 return false; 269} 270 271HashSet<String> ClipboardMac::types() const 272{ 273 if (policy() != ClipboardReadable && policy() != ClipboardTypesReadable) 274 return HashSet<String>(); 275 276 NSArray *types = [m_pasteboard.get() types]; 277 278 // Enforce changeCount ourselves for security. We check after reading instead of before to be 279 // sure it doesn't change between our testing the change count and accessing the data. 280 if (m_changeCount != [m_pasteboard.get() changeCount]) 281 return HashSet<String>(); 282 283 HashSet<String> result; 284 NSUInteger count = [types count]; 285 // FIXME: This loop could be split into two stages. One which adds all the HTML5 specified types 286 // and a second which adds all the extra types from the cocoa clipboard (which is Mac-only behavior). 287 for (NSUInteger i = 0; i < count; i++) { 288 NSString *pbType = [types objectAtIndex:i]; 289 if ([pbType isEqualToString:@"NeXT plain ascii pasteboard type"]) 290 continue; // skip this ancient type that gets auto-supplied by some system conversion 291 292 addHTMLClipboardTypesForCocoaType(result, pbType, m_pasteboard.get()); 293 } 294 295 return result; 296} 297 298// FIXME: We could cache the computed fileList if necessary 299// Currently each access gets a new copy, setData() modifications to the 300// clipboard are not reflected in any FileList objects the page has accessed and stored 301PassRefPtr<FileList> ClipboardMac::files() const 302{ 303 if (policy() != ClipboardReadable) 304 return FileList::create(); 305 306 NSArray *absoluteURLs = absoluteURLsFromPasteboardFilenames(m_pasteboard.get()); 307 NSUInteger count = [absoluteURLs count]; 308 309 RefPtr<FileList> fileList = FileList::create(); 310 for (NSUInteger x = 0; x < count; x++) { 311 NSURL *absoluteURL = [NSURL URLWithString:[absoluteURLs objectAtIndex:x]]; 312 ASSERT([absoluteURL isFileURL]); 313 fileList->append(File::create([absoluteURL path])); 314 } 315 return fileList.release(); // We will always return a FileList, sometimes empty 316} 317 318// The rest of these getters don't really have any impact on security, so for now make no checks 319 320void ClipboardMac::setDragImage(CachedImage* img, const IntPoint &loc) 321{ 322 setDragImage(img, 0, loc); 323} 324 325void ClipboardMac::setDragImageElement(Node *node, const IntPoint &loc) 326{ 327 setDragImage(0, node, loc); 328} 329 330void ClipboardMac::setDragImage(CachedImage* image, Node *node, const IntPoint &loc) 331{ 332 if (policy() == ClipboardImageWritable || policy() == ClipboardWritable) { 333 if (m_dragImage) 334 m_dragImage->removeClient(this); 335 m_dragImage = image; 336 if (m_dragImage) 337 m_dragImage->addClient(this); 338 339 m_dragLoc = loc; 340 m_dragImageElement = node; 341 342 if (dragStarted() && m_changeCount == [m_pasteboard.get() changeCount]) { 343 NSPoint cocoaLoc; 344 NSImage* cocoaImage = dragNSImage(cocoaLoc); 345 if (cocoaImage) { 346 // Dashboard wants to be able to set the drag image during dragging, but Cocoa does not allow this. 347 // Instead we must drop down to the CoreGraphics API. 348 wkSetDragImage(cocoaImage, cocoaLoc); 349 350 // Hack: We must post an event to wake up the NSDragManager, which is sitting in a nextEvent call 351 // up the stack from us because the CoreFoundation drag manager does not use the run loop by itself. 352 // This is the most innocuous event to use, per Kristen Forster. 353 NSEvent* ev = [NSEvent mouseEventWithType:NSMouseMoved location:NSZeroPoint 354 modifierFlags:0 timestamp:0 windowNumber:0 context:nil eventNumber:0 clickCount:0 pressure:0]; 355 [NSApp postEvent:ev atStart:YES]; 356 } 357 } 358 // Else either 1) we haven't started dragging yet, so we rely on the part to install this drag image 359 // as part of getting the drag kicked off, or 2) Someone kept a ref to the clipboard and is trying to 360 // set the image way too late. 361 } 362} 363 364void ClipboardMac::writeRange(Range* range, Frame* frame) 365{ 366 ASSERT(range); 367 ASSERT(frame); 368 Pasteboard::writeSelection(m_pasteboard.get(), range, frame->editor()->smartInsertDeleteEnabled() && frame->selectionGranularity() == WordGranularity, frame); 369} 370 371void ClipboardMac::writeURL(const KURL& url, const String& title, Frame* frame) 372{ 373 ASSERT(frame); 374 ASSERT(m_pasteboard); 375 Pasteboard::writeURL(m_pasteboard.get(), nil, url, title, frame); 376} 377 378#if ENABLE(DRAG_SUPPORT) 379void ClipboardMac::declareAndWriteDragImage(Element* element, const KURL& url, const String& title, Frame* frame) 380{ 381 ASSERT(frame); 382 if (Page* page = frame->page()) 383 page->dragController()->client()->declareAndWriteDragImage(m_pasteboard.get(), kit(element), url, title, frame); 384} 385#endif // ENABLE(DRAG_SUPPORT) 386 387DragImageRef ClipboardMac::createDragImage(IntPoint& loc) const 388{ 389 NSPoint nsloc = {loc.x(), loc.y()}; 390 DragImageRef result = dragNSImage(nsloc); 391 loc = (IntPoint)nsloc; 392 return result; 393} 394 395NSImage *ClipboardMac::dragNSImage(NSPoint& loc) const 396{ 397 NSImage *result = nil; 398 if (m_dragImageElement) { 399 if (m_frame) { 400 NSRect imageRect; 401 NSRect elementRect; 402 result = m_frame->snapshotDragImage(m_dragImageElement.get(), &imageRect, &elementRect); 403 // Client specifies point relative to element, not the whole image, which may include child 404 // layers spread out all over the place. 405 loc.x = elementRect.origin.x - imageRect.origin.x + m_dragLoc.x(); 406 loc.y = elementRect.origin.y - imageRect.origin.y + m_dragLoc.y(); 407 loc.y = imageRect.size.height - loc.y; 408 } 409 } else if (m_dragImage) { 410 result = m_dragImage->image()->getNSImage(); 411 412 loc = m_dragLoc; 413 loc.y = [result size].height - loc.y; 414 } 415 return result; 416} 417 418} 419