WebCore: dataTransfer.types() should not return Files if file list is empty in the...
[WebKit-https.git] / WebCore / platform / mac / ClipboardMac.mm
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
44 typedef unsigned NSUInteger;
45 #endif
46
47 namespace WebCore {
48
49 ClipboardMac::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
57 ClipboardMac::~ClipboardMac()
58 {
59 }
60
61 bool ClipboardMac::hasData()
62 {
63     return m_pasteboard && [m_pasteboard.get() types] && [[m_pasteboard.get() types] count] > 0;
64 }
65     
66 static 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
96 static 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
107 static 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     else if ([cocoaType isEqualToString:NSURLPboardType])
113         resultTypes.add("text/uri-list");
114     else if ([cocoaType isEqualToString:NSFilenamesPboardType]) {
115         // If file list is empty, add nothing.
116         // Note that there is a chance that the file list count could have changed since we grabbed the types array.
117         // However, this is not really an issue for us doing a sanity check here.
118         NSArray *fileList = [pasteboard propertyListForType:NSFilenamesPboardType];
119         if ([fileList count]) {
120             // It is unknown if NSFilenamesPboardType always implies NSURLPboardType in Cocoa,
121             // but NSFilenamesPboardType should imply both 'text/uri-list' and 'Files'
122             resultTypes.add("text/uri-list");
123             resultTypes.add("Files");
124         }
125     } else if (String utiType = utiTypeFromCocoaType(cocoaType))
126         resultTypes.add(utiType);
127     else {
128         // No mapping, just pass the whole string though
129         resultTypes.add(cocoaType);
130     }
131 }
132
133 void ClipboardMac::clearData(const String& type)
134 {
135     if (policy() != ClipboardWritable)
136         return;
137
138     // note NSPasteboard enforces changeCount itself on writing - can't write if not the owner
139
140     NSString *cocoaType = cocoaTypeFromHTMLClipboardType(type);
141     if (cocoaType)
142         [m_pasteboard.get() setString:@"" forType:cocoaType];
143 }
144
145 void ClipboardMac::clearAllData()
146 {
147     if (policy() != ClipboardWritable)
148         return;
149
150     // note NSPasteboard enforces changeCount itself on writing - can't write if not the owner
151
152     [m_pasteboard.get() declareTypes:[NSArray array] owner:nil];
153 }
154
155 static NSArray *absoluteURLsFromPasteboardFilenames(NSPasteboard* pasteboard, bool onlyFirstURL = false)
156 {
157     NSArray *fileList = [pasteboard propertyListForType:NSFilenamesPboardType];
158
159     // FIXME: Why does this code need to guard against bad values on the pasteboard?
160     ASSERT(!fileList || [fileList isKindOfClass:[NSArray class]]);
161     if (!fileList || ![fileList isKindOfClass:[NSArray class]] || ![fileList count])
162         return nil;
163
164     NSUInteger count = onlyFirstURL ? 1 : [fileList count];
165     NSMutableArray *urls = [NSMutableArray array];
166     for (NSUInteger i = 0; i < count; i++) {
167         NSString *string = [fileList objectAtIndex:i];
168
169         ASSERT([string isKindOfClass:[NSString class]]);  // Added to understand why this if code is here
170         if (![string isKindOfClass:[NSString class]])
171             return nil; // Non-string object in the list, bail out!  FIXME: When can this happen?
172
173         NSURL *url = [NSURL fileURLWithPath:string];
174         [urls addObject:[url absoluteString]];
175     }
176     return urls;
177 }
178
179 static NSArray *absoluteURLsFromPasteboard(NSPasteboard* pasteboard, bool onlyFirstURL = false)
180 {
181     // NOTE: We must always check [availableTypes containsObject:] before accessing pasteboard data
182     // or CoreFoundation will printf when there is not data of the corresponding type.
183     NSArray *availableTypes = [pasteboard types];
184
185     // Try NSFilenamesPboardType because it contains a list
186     if ([availableTypes containsObject:NSFilenamesPboardType]) {
187         if (NSArray* absoluteURLs = absoluteURLsFromPasteboardFilenames(pasteboard, onlyFirstURL))
188             return absoluteURLs;
189     }
190
191     // Fallback to NSURLPboardType (which is a single URL)
192     if ([availableTypes containsObject:NSURLPboardType]) {
193         if (NSURL *url = [NSURL URLFromPasteboard:pasteboard])
194             return [NSArray arrayWithObject:[url absoluteString]];
195     }
196
197     // No file paths on the pasteboard, return nil
198     return nil;
199 }
200
201 String ClipboardMac::getData(const String& type, bool& success) const
202 {
203     success = false;
204     if (policy() != ClipboardReadable)
205         return String();
206
207     NSString *cocoaType = cocoaTypeFromHTMLClipboardType(type);
208     NSString *cocoaValue = nil;
209
210     // Grab the value off the pasteboard corresponding to the cocoaType
211     if ([cocoaType isEqualToString:NSURLPboardType]) {
212         // "URL" and "text/url-list" both map to NSURLPboardType in cocoaTypeFromHTMLClipboardType(), "URL" only wants the first URL
213         bool onlyFirstURL = (type == "URL");
214         NSArray *absoluteURLs = absoluteURLsFromPasteboard(m_pasteboard.get(), onlyFirstURL);
215         cocoaValue = [absoluteURLs componentsJoinedByString:@"\n"];
216     } else if ([cocoaType isEqualToString:NSStringPboardType]) {
217         cocoaValue = [[m_pasteboard.get() stringForType:cocoaType] precomposedStringWithCanonicalMapping];
218     } else if (cocoaType)
219         cocoaValue = [m_pasteboard.get() stringForType:cocoaType];
220
221     // Enforce changeCount ourselves for security.  We check after reading instead of before to be
222     // sure it doesn't change between our testing the change count and accessing the data.
223     if (cocoaValue && m_changeCount == [m_pasteboard.get() changeCount]) {
224         success = true;
225         return cocoaValue;
226     }
227
228     return String();
229 }
230
231 bool ClipboardMac::setData(const String &type, const String &data)
232 {
233     if (policy() != ClipboardWritable)
234         return false;
235     // note NSPasteboard enforces changeCount itself on writing - can't write if not the owner
236
237     NSString *cocoaType = cocoaTypeFromHTMLClipboardType(type);
238     NSString *cocoaData = data;
239
240     if ([cocoaType isEqualToString:NSURLPboardType]) {
241         [m_pasteboard.get() addTypes:[NSArray arrayWithObject:NSURLPboardType] owner:nil];
242         NSURL *url = [[NSURL alloc] initWithString:cocoaData];
243         [url writeToPasteboard:m_pasteboard.get()];
244
245         if ([url isFileURL] && m_frame->document()->securityOrigin()->canLoadLocalResources()) {
246             [m_pasteboard.get() addTypes:[NSArray arrayWithObject:NSFilenamesPboardType] owner:nil];
247             NSArray *fileList = [NSArray arrayWithObject:[url path]];
248             [m_pasteboard.get() setPropertyList:fileList forType:NSFilenamesPboardType];
249         }
250
251         [url release];
252         return true;
253     }
254
255     if (cocoaType) {
256         // everything else we know of goes on the pboard as a string
257         [m_pasteboard.get() addTypes:[NSArray arrayWithObject:cocoaType] owner:nil];
258         return [m_pasteboard.get() setString:cocoaData forType:cocoaType];
259     }
260
261     return false;
262 }
263
264 HashSet<String> ClipboardMac::types() const
265 {
266     if (policy() != ClipboardReadable && policy() != ClipboardTypesReadable)
267         return HashSet<String>();
268
269     NSArray *types = [m_pasteboard.get() types];
270
271     // Enforce changeCount ourselves for security.  We check after reading instead of before to be
272     // sure it doesn't change between our testing the change count and accessing the data.
273     if (m_changeCount != [m_pasteboard.get() changeCount])
274         return HashSet<String>();
275
276     HashSet<String> result;
277     NSUInteger count = [types count];
278     // FIXME: This loop could be split into two stages. One which adds all the HTML5 specified types
279     // and a second which adds all the extra types from the cocoa clipboard (which is Mac-only behavior).
280     for (NSUInteger i = 0; i < count; i++) {
281         NSString *pbType = [types objectAtIndex:i];
282         if ([pbType isEqualToString:@"NeXT plain ascii pasteboard type"])
283             continue;   // skip this ancient type that gets auto-supplied by some system conversion
284
285         addHTMLClipboardTypesForCocoaType(result, pbType, m_pasteboard.get());
286     }
287
288     return result;
289 }
290
291 // FIXME: We could cache the computed fileList if necessary
292 // Currently each access gets a new copy, setData() modifications to the
293 // clipboard are not reflected in any FileList objects the page has accessed and stored
294 PassRefPtr<FileList> ClipboardMac::files() const
295 {
296     if (policy() != ClipboardReadable)
297         return FileList::create();
298
299     NSArray *absoluteURLs = absoluteURLsFromPasteboardFilenames(m_pasteboard.get());
300     NSUInteger count = [absoluteURLs count];
301
302     RefPtr<FileList> fileList = FileList::create();
303     for (NSUInteger x = 0; x < count; x++) {
304         NSURL *absoluteURL = [NSURL URLWithString:[absoluteURLs objectAtIndex:x]];
305         ASSERT([absoluteURL isFileURL]);
306         fileList->append(File::create([absoluteURL path]));
307     }
308     return fileList.release(); // We will always return a FileList, sometimes empty
309 }
310
311 // The rest of these getters don't really have any impact on security, so for now make no checks
312
313 void ClipboardMac::setDragImage(CachedImage* img, const IntPoint &loc)
314 {
315     setDragImage(img, 0, loc);
316 }
317
318 void ClipboardMac::setDragImageElement(Node *node, const IntPoint &loc)
319 {
320     setDragImage(0, node, loc);
321 }
322
323 void ClipboardMac::setDragImage(CachedImage* image, Node *node, const IntPoint &loc)
324 {
325     if (policy() == ClipboardImageWritable || policy() == ClipboardWritable) {
326         if (m_dragImage)
327             m_dragImage->removeClient(this);
328         m_dragImage = image;
329         if (m_dragImage)
330             m_dragImage->addClient(this);
331
332         m_dragLoc = loc;
333         m_dragImageElement = node;
334         
335         if (dragStarted() && m_changeCount == [m_pasteboard.get() changeCount]) {
336             NSPoint cocoaLoc;
337             NSImage* cocoaImage = dragNSImage(cocoaLoc);
338             if (cocoaImage) {
339                 // Dashboard wants to be able to set the drag image during dragging, but Cocoa does not allow this.
340                 // Instead we must drop down to the CoreGraphics API.
341                 wkSetDragImage(cocoaImage, cocoaLoc);
342
343                 // Hack: We must post an event to wake up the NSDragManager, which is sitting in a nextEvent call
344                 // up the stack from us because the CoreFoundation drag manager does not use the run loop by itself.
345                 // This is the most innocuous event to use, per Kristen Forster.
346                 NSEvent* ev = [NSEvent mouseEventWithType:NSMouseMoved location:NSZeroPoint
347                     modifierFlags:0 timestamp:0 windowNumber:0 context:nil eventNumber:0 clickCount:0 pressure:0];
348                 [NSApp postEvent:ev atStart:YES];
349             }
350         }
351         // Else either 1) we haven't started dragging yet, so we rely on the part to install this drag image
352         // as part of getting the drag kicked off, or 2) Someone kept a ref to the clipboard and is trying to
353         // set the image way too late.
354     }
355 }
356     
357 void ClipboardMac::writeRange(Range* range, Frame* frame)
358 {
359     ASSERT(range);
360     ASSERT(frame);
361     Pasteboard::writeSelection(m_pasteboard.get(), range, frame->editor()->smartInsertDeleteEnabled() && frame->selectionGranularity() == WordGranularity, frame);
362 }
363     
364 void ClipboardMac::writeURL(const KURL& url, const String& title, Frame* frame)
365 {   
366     ASSERT(frame);
367     ASSERT(m_pasteboard);
368     Pasteboard::writeURL(m_pasteboard.get(), nil, url, title, frame);
369 }
370     
371 void ClipboardMac::declareAndWriteDragImage(Element* element, const KURL& url, const String& title, Frame* frame)
372 {
373     ASSERT(frame);
374     if (Page* page = frame->page())
375         page->dragController()->client()->declareAndWriteDragImage(m_pasteboard.get(), kit(element), url, title, frame);
376 }
377     
378 DragImageRef ClipboardMac::createDragImage(IntPoint& loc) const
379 {
380     NSPoint nsloc = {loc.x(), loc.y()};
381     DragImageRef result = dragNSImage(nsloc);
382     loc = (IntPoint)nsloc;
383     return result;
384 }
385     
386 NSImage *ClipboardMac::dragNSImage(NSPoint& loc) const
387 {
388     NSImage *result = nil;
389     if (m_dragImageElement) {
390         if (m_frame) {
391             NSRect imageRect;
392             NSRect elementRect;
393             result = m_frame->snapshotDragImage(m_dragImageElement.get(), &imageRect, &elementRect);
394             // Client specifies point relative to element, not the whole image, which may include child
395             // layers spread out all over the place.
396             loc.x = elementRect.origin.x - imageRect.origin.x + m_dragLoc.x();
397             loc.y = elementRect.origin.y - imageRect.origin.y + m_dragLoc.y();
398             loc.y = imageRect.size.height - loc.y;
399         }
400     } else if (m_dragImage) {
401         result = m_dragImage->image()->getNSImage();
402         
403         loc = m_dragLoc;
404         loc.y = [result size].height - loc.y;
405     }
406     return result;
407 }
408
409 }