Dragging & dropping a file creates an attachment element even when it's disabled
[WebKit-https.git] / Source / WebCore / editing / mac / EditorMac.mm
1 /*
2  * Copyright (C) 2006-2016 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 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 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 "Editor.h"
28
29 #import "Blob.h"
30 #import "CSSPrimitiveValueMappings.h"
31 #import "CSSValuePool.h"
32 #import "DOMURL.h"
33 #import "DataTransfer.h"
34 #import "DocumentFragment.h"
35 #import "DocumentLoader.h"
36 #import "Editing.h"
37 #import "Editor.h"
38 #import "EditorClient.h"
39 #import "File.h"
40 #import "FontCascade.h"
41 #import "Frame.h"
42 #import "FrameLoader.h"
43 #import "FrameLoaderClient.h"
44 #import "FrameView.h"
45 #import "HTMLAnchorElement.h"
46 #import "HTMLAttachmentElement.h"
47 #import "HTMLConverter.h"
48 #import "HTMLElement.h"
49 #import "HTMLImageElement.h"
50 #import "HTMLNames.h"
51 #import "LegacyWebArchive.h"
52 #import "MIMETypeRegistry.h"
53 #import "NodeTraversal.h"
54 #import "Page.h"
55 #import "Pasteboard.h"
56 #import "PasteboardStrategy.h"
57 #import "PlatformStrategies.h"
58 #import "Range.h"
59 #import "RenderBlock.h"
60 #import "RenderImage.h"
61 #import "RuntimeApplicationChecks.h"
62 #import "RuntimeEnabledFeatures.h"
63 #import "Settings.h"
64 #import "StyleProperties.h"
65 #import "Text.h"
66 #import "TypingCommand.h"
67 #import "WebNSAttributedStringExtras.h"
68 #import "markup.h"
69 #import <pal/system/Sound.h>
70
71 namespace WebCore {
72
73 using namespace HTMLNames;
74
75 void Editor::showFontPanel()
76 {
77     [[NSFontManager sharedFontManager] orderFrontFontPanel:nil];
78 }
79
80 void Editor::showStylesPanel()
81 {
82     [[NSFontManager sharedFontManager] orderFrontStylesPanel:nil];
83 }
84
85 void Editor::showColorPanel()
86 {
87     [[NSApplication sharedApplication] orderFrontColorPanel:nil];
88 }
89
90 void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText, MailBlockquoteHandling mailBlockquoteHandling)
91 {
92     RefPtr<Range> range = selectedRange();
93
94 #pragma clang diagnostic push
95 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
96     // FIXME: How can this hard-coded pasteboard name be right, given that the passed-in pasteboard has a name?
97     client()->setInsertionPasteboard(NSGeneralPboard);
98 #pragma clang diagnostic pop
99
100     bool chosePlainText;
101     RefPtr<DocumentFragment> fragment = webContentFromPasteboard(*pasteboard, *range, allowPlainText, chosePlainText);
102
103     if (fragment && shouldInsertFragment(*fragment, range.get(), EditorInsertAction::Pasted))
104         pasteAsFragment(fragment.releaseNonNull(), canSmartReplaceWithPasteboard(*pasteboard), false, mailBlockquoteHandling);
105
106     client()->setInsertionPasteboard(String());
107 }
108
109 bool Editor::canCopyExcludingStandaloneImages()
110 {
111     const VisibleSelection& selection = m_frame.selection().selection();
112     return selection.isRange() && !selection.isInPasswordField();
113 }
114
115 void Editor::takeFindStringFromSelection()
116 {
117     if (!canCopyExcludingStandaloneImages()) {
118         PAL::systemBeep();
119         return;
120     }
121
122     Vector<String> types;
123     types.append(String(NSStringPboardType));
124 #pragma clang diagnostic push
125 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
126     platformStrategies()->pasteboardStrategy()->setTypes(types, NSFindPboard);
127     platformStrategies()->pasteboardStrategy()->setStringForType(m_frame.displayStringModifiedByEncoding(selectedTextForDataTransfer()), NSStringPboardType, NSFindPboard);
128 #pragma clang diagnostic pop
129 }
130
131 void Editor::readSelectionFromPasteboard(const String& pasteboardName, MailBlockquoteHandling mailBlockquoteHandling)
132 {
133     Pasteboard pasteboard(pasteboardName);
134     if (m_frame.selection().selection().isContentRichlyEditable())
135         pasteWithPasteboard(&pasteboard, true, mailBlockquoteHandling);
136     else
137         pasteAsPlainTextWithPasteboard(pasteboard);
138 }
139
140 static void maybeCopyNodeAttributesToFragment(const Node& node, DocumentFragment& fragment)
141 {
142     // This is only supported for single-Node fragments.
143     Node* firstChild = fragment.firstChild();
144     if (!firstChild || firstChild != fragment.lastChild())
145         return;
146
147     // And only supported for HTML elements.
148     if (!node.isHTMLElement() || !firstChild->isHTMLElement())
149         return;
150
151     // And only if the source Element and destination Element have the same HTML tag name.
152     const HTMLElement& oldElement = downcast<HTMLElement>(node);
153     HTMLElement& newElement = downcast<HTMLElement>(*firstChild);
154     if (oldElement.localName() != newElement.localName())
155         return;
156
157     for (const Attribute& attribute : oldElement.attributesIterator()) {
158         if (newElement.hasAttribute(attribute.name()))
159             continue;
160         newElement.setAttribute(attribute.name(), attribute.value());
161     }
162 }
163
164 void Editor::replaceNodeFromPasteboard(Node* node, const String& pasteboardName)
165 {
166     ASSERT(node);
167
168     if (&node->document() != m_frame.document())
169         return;
170
171     Ref<Frame> protector(m_frame);
172     RefPtr<Range> range = Range::create(node->document(), Position(node, Position::PositionIsBeforeAnchor), Position(node, Position::PositionIsAfterAnchor));
173     m_frame.selection().setSelection(VisibleSelection(*range), FrameSelection::DoNotSetFocus);
174
175     Pasteboard pasteboard(pasteboardName);
176
177     if (!m_frame.selection().selection().isContentRichlyEditable()) {
178         pasteAsPlainTextWithPasteboard(pasteboard);
179         return;
180     }
181
182 #pragma clang diagnostic push
183 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
184     // FIXME: How can this hard-coded pasteboard name be right, given that the passed-in pasteboard has a name?
185     client()->setInsertionPasteboard(NSGeneralPboard);
186 #pragma clang diagnostic pop
187
188     bool chosePlainText;
189     if (RefPtr<DocumentFragment> fragment = webContentFromPasteboard(pasteboard, *range, true, chosePlainText)) {
190         maybeCopyNodeAttributesToFragment(*node, *fragment);
191         if (shouldInsertFragment(*fragment, range.get(), EditorInsertAction::Pasted))
192             pasteAsFragment(fragment.releaseNonNull(), canSmartReplaceWithPasteboard(pasteboard), false, MailBlockquoteHandling::IgnoreBlockquote);
193     }
194
195     client()->setInsertionPasteboard(String());
196 }
197
198 String Editor::selectionInHTMLFormat()
199 {
200     return createMarkup(*selectedRange(), nullptr, AnnotateForInterchange, false, ResolveNonLocalURLs);
201 }
202
203 RefPtr<SharedBuffer> Editor::imageInWebArchiveFormat(Element& imageElement)
204 {
205     RefPtr<LegacyWebArchive> archive = LegacyWebArchive::create(imageElement);
206     if (!archive)
207         return nullptr;
208     return SharedBuffer::create(archive->rawDataRepresentation().get());
209 }
210
211 RefPtr<SharedBuffer> Editor::dataSelectionForPasteboard(const String& pasteboardType)
212 {
213     // FIXME: The interface to this function is awkward. We'd probably be better off with three separate functions.
214     // As of this writing, this is only used in WebKit2 to implement the method -[WKView writeSelectionToPasteboard:types:],
215     // which is only used to support OS X services.
216
217     // FIXME: Does this function really need to use adjustedSelectionRange()? Because writeSelectionToPasteboard() just uses selectedRange().
218     if (!canCopy())
219         return nullptr;
220
221     if (pasteboardType == WebArchivePboardType)
222         return selectionInWebArchiveFormat();
223
224     if (pasteboardType == String(NSRTFDPboardType))
225        return dataInRTFDFormat(attributedStringFromRange(*adjustedSelectionRange()));
226
227     if (pasteboardType == String(NSRTFPboardType)) {
228         NSAttributedString* attributedString = attributedStringFromRange(*adjustedSelectionRange());
229         // FIXME: Why is this attachment character stripping needed here, but not needed in writeSelectionToPasteboard?
230         if ([attributedString containsAttachments])
231             attributedString = attributedStringByStrippingAttachmentCharacters(attributedString);
232         return dataInRTFFormat(attributedString);
233     }
234
235     return nullptr;
236 }
237
238 static void getImage(Element& imageElement, RefPtr<Image>& image, CachedImage*& cachedImage)
239 {
240     auto* renderer = imageElement.renderer();
241     if (!is<RenderImage>(renderer))
242         return;
243
244     CachedImage* tentativeCachedImage = downcast<RenderImage>(*renderer).cachedImage();
245     if (!tentativeCachedImage || tentativeCachedImage->errorOccurred())
246         return;
247
248     image = tentativeCachedImage->imageForRenderer(renderer);
249     if (!image)
250         return;
251
252     cachedImage = tentativeCachedImage;
253 }
254
255 String Editor::userVisibleString(const URL& url)
256 {
257     return client()->userVisibleString(url);
258 }
259
260 void Editor::selectionWillChange()
261 {
262     if (!hasComposition() || ignoreSelectionChanges() || m_frame.selection().isNone())
263         return;
264
265     cancelComposition();
266     client()->canceledComposition();
267 }
268
269 String Editor::plainTextFromPasteboard(const PasteboardPlainText& text)
270 {
271     String string = text.text;
272
273     // FIXME: It's not clear this is 100% correct since we know -[NSURL URLWithString:] does not handle
274     // all the same cases we handle well in the URL code for creating an NSURL.
275     if (text.isURL)
276         string = client()->userVisibleString([NSURL URLWithString:string]);
277
278     // FIXME: WTF should offer a non-Mac-specific way to convert string to precomposed form so we can do it for all platforms.
279     return [(NSString *)string precomposedStringWithCanonicalMapping];
280 }
281
282 void Editor::writeImageToPasteboard(Pasteboard& pasteboard, Element& imageElement, const URL& url, const String& title)
283 {
284     PasteboardImage pasteboardImage;
285
286     CachedImage* cachedImage;
287     getImage(imageElement, pasteboardImage.image, cachedImage);
288     if (!pasteboardImage.image)
289         return;
290     ASSERT(cachedImage);
291
292     pasteboardImage.dataInWebArchiveFormat = imageInWebArchiveFormat(imageElement);
293     pasteboardImage.url.url = url;
294     pasteboardImage.url.title = title;
295     pasteboardImage.url.userVisibleForm = client()->userVisibleString(pasteboardImage.url.url);
296     pasteboardImage.resourceData = cachedImage->resourceBuffer();
297     pasteboardImage.resourceMIMEType = cachedImage->response().mimeType();
298
299     pasteboard.write(pasteboardImage);
300 }
301
302 class Editor::WebContentReader final : public PasteboardWebContentReader {
303 public:
304     Frame& frame;
305     Range& context;
306     const bool allowPlainText;
307
308     RefPtr<DocumentFragment> fragment;
309     bool madeFragmentFromPlainText;
310
311     WebContentReader(Frame& frame, Range& context, bool allowPlainText)
312         : frame(frame)
313         , context(context)
314         , allowPlainText(allowPlainText)
315         , madeFragmentFromPlainText(false)
316     {
317     }
318
319 private:
320     bool readWebArchive(SharedBuffer*) override;
321     bool readFilenames(const Vector<String>&) override;
322     bool readHTML(const String&) override;
323     bool readRTFD(SharedBuffer&) override;
324     bool readRTF(SharedBuffer&) override;
325     bool readImage(Ref<SharedBuffer>&&, const String& type) override;
326     bool readURL(const URL&, const String& title) override;
327     bool readPlainText(const String&) override;
328 };
329
330 bool Editor::WebContentReader::readWebArchive(SharedBuffer* buffer)
331 {
332     if (frame.settings().preferMIMETypeForImages())
333         return false;
334
335     if (!frame.document())
336         return false;
337
338     if (!buffer)
339         return false;
340
341     auto archive = LegacyWebArchive::create(URL(), *buffer);
342     if (!archive)
343         return false;
344
345     RefPtr<ArchiveResource> mainResource = archive->mainResource();
346     if (!mainResource)
347         return false;
348
349     const String& type = mainResource->mimeType();
350
351     if (frame.loader().client().canShowMIMETypeAsHTML(type)) {
352         // FIXME: The code in createFragmentAndAddResources calls setDefersLoading(true). Don't we need that here?
353         if (DocumentLoader* loader = frame.loader().documentLoader())
354             loader->addAllArchiveResources(*archive);
355
356         String markupString = String::fromUTF8(mainResource->data().data(), mainResource->data().size());
357         fragment = createFragmentFromMarkup(*frame.document(), markupString, mainResource->url(), DisallowScriptingAndPluginContent);
358         return true;
359     }
360
361     if (MIMETypeRegistry::isSupportedImageMIMEType(type)) {
362         fragment = frame.editor().createFragmentForImageResourceAndAddResource(WTFMove(mainResource));
363         return true;
364     }
365
366     return false;
367 }
368
369 bool Editor::WebContentReader::readFilenames(const Vector<String>& paths)
370 {
371     if (paths.isEmpty())
372         return false;
373
374     if (!frame.document())
375         return false;
376     Document& document = *frame.document();
377
378     fragment = document.createDocumentFragment();
379
380     for (auto& text : paths) {
381 #if ENABLE(ATTACHMENT_ELEMENT)
382         if (RuntimeEnabledFeatures::sharedFeatures().attachmentElementEnabled()) {
383             auto attachment = HTMLAttachmentElement::create(attachmentTag, document);
384             attachment->setFile(File::create([[NSURL fileURLWithPath:text] path]).ptr());
385             fragment->appendChild(attachment);
386             continue;
387         }
388 #endif
389         auto paragraph = createDefaultParagraphElement(document);
390         paragraph->appendChild(document.createTextNode(frame.editor().client()->userVisibleString([NSURL fileURLWithPath:text])));
391         fragment->appendChild(paragraph);
392     }
393
394     return true;
395 }
396
397 bool Editor::WebContentReader::readHTML(const String& string)
398 {
399     String stringOmittingMicrosoftPrefix = string;
400
401     // This code was added to make HTML paste from Microsoft Word on Mac work, back in 2004.
402     // It's a simple-minded way to ignore the CF_HTML clipboard format, just skipping over the
403     // description part and parsing the entire context plus fragment.
404     if (string.startsWith("Version:")) {
405         size_t location = string.findIgnoringCase("<html");
406         if (location != notFound)
407             stringOmittingMicrosoftPrefix = string.substring(location);
408     }
409
410     if (stringOmittingMicrosoftPrefix.isEmpty())
411         return false;
412
413     if (!frame.document())
414         return false;
415     Document& document = *frame.document();
416
417     fragment = createFragmentFromMarkup(document, stringOmittingMicrosoftPrefix, emptyString(), DisallowScriptingAndPluginContent);
418     return fragment;
419 }
420
421 bool Editor::WebContentReader::readRTFD(SharedBuffer& buffer)
422 {
423     if (frame.settings().preferMIMETypeForImages())
424         return false;
425
426     fragment = frame.editor().createFragmentAndAddResources(adoptNS([[NSAttributedString alloc] initWithRTFD:buffer.createNSData().get() documentAttributes:nullptr]).get());
427     return fragment;
428 }
429
430 bool Editor::WebContentReader::readRTF(SharedBuffer& buffer)
431 {
432     if (frame.settings().preferMIMETypeForImages())
433         return false;
434
435     fragment = frame.editor().createFragmentAndAddResources(adoptNS([[NSAttributedString alloc] initWithRTF:buffer.createNSData().get() documentAttributes:nullptr]).get());
436     return fragment;
437 }
438
439 bool Editor::WebContentReader::readImage(Ref<SharedBuffer>&& buffer, const String& type)
440 {
441     ASSERT(type.contains('/'));
442     String typeAsFilenameWithExtension = type;
443     typeAsFilenameWithExtension.replace('/', '.');
444
445     Vector<uint8_t> data;
446     data.append(buffer->data(), buffer->size());
447     auto blob = Blob::create(WTFMove(data), type);
448     ASSERT(frame.document());
449     String blobURL = DOMURL::createObjectURL(*frame.document(), blob);
450
451     fragment = frame.editor().createFragmentForImageAndURL(blobURL);
452     return fragment;
453 }
454
455 bool Editor::WebContentReader::readURL(const URL& url, const String& title)
456 {
457     if (url.string().isEmpty())
458         return false;
459
460     auto anchor = HTMLAnchorElement::create(*frame.document());
461     anchor->setAttributeWithoutSynchronization(HTMLNames::hrefAttr, url.string());
462     anchor->appendChild(frame.document()->createTextNode([title precomposedStringWithCanonicalMapping]));
463
464     fragment = frame.document()->createDocumentFragment();
465     fragment->appendChild(anchor);
466     return true;
467 }
468
469 bool Editor::WebContentReader::readPlainText(const String& text)
470 {
471     if (!allowPlainText)
472         return false;
473
474     fragment = createFragmentFromText(context, [text precomposedStringWithCanonicalMapping]);
475     if (!fragment)
476         return false;
477
478     madeFragmentFromPlainText = true;
479     return true;
480 }
481
482 // FIXME: Should give this function a name that makes it clear it adds resources to the document loader as a side effect.
483 // Or refactor so it does not do that.
484 RefPtr<DocumentFragment> Editor::webContentFromPasteboard(Pasteboard& pasteboard, Range& context, bool allowPlainText, bool& chosePlainText)
485 {
486     WebContentReader reader(m_frame, context, allowPlainText);
487     pasteboard.read(reader);
488     chosePlainText = reader.madeFragmentFromPlainText;
489     return WTFMove(reader.fragment);
490 }
491
492 void Editor::applyFontStyles(const String& fontFamily, double fontSize, unsigned fontTraits)
493 {
494     auto& cssValuePool = CSSValuePool::singleton();
495     Ref<MutableStyleProperties> style = MutableStyleProperties::create();
496     style->setProperty(CSSPropertyFontFamily, cssValuePool.createFontFamilyValue(fontFamily));
497     style->setProperty(CSSPropertyFontStyle, (fontTraits & NSFontItalicTrait) ? CSSValueItalic : CSSValueNormal);
498     style->setProperty(CSSPropertyFontWeight, (fontTraits & NSFontBoldTrait) ? CSSValueBold : CSSValueNormal);
499     style->setProperty(CSSPropertyFontSize, cssValuePool.createValue(fontSize, CSSPrimitiveValue::CSS_PX));
500     applyStyleToSelection(style.ptr(), EditActionSetFont);
501 }
502
503 } // namespace WebCore