/*
 *  Copyright (C) 2001 Peter Kelly (pmk@post.com)
 *  Copyright (C) 2003-2018 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 "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
// 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);
    } 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);
    ExecState* exec = globalObject->globalExec();

    MarkedArgumentBuffer args;
    args.append(jsNontrivialString(exec, m_eventParameterName));
    args.append(jsStringWithCache(exec, 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(exec,
        exec->lexicalGlobalObject(), args, Identifier::fromString(exec, m_functionName),
        SourceOrigin { m_sourceURL, CachedScriptFetcher::create(document.charset()) },
        m_sourceURL, m_sourcePosition, overrideLineNumber);
    if (UNLIKELY(scope.exception())) {
        reportCurrentException(exec);
        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(exec, 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(exec, 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
