| /* |
| * Copyright (C) 2001 Peter Kelly (pmk@post.com) |
| * Copyright (C) 2003-2019 Apple Inc. All Rights Reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| */ |
| |
| #include "config.h" |
| #include "JSLazyEventListener.h" |
| |
| #include "CachedScriptFetcher.h" |
| #include "ContentSecurityPolicy.h" |
| #include "Element.h" |
| #include "Frame.h" |
| #include "JSNode.h" |
| #include "QualifiedName.h" |
| #include "SVGElement.h" |
| #include "ScriptController.h" |
| #include <JavaScriptCore/CatchScope.h> |
| #include <JavaScriptCore/FunctionConstructor.h> |
| #include <JavaScriptCore/IdentifierInlines.h> |
| #include <wtf/NeverDestroyed.h> |
| #include <wtf/RefCountedLeakCounter.h> |
| #include <wtf/StdLibExtras.h> |
| #include <wtf/WeakPtr.h> |
| |
| namespace WebCore { |
| using namespace JSC; |
| |
| DEFINE_DEBUG_ONLY_GLOBAL(WTF::RefCountedLeakCounter, eventListenerCounter, ("JSLazyEventListener")); |
| |
| struct JSLazyEventListener::CreationArguments { |
| const QualifiedName& attributeName; |
| const AtomString& attributeValue; |
| Document& document; |
| WeakPtr<ContainerNode> node; |
| JSObject* wrapper; |
| bool shouldUseSVGEventName; |
| }; |
| |
| static const String& eventParameterName(bool shouldUseSVGEventName) |
| { |
| static NeverDestroyed<const String> eventString(MAKE_STATIC_STRING_IMPL("event")); |
| static NeverDestroyed<const String> evtString(MAKE_STATIC_STRING_IMPL("evt")); |
| return shouldUseSVGEventName ? evtString : eventString; |
| } |
| |
| static TextPosition convertZeroToOne(const TextPosition& position) |
| { |
| // A JSLazyEventListener can be created with a line number of zero when it is created with |
| // a setAttribute call from JavaScript, so make the line number 1 in that case. |
| if (position == TextPosition::belowRangePosition()) |
| return { }; |
| return position; |
| } |
| |
| JSLazyEventListener::JSLazyEventListener(CreationArguments&& arguments, const String& sourceURL, const TextPosition& sourcePosition) |
| : JSEventListener(nullptr, arguments.wrapper, true, mainThreadNormalWorld()) |
| , m_functionName(arguments.attributeName.localName().string()) |
| , m_eventParameterName(eventParameterName(arguments.shouldUseSVGEventName)) |
| , m_code(arguments.attributeValue) |
| , m_sourceURL(sourceURL) |
| , m_sourcePosition(convertZeroToOne(sourcePosition)) |
| , m_originalNode(WTFMove(arguments.node)) |
| { |
| #ifndef NDEBUG |
| eventListenerCounter.increment(); |
| #endif |
| } |
| |
| #if !ASSERT_DISABLED |
| static inline bool isCloneInShadowTreeOfSVGUseElement(Node& originalNode, EventTarget& eventTarget) |
| { |
| if (!eventTarget.isNode()) |
| return false; |
| |
| auto& node = downcast<Node>(eventTarget); |
| if (!is<SVGElement>(node)) |
| return false; |
| |
| auto& element = downcast<SVGElement>(node); |
| if (!element.correspondingElement()) |
| return false; |
| |
| ASSERT(element.isInShadowTree()); |
| return &originalNode == element.correspondingElement(); |
| } |
| |
| // This is to help find the underlying cause of <rdar://problem/24314027>. |
| void JSLazyEventListener::checkValidityForEventTarget(EventTarget& eventTarget) |
| { |
| if (eventTarget.isNode()) { |
| ASSERT(m_originalNode); |
| ASSERT(static_cast<EventTarget*>(m_originalNode.get()) == &eventTarget || isCloneInShadowTreeOfSVGUseElement(*m_originalNode, eventTarget)); |
| } else |
| ASSERT(!m_originalNode); |
| } |
| #endif |
| |
| JSLazyEventListener::~JSLazyEventListener() |
| { |
| #ifndef NDEBUG |
| eventListenerCounter.decrement(); |
| #endif |
| } |
| |
| JSObject* JSLazyEventListener::initializeJSFunction(ScriptExecutionContext& executionContext) const |
| { |
| ASSERT(is<Document>(executionContext)); |
| |
| auto& executionContextDocument = downcast<Document>(executionContext); |
| |
| // As per the HTML specification [1], if this is an element's event handler, then document should be the |
| // element's document. The script execution context may be different from the node's document if the |
| // node's document was created by JavaScript. |
| // [1] https://html.spec.whatwg.org/multipage/webappapis.html#getting-the-current-value-of-the-event-handler |
| auto& document = m_originalNode ? m_originalNode->document() : executionContextDocument; |
| if (!document.frame()) |
| return nullptr; |
| |
| if (!document.contentSecurityPolicy()->allowInlineEventHandlers(m_sourceURL, m_sourcePosition.m_line)) |
| return nullptr; |
| |
| auto& script = document.frame()->script(); |
| if (!script.canExecuteScripts(AboutToCreateEventListener) || script.isPaused()) |
| return nullptr; |
| |
| if (!executionContextDocument.frame()) |
| return nullptr; |
| auto* globalObject = toJSDOMWindow(*executionContextDocument.frame(), isolatedWorld()); |
| if (!globalObject) |
| return nullptr; |
| |
| VM& vm = globalObject->vm(); |
| JSLockHolder lock(vm); |
| auto scope = DECLARE_CATCH_SCOPE(vm); |
| JSGlobalObject* lexicalGlobalObject = globalObject; |
| |
| MarkedArgumentBuffer args; |
| args.append(jsNontrivialString(vm, m_eventParameterName)); |
| args.append(jsStringWithCache(lexicalGlobalObject, m_code)); |
| ASSERT(!args.hasOverflowed()); |
| |
| // We want all errors to refer back to the line on which our attribute was |
| // declared, regardless of any newlines in our JavaScript source text. |
| int overrideLineNumber = m_sourcePosition.m_line.oneBasedInt(); |
| |
| JSObject* jsFunction = constructFunctionSkippingEvalEnabledCheck( |
| lexicalGlobalObject, args, Identifier::fromString(vm, m_functionName), |
| SourceOrigin { m_sourceURL, CachedScriptFetcher::create(document.charset()) }, |
| m_sourceURL, m_sourcePosition, overrideLineNumber); |
| if (UNLIKELY(scope.exception())) { |
| reportCurrentException(lexicalGlobalObject); |
| scope.clearException(); |
| return nullptr; |
| } |
| |
| JSFunction* listenerAsFunction = jsCast<JSFunction*>(jsFunction); |
| |
| if (m_originalNode) { |
| if (!wrapper()) { |
| // Ensure that 'node' has a JavaScript wrapper to mark the event listener we're creating. |
| // FIXME: Should pass the global object associated with the node |
| setWrapper(vm, asObject(toJS(lexicalGlobalObject, globalObject, *m_originalNode))); |
| } |
| |
| // Add the event's home element to the scope |
| // (and the document, and the form - see JSHTMLElement::eventHandlerScope) |
| listenerAsFunction->setScope(vm, jsCast<JSNode*>(wrapper())->pushEventHandlerScope(lexicalGlobalObject, listenerAsFunction->scope())); |
| } |
| |
| return jsFunction; |
| } |
| |
| RefPtr<JSLazyEventListener> JSLazyEventListener::create(CreationArguments&& arguments) |
| { |
| if (arguments.attributeValue.isNull()) |
| return nullptr; |
| |
| // FIXME: We should be able to provide source information for frameless documents too (e.g. for importing nodes from XMLHttpRequest.responseXML). |
| TextPosition position; |
| String sourceURL; |
| if (Frame* frame = arguments.document.frame()) { |
| if (!frame->script().canExecuteScripts(AboutToCreateEventListener)) |
| return nullptr; |
| position = frame->script().eventHandlerPosition(); |
| sourceURL = arguments.document.url().string(); |
| } |
| |
| return adoptRef(*new JSLazyEventListener(WTFMove(arguments), sourceURL, position)); |
| } |
| |
| RefPtr<JSLazyEventListener> JSLazyEventListener::create(Element& element, const QualifiedName& attributeName, const AtomString& attributeValue) |
| { |
| return create({ attributeName, attributeValue, element.document(), makeWeakPtr(element), nullptr, element.isSVGElement() }); |
| } |
| |
| RefPtr<JSLazyEventListener> JSLazyEventListener::create(Document& document, const QualifiedName& attributeName, const AtomString& attributeValue) |
| { |
| // FIXME: This always passes false for "shouldUseSVGEventName". Is that correct for events dispatched to SVG documents? |
| // This has been this way for a long time, but became more obvious when refactoring to separate the Element and Document code paths. |
| return create({ attributeName, attributeValue, document, makeWeakPtr(document), nullptr, false }); |
| } |
| |
| RefPtr<JSLazyEventListener> JSLazyEventListener::create(DOMWindow& window, const QualifiedName& attributeName, const AtomString& attributeValue) |
| { |
| ASSERT(window.document()); |
| auto& document = *window.document(); |
| ASSERT(document.frame()); |
| return create({ attributeName, attributeValue, document, nullptr, toJSDOMWindow(document.frame(), mainThreadNormalWorld()), document.isSVGDocument() }); |
| } |
| |
| } // namespace WebCore |