blob: 523351cf144ddd81e6dc04af5f33bb8e7ac57268 [file] [log] [blame]
/*
* Copyright (C) 2006-2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "Editor.h"
#import "ArchiveResource.h"
#import "CSSValueList.h"
#import "CSSValuePool.h"
#import "CachedResourceLoader.h"
#import "ColorMac.h"
#import "DocumentFragment.h"
#import "DocumentLoader.h"
#import "Editing.h"
#import "EditingStyle.h"
#import "EditorClient.h"
#import "ElementInlines.h"
#import "FontAttributes.h"
#import "FontCascade.h"
#import "Frame.h"
#import "FrameLoader.h"
#import "FrameSelection.h"
#import "HTMLAttachmentElement.h"
#import "HTMLConverter.h"
#import "HTMLImageElement.h"
#import "HTMLSpanElement.h"
#import "ImageOverlay.h"
#import "LegacyNSPasteboardTypes.h"
#import "LegacyWebArchive.h"
#import "Page.h"
#import "PagePasteboardContext.h"
#import "Pasteboard.h"
#import "PasteboardStrategy.h"
#import "PlatformStrategies.h"
#import "RenderElement.h"
#import "RenderStyle.h"
#import "Settings.h"
#import "SystemSoundManager.h"
#import "Text.h"
#import "UTIUtilities.h"
#import "WebContentReader.h"
#import "markup.h"
#import <pal/spi/cocoa/NSAttributedStringSPI.h>
#import <wtf/BlockObjCExceptions.h>
#import <wtf/cocoa/NSURLExtras.h>
namespace WebCore {
static RefPtr<SharedBuffer> archivedDataForAttributedString(NSAttributedString *attributedString)
{
if (!attributedString.length)
return nullptr;
return SharedBuffer::create([NSKeyedArchiver archivedDataWithRootObject:attributedString requiringSecureCoding:YES error:nullptr]);
}
String Editor::selectionInHTMLFormat()
{
if (ImageOverlay::isInsideOverlay(m_document.selection().selection()))
return { };
return serializePreservingVisualAppearance(m_document.selection().selection(), ResolveURLs::YesExcludingLocalFileURLsForPrivacy, SerializeComposedTree::Yes);
}
#if ENABLE(ATTACHMENT_ELEMENT)
void Editor::getPasteboardTypesAndDataForAttachment(Element& element, Vector<String>& outTypes, Vector<RefPtr<SharedBuffer>>& outData)
{
auto elementRange = makeRangeSelectingNode(element);
client()->getClientPasteboardData(elementRange, outTypes, outData);
outTypes.append(PasteboardCustomData::cocoaType());
outData.append(PasteboardCustomData { element.document().originIdentifierForPasteboard(), { } }.createSharedBuffer());
if (elementRange) {
if (auto archive = LegacyWebArchive::create(*elementRange)) {
if (auto data = archive->rawDataRepresentation()) {
outTypes.append(WebArchivePboardType);
outData.append(SharedBuffer::create(data.get()));
}
}
}
}
#endif
static RetainPtr<NSAttributedString> selectionInImageOverlayAsAttributedString(const VisibleSelection& selection)
{
#if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS)
auto* page = selection.document()->page();
if (!page)
return nil;
RefPtr hostElement = dynamicDowncast<HTMLElement>(selection.start().containerNode()->shadowHost());
if (!hostElement) {
ASSERT_NOT_REACHED();
return nil;
}
auto cachedResult = page->cachedTextRecognitionResult(*hostElement);
if (!cachedResult)
return nil;
auto characterRange = valueOrDefault(ImageOverlay::characterRange(selection));
if (!characterRange.length)
return nil;
auto string = stringForRange(*cachedResult, characterRange);
__block bool hasAnyAttributes = false;
[string enumerateAttributesInRange:NSMakeRange(0, [string length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange, BOOL *stop) {
if (attributes.count) {
hasAnyAttributes = true;
*stop = YES;
}
}];
if (!hasAnyAttributes)
return nil;
return string;
#else
UNUSED_PARAM(selection);
return nil;
#endif
}
static RetainPtr<NSAttributedString> selectionAsAttributedString(const Document& document)
{
auto selection = document.selection().selection();
if (ImageOverlay::isInsideOverlay(selection))
return selectionInImageOverlayAsAttributedString(selection);
auto range = selection.firstRange();
return range ? attributedString(*range).string : adoptNS([[NSAttributedString alloc] init]);
}
void Editor::writeSelectionToPasteboard(Pasteboard& pasteboard)
{
auto string = selectionAsAttributedString(m_document);
PasteboardWebContent content;
content.contentOrigin = m_document.originIdentifierForPasteboard();
content.canSmartCopyOrDelete = canSmartCopyOrDelete();
if (!pasteboard.isStatic()) {
content.dataInWebArchiveFormat = selectionInWebArchiveFormat();
content.dataInRTFDFormat = [string containsAttachments] ? dataInRTFDFormat(string.get()) : nullptr;
content.dataInRTFFormat = dataInRTFFormat(string.get());
content.dataInAttributedStringFormat = archivedDataForAttributedString(string.get());
client()->getClientPasteboardData(selectedRange(), content.clientTypes, content.clientData);
}
content.dataInHTMLFormat = selectionInHTMLFormat();
content.dataInStringFormat = stringSelectionForPasteboardWithImageAltText();
pasteboard.write(content);
}
void Editor::writeSelection(PasteboardWriterData& pasteboardWriterData)
{
auto string = selectionAsAttributedString(m_document);
PasteboardWriterData::WebContent webContent;
webContent.contentOrigin = m_document.originIdentifierForPasteboard();
webContent.canSmartCopyOrDelete = canSmartCopyOrDelete();
webContent.dataInWebArchiveFormat = selectionInWebArchiveFormat();
webContent.dataInRTFDFormat = [string containsAttachments] ? dataInRTFDFormat(string.get()) : nullptr;
webContent.dataInRTFFormat = dataInRTFFormat(string.get());
webContent.dataInAttributedStringFormat = archivedDataForAttributedString(string.get());
webContent.dataInHTMLFormat = selectionInHTMLFormat();
webContent.dataInStringFormat = stringSelectionForPasteboardWithImageAltText();
client()->getClientPasteboardData(selectedRange(), webContent.clientTypes, webContent.clientData);
pasteboardWriterData.setWebContent(WTFMove(webContent));
}
RefPtr<SharedBuffer> Editor::selectionInWebArchiveFormat()
{
if (ImageOverlay::isInsideOverlay(m_document.selection().selection()))
return nullptr;
auto archive = LegacyWebArchive::createFromSelection(m_document.frame());
if (!archive)
return nullptr;
return SharedBuffer::create(archive->rawDataRepresentation().get());
}
// FIXME: Makes no sense that selectedTextForDataTransfer always includes alt text, but stringSelectionForPasteboard does not.
// This was left in a bad state when selectedTextForDataTransfer was added. Need to look over clients and fix this.
String Editor::stringSelectionForPasteboard()
{
if (!canCopy())
return emptyString();
return makeStringByReplacingAll(selectedText(), noBreakSpace, ' ');
}
String Editor::stringSelectionForPasteboardWithImageAltText()
{
if (!canCopy())
return emptyString();
return makeStringByReplacingAll(selectedTextForDataTransfer(), noBreakSpace, ' ');
}
void Editor::replaceSelectionWithAttributedString(NSAttributedString *attributedString, MailBlockquoteHandling mailBlockquoteHandling)
{
if (m_document.selection().isNone())
return;
if (m_document.selection().selection().isContentRichlyEditable()) {
if (auto fragment = createFragmentAndAddResources(*m_document.frame(), attributedString)) {
if (shouldInsertFragment(*fragment, selectedRange(), EditorInsertAction::Pasted))
pasteAsFragment(fragment.releaseNonNull(), false, false, mailBlockquoteHandling);
}
} else {
String text = attributedString.string;
if (shouldInsertText(text, selectedRange(), EditorInsertAction::Pasted))
pasteAsPlainText(text, false);
}
}
String Editor::userVisibleString(const URL& url)
{
return WTF::userVisibleString(url);
}
RefPtr<SharedBuffer> Editor::dataInRTFDFormat(NSAttributedString *string)
{
NSUInteger length = string.length;
if (!length)
return nullptr;
BEGIN_BLOCK_OBJC_EXCEPTIONS
return SharedBuffer::create([string RTFDFromRange:NSMakeRange(0, length) documentAttributes:@{ }]);
END_BLOCK_OBJC_EXCEPTIONS
return nullptr;
}
RefPtr<SharedBuffer> Editor::dataInRTFFormat(NSAttributedString *string)
{
NSUInteger length = string.length;
if (!length)
return nullptr;
BEGIN_BLOCK_OBJC_EXCEPTIONS
return SharedBuffer::create([string RTFFromRange:NSMakeRange(0, length) documentAttributes:@{ }]);
END_BLOCK_OBJC_EXCEPTIONS
return nullptr;
}
// FIXME: Should give this function a name that makes it clear it adds resources to the document loader as a side effect.
// Or refactor so it does not do that.
RefPtr<DocumentFragment> Editor::webContentFromPasteboard(Pasteboard& pasteboard, const SimpleRange& context, bool allowPlainText, bool& chosePlainText)
{
WebContentReader reader(*m_document.frame(), context, allowPlainText);
pasteboard.read(reader);
chosePlainText = reader.madeFragmentFromPlainText;
return WTFMove(reader.fragment);
}
void Editor::takeFindStringFromSelection()
{
if (!canCopyExcludingStandaloneImages()) {
SystemSoundManager::singleton().systemBeep();
return;
}
auto stringFromSelection = m_document.frame()->displayStringModifiedByEncoding(selectedTextForDataTransfer());
#if PLATFORM(MAC)
Vector<String> types;
types.append(String(legacyStringPasteboardType()));
auto context = PagePasteboardContext::create(m_document.pageID());
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
platformStrategies()->pasteboardStrategy()->setTypes(types, NSFindPboard, context.get());
platformStrategies()->pasteboardStrategy()->setStringForType(WTFMove(stringFromSelection), legacyStringPasteboardType(), NSFindPboard, context.get());
ALLOW_DEPRECATED_DECLARATIONS_END
#else
if (auto* client = this->client()) {
// Since the find pasteboard doesn't exist on iOS, WebKit maintains its own notion of the latest find string,
// which SPI clients may respect when presenting find-in-page UI.
client->updateStringForFind(stringFromSelection);
}
#endif
}
String Editor::platformContentTypeForBlobType(const String& type) const
{
auto utiType = UTIFromMIMEType(type);
if (!utiType.isEmpty())
return utiType;
return type;
}
void Editor::readSelectionFromPasteboard(const String& pasteboardName)
{
Pasteboard pasteboard(PagePasteboardContext::create(m_document.pageID()), pasteboardName);
if (m_document.selection().selection().isContentRichlyEditable())
pasteWithPasteboard(&pasteboard, { PasteOption::AllowPlainText });
else
pasteAsPlainTextWithPasteboard(pasteboard);
}
static void maybeCopyNodeAttributesToFragment(const Node& node, DocumentFragment& fragment)
{
// This is only supported for single-Node fragments.
RefPtr firstChild = fragment.firstChild();
if (!firstChild || firstChild != fragment.lastChild())
return;
// And only supported for HTML elements.
if (!node.isHTMLElement() || !firstChild->isHTMLElement())
return;
// And only if the source Element and destination Element have the same HTML tag name.
Ref oldElement = downcast<HTMLElement>(node);
Ref newElement = downcast<HTMLElement>(*firstChild);
if (oldElement->localName() != newElement->localName())
return;
for (auto& attribute : oldElement->attributesIterator()) {
if (newElement->hasAttribute(attribute.name()))
continue;
newElement->setAttribute(attribute.name(), attribute.value());
}
}
void Editor::replaceNodeFromPasteboard(Node& node, const String& pasteboardName, EditAction action)
{
if (node.document() != m_document)
return;
auto range = makeRangeSelectingNode(node);
if (!range)
return;
Ref protectedDocument = m_document;
m_document.selection().setSelection({ *range }, FrameSelection::DoNotSetFocus);
Pasteboard pasteboard(PagePasteboardContext::create(m_document.pageID()), pasteboardName);
if (!m_document.selection().selection().isContentRichlyEditable()) {
pasteAsPlainTextWithPasteboard(pasteboard);
return;
}
#if PLATFORM(MAC)
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
// FIXME: How can this hard-coded pasteboard name be right, given that the passed-in pasteboard has a name?
// FIXME: We can also remove `setInsertionPasteboard` altogether once Mail compose on macOS no longer uses WebKitLegacy,
// since it's only implemented for WebKitLegacy on macOS, and the only known client is Mail compose.
client()->setInsertionPasteboard(NSGeneralPboard);
ALLOW_DEPRECATED_DECLARATIONS_END
#endif
bool chosePlainText;
if (auto fragment = webContentFromPasteboard(pasteboard, *range, true, chosePlainText)) {
maybeCopyNodeAttributesToFragment(node, *fragment);
if (shouldInsertFragment(*fragment, *range, EditorInsertAction::Pasted))
pasteAsFragment(fragment.releaseNonNull(), false, false, MailBlockquoteHandling::IgnoreBlockquote, action);
}
#if PLATFORM(MAC)
client()->setInsertionPasteboard({ });
#endif
}
}