/*
 * Copyright (C) 2004-2019 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 "WebScriptObjectPrivate.h"

#import "BridgeJSC.h"
#import "Frame.h"
#import "JSDOMBindingSecurity.h"
#import "JSDOMWindow.h"
#import "JSDOMWindowCustom.h"
#import "JSExecState.h"
#import "JSHTMLElement.h"
#import "JSPluginElementFunctions.h"
#import "ObjCRuntimeObject.h"
#import "WebCoreObjCExtras.h"
#import "objc_instance.h"
#import "runtime_object.h"
#import "runtime_root.h"
#import <JavaScriptCore/APICast.h>
#import <JavaScriptCore/CallFrame.h>
#import <JavaScriptCore/CatchScope.h>
#import <JavaScriptCore/Completion.h>
#import <JavaScriptCore/InitializeThreading.h>
#import <JavaScriptCore/JSContextInternal.h>
#import <JavaScriptCore/JSGlobalObject.h>
#import <JavaScriptCore/JSLock.h>
#import <JavaScriptCore/JSValueInternal.h>
#import <wtf/HashMap.h>
#import <wtf/Lock.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/Threading.h>
#import <wtf/text/WTFString.h>

using namespace JSC::Bindings;
using namespace WebCore;

using JSC::CallData;
using JSC::CallType;
using JSC::Identifier;
using JSC::JSLockHolder;
using JSC::JSObject;
using JSC::MarkedArgumentBuffer;
using JSC::PutPropertySlot;
using JSC::jsCast;
using JSC::jsUndefined;
using JSC::makeSource;

namespace WebCore {

static Lock spinLock;
static CreateWrapperFunction createDOMWrapperFunction;
static DisconnectWindowWrapperFunction disconnectWindowWrapperFunction;

static HashMap<JSObject*, NSObject *>& wrapperCache()
{
    static NeverDestroyed<HashMap<JSObject*, NSObject *>> map;
    return map;
}

NSObject *getJSWrapper(JSObject* impl)
{
    ASSERT(isMainThread());
    LockHolder holder(&spinLock);

    NSObject* wrapper = wrapperCache().get(impl);
    return wrapper ? [[wrapper retain] autorelease] : nil;
}

void addJSWrapper(NSObject *wrapper, JSObject* impl)
{
    ASSERT(isMainThread());
    LockHolder holder(&spinLock);

    wrapperCache().set(impl, wrapper);
}

void removeJSWrapper(JSObject* impl)
{
    LockHolder holder(&spinLock);

    wrapperCache().remove(impl);
}

static void removeJSWrapperIfRetainCountOne(NSObject* wrapper, JSObject* impl)
{
    LockHolder holder(&spinLock);

    if ([wrapper retainCount] == 1)
        wrapperCache().remove(impl);
}

id createJSWrapper(JSC::JSObject* object, RefPtr<JSC::Bindings::RootObject>&& origin, RefPtr<JSC::Bindings::RootObject>&& root)
{
    if (id wrapper = getJSWrapper(object))
        return wrapper;
    return [[[WebScriptObject alloc] _initWithJSObject:object originRootObject:WTFMove(origin) rootObject:WTFMove(root)] autorelease];
}

static void addExceptionToConsole(JSC::JSGlobalObject* lexicalGlobalObject, JSC::Exception* exception)
{
    JSC::VM& vm = lexicalGlobalObject->vm();
    JSDOMWindow* window = asJSDOMWindow(vm.deprecatedVMEntryGlobalObject(lexicalGlobalObject));
    if (!window || !exception)
        return;
    reportException(lexicalGlobalObject, exception);
}

static void addExceptionToConsole(JSC::JSGlobalObject* lexicalGlobalObject)
{
    JSC::VM& vm = lexicalGlobalObject->vm();
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::Exception* exception = scope.exception();
    scope.clearException();
    addExceptionToConsole(lexicalGlobalObject, exception);
}

void initializeDOMWrapperHooks(CreateWrapperFunction createFunction, DisconnectWindowWrapperFunction disconnectFunction)
{
    ASSERT(createFunction);
    ASSERT(disconnectFunction);
    ASSERT(!createDOMWrapperFunction);
    ASSERT(!disconnectWindowWrapperFunction);
    createDOMWrapperFunction = createFunction;
    disconnectWindowWrapperFunction = disconnectFunction;
}

void disconnectWindowWrapper(WebScriptObject *windowWrapper)
{
    ASSERT(windowWrapper);
    ASSERT(disconnectWindowWrapperFunction);
    disconnectWindowWrapperFunction(windowWrapper);
}

} // namespace WebCore

@implementation WebScriptObjectPrivate

@end

@implementation WebScriptObject

+ (void)initialize
{
#if !USE(WEB_THREAD)
    JSC::initializeThreading();
    WTF::initializeMainThread();
#endif
}

+ (id)scriptObjectForJSObject:(JSObjectRef)jsObject originRootObject:(RootObject*)originRootObject rootObject:(RootObject*)rootObject
{
    ASSERT(jsObject);
    auto& wrapped = *toJS(jsObject);

    if (WebCore::createDOMWrapperFunction) {
        if (auto wrapper = WebCore::createDOMWrapperFunction(wrapped)) {
            if (![wrapper _hasImp]) // new wrapper, not from cache
                [wrapper _setImp:&wrapped originRootObject:originRootObject rootObject:rootObject];
            return wrapper;
        }
    }

    return WebCore::createJSWrapper(&wrapped, originRootObject, rootObject);
}

- (void)_setImp:(JSObject*)imp originRootObject:(RefPtr<RootObject>&&)originRootObject rootObject:(RefPtr<RootObject>&&)rootObject
{
    // This function should only be called once, as a (possibly lazy) initializer.
    ASSERT(!_private->imp);
    ASSERT(!_private->rootObject);
    ASSERT(!_private->originRootObject);
    ASSERT(imp);

    _private->imp = imp;
    _private->rootObject = rootObject.leakRef();
    _private->originRootObject = originRootObject.leakRef();

    WebCore::addJSWrapper(self, imp);

    if (_private->rootObject)
        _private->rootObject->gcProtect(imp);
}

- (void)_setOriginRootObject:(RefPtr<RootObject>&&)originRootObject andRootObject:(RefPtr<RootObject>&&)rootObject
{
    ASSERT(_private->imp);

    if (rootObject)
        rootObject->gcProtect(_private->imp);

    if (_private->rootObject && _private->rootObject->isValid())
        _private->rootObject->gcUnprotect(_private->imp);

    if (_private->rootObject)
        _private->rootObject->deref();

    if (_private->originRootObject)
        _private->originRootObject->deref();

    _private->rootObject = rootObject.leakRef();
    _private->originRootObject = originRootObject.leakRef();
}

- (id)_initWithJSObject:(JSC::JSObject*)imp originRootObject:(RefPtr<JSC::Bindings::RootObject>&&)originRootObject rootObject:(RefPtr<JSC::Bindings::RootObject>&&)rootObject
{
    ASSERT(imp);

    self = [super init];
    _private = [[WebScriptObjectPrivate alloc] init];
    [self _setImp:imp originRootObject:WTFMove(originRootObject) rootObject:WTFMove(rootObject)];
    
    return self;
}

- (JSObject*)_imp
{
    // Associate the WebScriptObject with the JS wrapper for the ObjC DOM wrapper.
    // This is done on lazily, on demand.
    if (!_private->imp && _private->isCreatedByDOMWrapper)
        [self _initializeScriptDOMNodeImp];
    return [self _rootObject] ? _private->imp : 0;
}

- (BOOL)_hasImp
{
    return _private->imp != nil;
}

// Node that DOMNode overrides this method. So you should almost always
// use this method call instead of _private->rootObject directly.
- (RootObject*)_rootObject
{
    return _private->rootObject && _private->rootObject->isValid() ? _private->rootObject : 0;
}

- (RootObject *)_originRootObject
{
    return _private->originRootObject && _private->originRootObject->isValid() ? _private->originRootObject : 0;
}

- (BOOL)_isSafeScript
{
    RootObject *root = [self _rootObject];
    if (!root)
        return false;

    if (!_private->originRootObject)
        return true;

    if (!_private->originRootObject->isValid())
        return false;

    // It's not actually correct to call shouldAllowAccessToFrame in this way because
    // JSDOMWindowBase* isn't the right object to represent the currently executing
    // JavaScript. Instead, we should use JSGlobalObject, like we do elsewhere.
    auto* target = JSC::jsDynamicCast<JSDOMWindowBase*>(root->globalObject()->vm(), root->globalObject());
    if (!target)
        return false;
    return BindingSecurity::shouldAllowAccessToDOMWindow(_private->originRootObject->globalObject(), target->wrapped());
}

- (JSGlobalContextRef)_globalContextRef
{
    if (![self _isSafeScript])
        return nil;
    return toGlobalRef([self _rootObject]->globalObject());
}

- (oneway void)release
{
    // If we're releasing the last reference to this object, remove if from the map.
    if (_private->imp)
        WebCore::removeJSWrapperIfRetainCountOne(self, _private->imp);

    [super release];
}

- (void)dealloc
{
    if (WebCoreObjCScheduleDeallocateOnMainThread([WebScriptObject class], self))
        return;

    if (_private->rootObject && _private->rootObject->isValid())
        _private->rootObject->gcUnprotect(_private->imp);

    if (_private->rootObject)
        _private->rootObject->deref();

    if (_private->originRootObject)
        _private->originRootObject->deref();

    [_private release];

    [super dealloc];
}

+ (BOOL)throwException:(NSString *)exceptionMessage
{
    ObjcInstance::setGlobalException(exceptionMessage);
    return YES;
}

static void getListFromNSArray(JSC::JSGlobalObject* lexicalGlobalObject, NSArray *array, RootObject* rootObject, MarkedArgumentBuffer& aList)
{
    int i, numObjects = array ? [array count] : 0;
    
    for (i = 0; i < numObjects; i++) {
        id anObject = [array objectAtIndex:i];
        aList.append(convertObjcValueToValue(lexicalGlobalObject, &anObject, ObjcObjectType, rootObject));
    }
}

- (id)callWebScriptMethod:(NSString *)name withArguments:(NSArray *)args
{
    if (![self _isSafeScript])
        return nil;

    // Look up the function object.
    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::JSGlobalObject* lexicalGlobalObject = globalObject;
    UNUSED_PARAM(scope);

    JSC::JSValue function = [self _imp]->get(lexicalGlobalObject, Identifier::fromString(vm, String(name)));
    CallData callData;
    CallType callType = getCallData(vm, function, callData);
    if (callType == CallType::None)
        return nil;

    MarkedArgumentBuffer argList;
    ASSERT(!argList.hasOverflowed());
    getListFromNSArray(lexicalGlobalObject, args, [self _rootObject], argList);

    if (![self _isSafeScript])
        return nil;

    NakedPtr<JSC::Exception> exception;
    JSC::JSValue result = JSExecState::profiledCall(lexicalGlobalObject, JSC::ProfilingReason::Other, function, callType, callData, [self _imp], argList, exception);

    if (exception) {
        addExceptionToConsole(lexicalGlobalObject, exception);
        result = jsUndefined();
    }

    // Convert and return the result of the function call.
    id resultObj = [WebScriptObject _convertValueToObjcValue:result originRootObject:[self _originRootObject] rootObject:[self _rootObject]];

    return resultObj;
}

- (id)evaluateWebScript:(NSString *)script
{
    if (![self _isSafeScript])
        return nil;
    
    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    UNUSED_PARAM(scope);
    
    JSC::JSValue returnValue = JSExecState::profiledEvaluate(globalObject, JSC::ProfilingReason::Other, makeSource(String(script), { }), JSC::JSValue());

    id resultObj = [WebScriptObject _convertValueToObjcValue:returnValue originRootObject:[self _originRootObject] rootObject:[self _rootObject]];
    
    return resultObj;
}

- (void)setValue:(id)value forKey:(NSString *)key
{
    if (![self _isSafeScript])
        return;

    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::JSGlobalObject* lexicalGlobalObject = globalObject;

    JSObject* object = JSC::jsDynamicCast<JSObject*>(vm, [self _imp]);
    PutPropertySlot slot(object);
    object->methodTable(vm)->put(object, lexicalGlobalObject, Identifier::fromString(vm, String(key)), convertObjcValueToValue(lexicalGlobalObject, &value, ObjcObjectType, [self _rootObject]), slot);

    if (UNLIKELY(scope.exception())) {
        addExceptionToConsole(lexicalGlobalObject);
        scope.clearException();
    }
}

- (id)valueForKey:(NSString *)key
{
    if (![self _isSafeScript])
        return nil;

    id resultObj;
    {
        auto globalObject = [self _rootObject]->globalObject();
        auto& vm = globalObject->vm();

        // Need to scope this lock to ensure that we release the lock before calling
        // [super valueForKey:key] which might throw an exception and bypass the JSLock destructor,
        // leaving the lock permanently held
        JSLockHolder lock(vm);

        auto scope = DECLARE_CATCH_SCOPE(vm);
        JSC::JSGlobalObject* lexicalGlobalObject = globalObject;

        JSC::JSValue result = [self _imp]->get(lexicalGlobalObject, Identifier::fromString(vm, String(key)));
        
        if (UNLIKELY(scope.exception())) {
            addExceptionToConsole(lexicalGlobalObject);
            result = jsUndefined();
            scope.clearException();
        }

        resultObj = [WebScriptObject _convertValueToObjcValue:result originRootObject:[self _originRootObject] rootObject:[self _rootObject]];
    }
    
    if ([resultObj isKindOfClass:[WebUndefined class]])
        resultObj = [super valueForKey:key];    // defaults to throwing an exception

    return resultObj;
}

- (void)removeWebScriptKey:(NSString *)key
{
    if (![self _isSafeScript])
        return;

    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::JSGlobalObject* lexicalGlobalObject = globalObject;

    [self _imp]->methodTable(vm)->deleteProperty([self _imp], lexicalGlobalObject, Identifier::fromString(vm, String(key)));

    if (UNLIKELY(scope.exception())) {
        addExceptionToConsole(lexicalGlobalObject);
        scope.clearException();
    }
}

- (BOOL)hasWebScriptKey:(NSString *)key
{
    if (![self _isSafeScript])
        return NO;

    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::JSGlobalObject* lexicalGlobalObject = globalObject;

    BOOL result = [self _imp]->hasProperty(lexicalGlobalObject, Identifier::fromString(vm, String(key)));

    if (UNLIKELY(scope.exception())) {
        addExceptionToConsole(lexicalGlobalObject);
        scope.clearException();
    }

    return result;
}

- (NSString *)stringRepresentation
{
    if (![self _isSafeScript]) {
        // This is a workaround for a gcc 3.3 internal compiler error.
        return @"Undefined";
    }

    JSC::JSGlobalObject* lexicalGlobalObject = [self _rootObject]->globalObject();
    JSLockHolder lock(lexicalGlobalObject);

    return [(__bridge id)convertValueToObjcValue(lexicalGlobalObject, [self _imp], ObjcObjectType).objectValue description];
}

- (id)webScriptValueAtIndex:(unsigned)index
{
    if (![self _isSafeScript])
        return nil;

    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::JSGlobalObject* lexicalGlobalObject = globalObject;

    JSC::JSValue result = [self _imp]->get(lexicalGlobalObject, index);

    if (UNLIKELY(scope.exception())) {
        addExceptionToConsole(lexicalGlobalObject);
        result = jsUndefined();
        scope.clearException();
    }

    id resultObj = [WebScriptObject _convertValueToObjcValue:result originRootObject:[self _originRootObject] rootObject:[self _rootObject]];

    return resultObj;
}

- (void)setWebScriptValueAtIndex:(unsigned)index value:(id)value
{
    if (![self _isSafeScript])
        return;

    auto globalObject = [self _rootObject]->globalObject();
    auto& vm = globalObject->vm();
    JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::JSGlobalObject* lexicalGlobalObject = globalObject;

    [self _imp]->methodTable(vm)->putByIndex([self _imp], lexicalGlobalObject, index, convertObjcValueToValue(lexicalGlobalObject, &value, ObjcObjectType, [self _rootObject]), false);

    if (UNLIKELY(scope.exception())) {
        addExceptionToConsole(lexicalGlobalObject);
        scope.clearException();
    }
}

- (void)setException:(NSString *)description
{
    if (![self _rootObject])
        return;
    ObjcInstance::setGlobalException(description, [self _rootObject]->globalObject());
}

- (JSObjectRef)JSObject
{
    if (![self _isSafeScript])
        return 0;
    JSC::JSGlobalObject* lexicalGlobalObject = [self _rootObject]->globalObject();

    JSLockHolder lock(lexicalGlobalObject);
    return toRef([self _imp]);
}

+ (id)_convertValueToObjcValue:(JSC::JSValue)value originRootObject:(RootObject*)originRootObject rootObject:(RootObject*)rootObject
{
    if (value.isObject()) {
        JSObject* object = asObject(value);
        JSC::VM& vm = rootObject->globalObject()->vm();
        JSLockHolder lock(vm);

        if (object->inherits<JSHTMLElement>(vm)) {
            // Plugin elements cache the instance internally.
            if (ObjcInstance* instance = static_cast<ObjcInstance*>(pluginInstance(jsCast<JSHTMLElement*>(object)->wrapped())))
                return instance->getObject();
        } else if (object->inherits<ObjCRuntimeObject>(vm)) {
            ObjCRuntimeObject* runtimeObject = static_cast<ObjCRuntimeObject*>(object);
            ObjcInstance* instance = runtimeObject->getInternalObjCInstance();
            if (instance)
                return instance->getObject();
            return nil;
        }

        return [WebScriptObject scriptObjectForJSObject:toRef(object) originRootObject:originRootObject rootObject:rootObject];
    }

    if (value.isString())
        return asString(value)->value(rootObject->globalObject());

    if (value.isNumber())
        return [NSNumber numberWithDouble:value.asNumber()];

    if (value.isBoolean())
        return [NSNumber numberWithBool:value.asBoolean()];

    if (value.isUndefined())
        return [WebUndefined undefined];

    // jsNull is not returned as NSNull because existing applications do not expect
    // that return value. Return as nil for compatibility. <rdar://problem/4651318> <rdar://problem/4701626>
    // Other types (e.g., UnspecifiedType) also return as nil.
    return nil;
}


#if JSC_OBJC_API_ENABLED
- (JSValue *)JSValue
{
    if (![self _isSafeScript])
        return 0;

    return [JSValue valueWithJSValueRef:[self JSObject]
                    inContext:[JSContext contextWithJSGlobalContextRef:[self _globalContextRef]]];
}
#endif

@end

@interface WebScriptObject (WebKitCocoaBindings)

- (id)objectAtIndex:(unsigned)index;

@end

@implementation WebScriptObject (WebKitCocoaBindings)

#if 0

// FIXME: We'd like to add this, but we can't do that until this issue is resolved:
// http://bugs.webkit.org/show_bug.cgi?id=13129: presence of 'count' method on
// WebScriptObject breaks Democracy player.

- (unsigned)count
{
    id length = [self valueForKey:@"length"];
    if (![length respondsToSelector:@selector(intValue)])
        return 0;
    return [length intValue];
}

#endif

- (id)objectAtIndex:(unsigned)index
{
    return [self webScriptValueAtIndex:index];
}

@end

@implementation WebUndefined

+ (id)allocWithZone:(NSZone *)unusedZone
{
    UNUSED_PARAM(unusedZone);

    static NeverDestroyed<RetainPtr<WebUndefined>> sharedUndefined;
    if (!sharedUndefined.get())
        sharedUndefined.get() = adoptNS([super allocWithZone:nullptr]);
    return [sharedUndefined.get() retain];
}

- (NSString *)description
{
    return @"undefined";
}

- (id)initWithCoder:(NSCoder *)unusedCoder
{
    UNUSED_PARAM(unusedCoder);

    return self;
}

- (void)encodeWithCoder:(NSCoder *)unusedCoder
{
    UNUSED_PARAM(unusedCoder);
}

- (id)copyWithZone:(NSZone *)unusedZone
{
    UNUSED_PARAM(unusedZone);

    return self;
}

- (id)retain
{
    return self;
}

- (oneway void)release
{
}

- (NSUInteger)retainCount
{
    return NSUIntegerMax;
}

- (id)autorelease
{
    return self;
}

IGNORE_WARNINGS_BEGIN("objc-missing-super-calls")
- (void)dealloc
{
    // Intentionally not calling [super dealloc] since we never want to deallocate our single instance.
}
IGNORE_WARNINGS_END

+ (WebUndefined *)undefined
{
    return [[WebUndefined allocWithZone:NULL] autorelease];
}

@end
