1/*
2 * Copyright (C) 2015 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. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "UIScriptContext.h"
28
29#include "UIScriptController.h"
30#include <JavaScriptCore/JSContextRef.h>
31#include <JavaScriptCore/JSValueRef.h>
32#include <WebCore/FloatRect.h>
33
34using namespace WTR;
35
36static inline bool isPersistentCallbackID(unsigned callbackID)
37{
38 return callbackID < firstNonPersistentCallbackID;
39}
40
41UIScriptContext::UIScriptContext(UIScriptContextDelegate& delegate)
42 : m_context(adopt(JSGlobalContextCreate(nullptr)))
43 , m_delegate(delegate)
44{
45 m_controller = UIScriptController::create(*this);
46
47 JSObjectRef globalObject = JSContextGetGlobalObject(m_context.get());
48
49 JSValueRef exception = nullptr;
50 m_controller->makeWindowObject(m_context.get(), globalObject, &exception);
51}
52
53UIScriptContext::~UIScriptContext()
54{
55 m_controller->checkForOutstandingCallbacks();
56 m_controller->contextDestroyed();
57}
58
59void UIScriptContext::runUIScript(const String& script, unsigned scriptCallbackID)
60{
61 m_currentScriptCallbackID = scriptCallbackID;
62
63 auto stringRef = adopt(JSStringCreateWithUTF8CString(script.utf8().data()));
64
65 JSValueRef exception = nullptr;
66 JSValueRef result = JSEvaluateScript(m_context.get(), stringRef.get(), 0, 0, 1, &exception);
67
68 if (!hasOutstandingAsyncTasks()) {
69 JSValueRef stringifyException = nullptr;
70 auto stringified = adopt(JSValueToStringCopy(m_context.get(), result, &stringifyException));
71 requestUIScriptCompletion(stringified.get());
72 tryToCompleteUIScriptForCurrentParentCallback();
73 }
74}
75
76unsigned UIScriptContext::nextTaskCallbackID(CallbackType type)
77{
78 if (type == CallbackTypeNonPersistent)
79 return ++m_nextTaskCallbackID + firstNonPersistentCallbackID;
80
81 return type;
82}
83
84unsigned UIScriptContext::prepareForAsyncTask(JSValueRef callback, CallbackType type)
85{
86 unsigned callbackID = nextTaskCallbackID(type);
87
88 JSValueProtect(m_context.get(), callback);
89 Task task;
90 task.parentScriptCallbackID = m_currentScriptCallbackID;
91 task.callback = callback;
92
93 ASSERT(!m_callbacks.contains(callbackID));
94 m_callbacks.add(callbackID, task);
95
96 return callbackID;
97}
98
99void UIScriptContext::asyncTaskComplete(unsigned callbackID)
100{
101 Task task = m_callbacks.take(callbackID);
102 ASSERT(task.callback);
103
104 JSValueRef exception = nullptr;
105 JSObjectRef callbackObject = JSValueToObject(m_context.get(), task.callback, &exception);
106
107 m_currentScriptCallbackID = task.parentScriptCallbackID;
108
109 exception = nullptr;
110 JSObjectCallAsFunction(m_context.get(), callbackObject, JSContextGetGlobalObject(m_context.get()), 0, nullptr, &exception);
111 JSValueUnprotect(m_context.get(), task.callback);
112
113 tryToCompleteUIScriptForCurrentParentCallback();
114 m_currentScriptCallbackID = 0;
115}
116
117unsigned UIScriptContext::registerCallback(JSValueRef taskCallback, CallbackType type)
118{
119 if (m_callbacks.contains(type))
120 unregisterCallback(type);
121
122 if (JSValueIsUndefined(m_context.get(), taskCallback))
123 return 0;
124
125 return prepareForAsyncTask(taskCallback, type);
126}
127
128void UIScriptContext::unregisterCallback(unsigned callbackID)
129{
130 Task task = m_callbacks.take(callbackID);
131 ASSERT(task.callback);
132 JSValueUnprotect(m_context.get(), task.callback);
133}
134
135JSValueRef UIScriptContext::callbackWithID(unsigned callbackID)
136{
137 Task task = m_callbacks.get(callbackID);
138 return task.callback;
139}
140
141void UIScriptContext::fireCallback(unsigned callbackID)
142{
143 Task task = m_callbacks.get(callbackID);
144 ASSERT(task.callback);
145
146 JSValueRef exception = nullptr;
147 JSObjectRef callbackObject = JSValueToObject(m_context.get(), task.callback, &exception);
148
149 m_currentScriptCallbackID = task.parentScriptCallbackID;
150
151 exception = nullptr;
152 JSObjectCallAsFunction(m_context.get(), callbackObject, JSContextGetGlobalObject(m_context.get()), 0, nullptr, &exception);
153
154 tryToCompleteUIScriptForCurrentParentCallback();
155 m_currentScriptCallbackID = 0;
156}
157
158void UIScriptContext::requestUIScriptCompletion(JSStringRef result)
159{
160 ASSERT(m_currentScriptCallbackID);
161 if (currentParentCallbackIsPendingCompletion())
162 return;
163
164 // This request for the UI script to complete is not fulfilled until the last non-persistent task for the parent callback is finished.
165 m_uiScriptResultsPendingCompletion.add(m_currentScriptCallbackID, result ? JSStringRetain(result) : nullptr);
166}
167
168void UIScriptContext::tryToCompleteUIScriptForCurrentParentCallback()
169{
170 if (!currentParentCallbackIsPendingCompletion() || currentParentCallbackHasOutstandingAsyncTasks())
171 return;
172
173 JSStringRef result = m_uiScriptResultsPendingCompletion.take(m_currentScriptCallbackID);
174 String scriptResult(reinterpret_cast<const UChar*>(JSStringGetCharactersPtr(result)), JSStringGetLength(result));
175
176 m_delegate.uiScriptDidComplete(scriptResult, m_currentScriptCallbackID);
177
178 // Unregister tasks associated with this callback
179 m_callbacks.removeIf([&](auto& keyAndValue) {
180 return keyAndValue.value.parentScriptCallbackID == m_currentScriptCallbackID;
181 });
182
183 m_currentScriptCallbackID = 0;
184 if (result)
185 JSStringRelease(result);
186}
187
188JSObjectRef UIScriptContext::objectFromRect(const WebCore::FloatRect& rect) const
189{
190 JSObjectRef object = JSObjectMake(m_context.get(), nullptr, nullptr);
191
192 JSObjectSetProperty(m_context.get(), object, adopt(JSStringCreateWithUTF8CString("left")).get(), JSValueMakeNumber(m_context.get(), rect.x()), kJSPropertyAttributeNone, nullptr);
193 JSObjectSetProperty(m_context.get(), object, adopt(JSStringCreateWithUTF8CString("top")).get(), JSValueMakeNumber(m_context.get(), rect.y()), kJSPropertyAttributeNone, nullptr);
194 JSObjectSetProperty(m_context.get(), object, adopt(JSStringCreateWithUTF8CString("width")).get(), JSValueMakeNumber(m_context.get(), rect.width()), kJSPropertyAttributeNone, nullptr);
195 JSObjectSetProperty(m_context.get(), object, adopt(JSStringCreateWithUTF8CString("height")).get(), JSValueMakeNumber(m_context.get(), rect.height()), kJSPropertyAttributeNone, nullptr);
196
197 return object;
198}
199
200bool UIScriptContext::currentParentCallbackHasOutstandingAsyncTasks() const
201{
202 for (auto entry : m_callbacks) {
203 unsigned callbackID = entry.key;
204 Task task = entry.value;
205 if (task.parentScriptCallbackID == m_currentScriptCallbackID && !isPersistentCallbackID(callbackID))
206 return true;
207 }
208
209 return false;
210}
211
212