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