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::writePlainText(const String& text)
372{
373    Pasteboard::writePlainText(m_pasteboard.get(), text);
374}
375
376void ClipboardMac::writeURL(const KURL& url, const String& title, Frame* frame)
377{
378    ASSERT(frame);
379    ASSERT(m_pasteboard);
380    Pasteboard::writeURL(m_pasteboard.get(), nil, url, title, frame);
381}
382
383#if ENABLE(DRAG_SUPPORT)
384void ClipboardMac::declareAndWriteDragImage(Element* element, const KURL& url, const String& title, Frame* frame)
385{
386    ASSERT(frame);
387    if (Page* page = frame->page())
388        page->dragController()->client()->declareAndWriteDragImage(m_pasteboard.get(), kit(element), url, title, frame);
389}
390#endif // ENABLE(DRAG_SUPPORT)
391
392DragImageRef ClipboardMac::createDragImage(IntPoint& loc) const
393{
394    NSPoint nsloc = {loc.x(), loc.y()};
395    DragImageRef result = dragNSImage(nsloc);
396    loc = (IntPoint)nsloc;
397    return result;
398}
399
400NSImage *ClipboardMac::dragNSImage(NSPoint& loc) const
401{
402    NSImage *result = nil;
403    if (m_dragImageElement) {
404        if (m_frame) {
405            NSRect imageRect;
406            NSRect elementRect;
407            result = m_frame->snapshotDragImage(m_dragImageElement.get(), &imageRect, &elementRect);
408            // Client specifies point relative to element, not the whole image, which may include child
409            // layers spread out all over the place.
410            loc.x = elementRect.origin.x - imageRect.origin.x + m_dragLoc.x();
411            loc.y = elementRect.origin.y - imageRect.origin.y + m_dragLoc.y();
412            loc.y = imageRect.size.height - loc.y;
413        }
414    } else if (m_dragImage) {
415        result = m_dragImage->image()->getNSImage();
416
417        loc = m_dragLoc;
418        loc.y = [result size].height - loc.y;
419    }
420    return result;
421}
422
423}
424