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