| /* |
| * Copyright 2008 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| #include "FFSessionHandler.h" |
| #include "HostChannel.h" |
| #include "JavaObject.h" |
| #include "JSRunner.h" |
| #include "Debug.h" |
| #include "XpcomDebug.h" |
| #include "scoped_ptr/scoped_ptr.h" |
| #include "RootedObject.h" |
| #include "InvokeMessage.h" |
| #include "ServerMethods.h" |
| #include "AllowedConnections.h" |
| |
| #include "jsapi.h" |
| #include "nsCOMPtr.h" |
| #include "nsStringAPI.h" |
| #include "nsIJSContextStack.h" |
| #include "nsIPrincipal.h" |
| #include "nsServiceManagerUtils.h" |
| |
| static JSContext* getJSContext() { |
| // Get JSContext from stack. |
| nsCOMPtr<nsIJSContextStack> stack = |
| do_GetService("@mozilla.org/js/xpc/ContextStack;1"); |
| if (!stack) { |
| return NULL; |
| } |
| |
| JSContext *cx; |
| if (NS_FAILED(stack->Peek(&cx))) { |
| return NULL; |
| } |
| |
| if (cx == nsnull) { |
| // TODO(jat): figure out why this can be null at plugin unload time |
| Debug::log(Debug::Error) << "GWT DMP: Null JS context" << Debug::flush; |
| } |
| |
| return cx; |
| } |
| |
| FFSessionHandler::FFSessionHandler(HostChannel* channel) |
| : SessionData(channel, this, getJSContext()), jsObjectId(0), |
| jsObjectsById(NULL), stringObjectClass(NULL) { |
| // TODO(jat): is there a way to avoid calling this twice, without keeping |
| // JSContext in an instance field? |
| JSContext* ctx = getJSContext(); |
| if (!JS_AddNamedRoot(ctx, &jsObjectsById, "jsObjectsById")) { |
| Debug::log(Debug::Error) << "Error rooting jsObjectsById" << Debug::flush; |
| } |
| jsObjectsById = JS_NewArrayObject(ctx, 0, NULL); |
| if (!jsObjectsById) { |
| Debug::log(Debug::Error) << "Error rooting jsObjectsById" << Debug::flush; |
| } |
| if (!JS_AddNamedRoot(ctx, &toStringTearOff, "toStringTearOff")) { |
| Debug::log(Debug::Error) << "Error rooting toStringTearOff" << Debug::flush; |
| } |
| getStringObjectClass(ctx); |
| getToStringTearOff(ctx); |
| } |
| |
| void FFSessionHandler::getStringObjectClass(JSContext* ctx) { |
| jsval str = JS_GetEmptyStringValue(ctx); |
| JSObject* obj = 0; |
| if (!JS_ValueToObject(ctx, str, &obj)) { |
| return; |
| } |
| if (!obj) { |
| return; |
| } |
| stringObjectClass = JS_GET_CLASS(ctx, obj); |
| } |
| |
| void FFSessionHandler::getToStringTearOff(JSContext* ctx) { |
| jsval funcVal; |
| |
| Debug::log(Debug::Debugging) << "Getting function \"__gwt_makeTearOff\"" |
| << Debug::flush; |
| |
| if (!JS_GetProperty(ctx, global, "__gwt_makeTearOff", &funcVal) |
| || funcVal == JSVAL_VOID) { |
| Debug::log(Debug::Error) << "Could not get function \"__gwt_makeTearOff\"" |
| << Debug::flush; |
| return; |
| } |
| jsval jsargs[3] = { |
| JSVAL_NULL, // no proxy |
| INT_TO_JSVAL(InvokeMessage::TOSTRING_DISP_ID), // dispId |
| JSVAL_ZERO // arg count is zero |
| }; |
| if (!JS_CallFunctionValue(ctx, global, funcVal, 3, jsargs, &toStringTearOff)) { |
| jsval exc; |
| if (JS_GetPendingException(ctx, &exc)) { |
| Debug::log(Debug::Error) |
| << "__gwt_makeTearOff(null,0,0) threw exception " |
| << dumpJsVal(ctx, exc) << Debug::flush; |
| } else { |
| Debug::log(Debug::Error) << "Error creating toString tear-off" |
| << Debug::flush; |
| } |
| // TODO(jat): show some crash page and die |
| } |
| } |
| |
| FFSessionHandler::~FFSessionHandler(void) { |
| Debug::log(Debug::Debugging) << "FFSessionHandler::~FFSessionHandler" << Debug::flush; |
| disconnect(); |
| if (runtime) { |
| JS_RemoveRootRT(runtime, &jsObjectsById); |
| jsObjectsById = NULL; |
| JS_RemoveRootRT(runtime, &toStringTearOff); |
| runtime = NULL; |
| } |
| } |
| |
| void FFSessionHandler::freeValue(HostChannel& channel, int idCount, const int* ids) { |
| Debug::DebugStream& dbg = Debug::log(Debug::Spam) |
| << "FFSessionHandler::freeValue [ "; |
| JSContext* ctx = getJSContext(); |
| |
| for (int i = 0; i < idCount; ++i) { |
| int objId = ids[i]; |
| dbg << objId << " "; |
| jsval toRemove; |
| if (JS_GetElement(ctx, jsObjectsById, objId, &toRemove) && JSVAL_IS_OBJECT(toRemove)) { |
| jsIdsByObject.erase(identityFromObject(JSVAL_TO_OBJECT(toRemove))); |
| JS_DeleteElement(ctx, jsObjectsById, objId); |
| } else { |
| Debug::log(Debug::Error) << "Error deleting js objId=" << objId << Debug::flush; |
| } |
| } |
| |
| dbg << "]" << Debug::flush; |
| } |
| |
| void FFSessionHandler::loadJsni(HostChannel& channel, const std::string& js) { |
| Debug::log(Debug::Spam) << "FFSessionHandler::loadJsni " << js << "(EOM)" << Debug::flush; |
| JSContext* ctx = getJSContext(); |
| if (!JSRunner::eval(ctx, global, js)) { |
| Debug::log(Debug::Error) << "Error executing script" << Debug::flush; |
| } |
| } |
| |
| void FFSessionHandler::sendFreeValues(HostChannel& channel) { |
| unsigned n = javaObjectsToFree.size(); |
| if (n) { |
| scoped_array<int> ids(new int[n]); |
| int i = 0; |
| for (std::set<int>::iterator it = javaObjectsToFree.begin(); |
| it != javaObjectsToFree.end(); ++it) { |
| ids[i++] = *it; |
| } |
| if (ServerMethods::freeJava(channel, this, n, ids.get())) { |
| javaObjectsToFree.clear(); |
| } |
| } |
| } |
| |
| void FFSessionHandler::fatalError(HostChannel& channel, |
| const std::string& message) { |
| // TODO(jat): implement |
| } |
| |
| bool FFSessionHandler::invoke(HostChannel& channel, const Value& thisObj, const std::string& methodName, |
| int numArgs, const Value* const args, Value* returnValue) { |
| Debug::log(Debug::Spam) << "FFSessionHandler::invoke " << thisObj.toString() |
| << "::" << methodName << Debug::flush; |
| JSContext* ctx = getJSContext(); |
| |
| // Used to root JSthis and args while making the JS call |
| // TODO(jat): keep one object and just keep a "stack pointer" into that |
| // object on the native stack so we don't keep allocating/rooting/freeing |
| // an object |
| RootedObject argsRoot(ctx, "FFSessionhandler::invoke"); |
| argsRoot = JS_NewArrayObject(ctx, 0, NULL); |
| if (!JS_SetArrayLength(ctx, argsRoot.get(), numArgs + 1)) { |
| Debug::log(Debug::Error) |
| << "FFSessionhandler::invoke - could not set argsRoot length" |
| << Debug::flush; |
| return true; |
| } |
| |
| jsval jsThis; |
| if (thisObj.isNull()) { |
| jsThis = OBJECT_TO_JSVAL(global); |
| Debug::log(Debug::Spam) << " using global object for this" << Debug::flush; |
| } else { |
| makeJsvalFromValue(jsThis, ctx, thisObj); |
| if (Debug::level(Debug::Spam)) { |
| Debug::log(Debug::Spam) << " obj=" << dumpJsVal(ctx, jsThis) |
| << Debug::flush; |
| } |
| } |
| if (!JS_SetElement(ctx, argsRoot.get(), 0, &jsThis)) { |
| Debug::log(Debug::Error) |
| << "FFSessionhandler::invoke - could not set argsRoot[0] to this" |
| << Debug::flush; |
| return true; |
| } |
| |
| jsval funcVal; |
| // TODO: handle non-ASCII method names |
| if (!JS_GetProperty(ctx, global, methodName.c_str(), &funcVal) |
| || funcVal == JSVAL_VOID) { |
| Debug::log(Debug::Error) << "Could not get function " << methodName |
| << Debug::flush; |
| return true; |
| } |
| |
| scoped_array<jsval> jsargs(new jsval[numArgs]); |
| for (int i = 0; i < numArgs; ++i) { |
| makeJsvalFromValue(jsargs[i], ctx, args[i]); |
| if (Debug::level(Debug::Spam)) { |
| Debug::log(Debug::Spam) << " arg[" << i << "] = " << dumpJsVal(ctx, |
| jsargs[i]) << Debug::flush; |
| } |
| if (!JS_SetElement(ctx, argsRoot.get(), i + 1, &jsargs[i])) { |
| Debug::log(Debug::Error) |
| << "FFSessionhandler::invoke - could not set args[" << (i + 1) << "]" |
| << Debug::flush; |
| return true; |
| } |
| } |
| |
| if (JS_IsExceptionPending(ctx)) { |
| JS_ClearPendingException(ctx); |
| } |
| |
| jsval rval; |
| JSBool ok = JS_CallFunctionValue(ctx, JSVAL_TO_OBJECT(jsThis), funcVal, |
| numArgs, jsargs.get(), &rval); |
| |
| if (!ok) { |
| if (JS_GetPendingException(ctx, &rval)) { |
| makeValueFromJsval(*returnValue, ctx, rval); |
| Debug::log(Debug::Debugging) << "FFSessionHandler::invoke " |
| << thisObj.toString() << "::" << methodName << " threw exception " |
| << dumpJsVal(ctx, rval) << Debug::flush; |
| } else { |
| Debug::log(Debug::Error) << "Non-exception failure invoking " |
| << methodName << Debug::flush; |
| returnValue->setUndefined(); |
| } |
| } else { |
| makeValueFromJsval(*returnValue, ctx, rval); |
| } |
| Debug::log(Debug::Spam) << " return= " << *returnValue << Debug::flush; |
| return !ok; |
| } |
| |
| /** |
| * Invoke a plugin-provided method with the given args. As above, this method does not own |
| * any of its args. |
| * |
| * Returns true if an exception occurred. |
| */ |
| bool FFSessionHandler::invokeSpecial(HostChannel& channel, SpecialMethodId method, int numArgs, |
| const Value* const args, Value* returnValue) { |
| Debug::log(Debug::Spam) << "FFSessionHandler::invokeSpecial" << Debug::flush; |
| return false; |
| } |
| |
| /** |
| * Convert UTF16 string to UTF8-encoded std::string. |
| * |
| * This is implemented here because the Mozilla libraries mangle certain UTF8 |
| * strings. |
| * |
| * @return UTF8-encoded string. |
| */ |
| static std::string utf8String(const jschar* str, unsigned len) { |
| std::string utf8str; |
| while (len-- > 0) { |
| unsigned ch = *str++; |
| // check for paired surrogates first, leave unpaired surrogates as-is |
| if (ch >= 0xD800 && ch < 0xDC00 && len > 0 && *str >= 0xDC00 && *str < 0xE000) { |
| ch = ((ch & 1023) << 10) + (*str++ & 1023) + 0x10000; |
| len--; |
| } |
| if (ch < 0x80) { // U+0000 - U+007F as 0xxxxxxx |
| utf8str.append(1, ch); |
| } else if (ch < 0x800) { // U+0080 - U+07FF as 110xxxxx 10xxxxxx |
| utf8str.append(1, 0xC0 + ((ch >> 6) & 31)); |
| utf8str.append(1, 0x80 + (ch & 63)); |
| } else if (ch < 0x10000) { // U+0800 - U+FFFF as 1110xxxx 10xxxxxx 10xxxxxx |
| utf8str.append(1, 0xE0 + ((ch >> 12) & 15)); |
| utf8str.append(1, 0x80 + ((ch >> 6) & 63)); |
| utf8str.append(1, 0x80 + (ch & 63)); |
| } else { // rest as 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
| utf8str.append(1, 0xF0 + ((ch >> 18) & 7)); |
| utf8str.append(1, 0x80 + ((ch >> 12) & 63)); |
| utf8str.append(1, 0x80 + ((ch >> 6) & 63)); |
| utf8str.append(1, 0x80 + (ch & 63)); |
| } |
| } |
| return utf8str; |
| } |
| |
| /** |
| * Creates a JSString from a UTF8-encoded std::string. |
| * |
| * This is implemented here because the Mozilla libraries mangle certain UTF8 |
| * strings. |
| * |
| * @return the JSString object, which owns its memory buffer. |
| */ |
| static JSString* stringUtf8(JSContext* ctx, const std::string& utf8str) { |
| unsigned len = 0; |
| for (unsigned i = 0; i < utf8str.length(); ++i) { |
| char ch = utf8str[i]; |
| switch (ch & 0xF8) { |
| // continuation & invalid chars |
| default: |
| // ASCII characters |
| case 0x00: case 0x08: case 0x10: case 0x18: |
| case 0x20: case 0x28: case 0x30: case 0x38: |
| case 0x40: case 0x48: case 0x50: case 0x58: |
| case 0x60: case 0x68: case 0x70: case 0x78: |
| // 2-byte UTF8 characters |
| case 0xC0: case 0xC8: case 0xD0: case 0xD8: |
| // 3-byte UTF8 characters |
| case 0xE0: case 0xE8: |
| ++len; |
| break; |
| case 0xF0: |
| len += 2; |
| break; |
| } |
| } |
| // Account for null terminator even if it isn't included in the string length |
| // Note that buf becomes owned by the JSString and must not be freed here. |
| jschar* buf = static_cast<jschar*>(JS_malloc(ctx, (len + 1) * sizeof(jschar))); |
| if (!buf) { |
| return NULL; |
| } |
| jschar* p = buf; |
| unsigned codePoint; |
| int charsLeft = -1; |
| for (unsigned i = 0; i < utf8str.length(); ++i) { |
| char ch = utf8str[i]; |
| if (charsLeft >= 0) { |
| if ((ch & 0xC0) != 0x80) { |
| // invalid, missing continuation character |
| *p++ = static_cast<jschar>(0xFFFD); |
| charsLeft = -1; |
| } else { |
| codePoint = (codePoint << 6) | (ch & 63); |
| if (!--charsLeft) { |
| if (codePoint >= 0x10000) { |
| codePoint -= 0x10000; |
| *p++ = static_cast<jschar>(0xD800 + ((codePoint >> 10) & 1023)); |
| *p++ = static_cast<jschar>(0xDC00 + (codePoint & 1023)); |
| } else { |
| *p++ = static_cast<jschar>(codePoint); |
| } |
| charsLeft = -1; |
| } |
| } |
| continue; |
| } |
| // Look at the top 5 bits to determine how many bytes are in this character. |
| switch (ch & 0xF8) { |
| default: // skip invalid and continuation chars |
| break; |
| case 0x00: case 0x08: case 0x10: case 0x18: |
| case 0x20: case 0x28: case 0x30: case 0x38: |
| case 0x40: case 0x48: case 0x50: case 0x58: |
| case 0x60: case 0x68: case 0x70: case 0x78: |
| *p++ = static_cast<jschar>(ch); |
| break; |
| case 0xC0: case 0xC8: case 0xD0: case 0xD8: |
| charsLeft = 1; |
| codePoint = ch & 31; |
| break; |
| case 0xE0: case 0xE8: |
| charsLeft = 2; |
| codePoint = ch & 15; |
| break; |
| case 0xF0: |
| charsLeft = 3; |
| codePoint = ch & 7; |
| break; |
| } |
| } |
| // null terminator, apparently some code expects a terminator even though |
| // the strings are counted. Note that this null word should not be included |
| // in the length, and that the buffer becomes owned by the JSString object. |
| *p = 0; |
| return JS_NewUCString(ctx, buf, p - buf); |
| } |
| |
| void FFSessionHandler::makeValueFromJsval(Value& retVal, JSContext* ctx, |
| const jsval& value) { |
| if (JSVAL_IS_VOID(value)) { |
| retVal.setUndefined(); |
| } else if (JSVAL_IS_NULL(value)) { |
| retVal.setNull(); |
| } else if (JSVAL_IS_INT(value)) { |
| retVal.setInt(JSVAL_TO_INT(value)); |
| } else if (JSVAL_IS_BOOLEAN(value)) { |
| retVal.setBoolean(JSVAL_TO_BOOLEAN(value)); |
| } else if (JSVAL_IS_STRING(value)) { |
| JSString* str = JSVAL_TO_STRING(value); |
| retVal.setString(utf8String(JS_GetStringChars(str), |
| JS_GetStringLength(str))); |
| } else if (JSVAL_IS_DOUBLE(value)) { |
| retVal.setDouble(*JSVAL_TO_DOUBLE(value)); |
| } else if (JSVAL_IS_OBJECT(value)) { |
| JSObject* obj = JSVAL_TO_OBJECT(value); |
| if (JavaObject::isJavaObject(ctx, obj)) { |
| retVal.setJavaObject(JavaObject::getObjectId(ctx, obj)); |
| } else if (JS_GET_CLASS(ctx, obj) == stringObjectClass) { |
| // JS String wrapper object, treat as a string primitive |
| JSString* str = JS_ValueToString(ctx, value); |
| retVal.setString(utf8String(JS_GetStringChars(str), |
| JS_GetStringLength(str))); |
| // str will be garbage-collected, does not need to be freed |
| } else { |
| // It's a plain-old JavaScript Object |
| void* objKey = identityFromObject(obj); |
| std::map<void*, int>::iterator it = jsIdsByObject.find(objKey); |
| if (it != jsIdsByObject.end()) { |
| retVal.setJsObjectId(it->second); |
| } else { |
| // Allocate a new id |
| int objId = ++jsObjectId; |
| JS_SetElement(ctx, jsObjectsById, objId, const_cast<jsval*>(&value)); |
| jsIdsByObject[objKey] = objId; |
| retVal.setJsObjectId(objId); |
| } |
| } |
| } else { |
| Debug::log(Debug::Error) << "Unhandled jsval type " << Debug::flush; |
| retVal.setString("Unhandled jsval type"); |
| } |
| } |
| |
| void FFSessionHandler::makeJsvalFromValue(jsval& retVal, JSContext* ctx, |
| const Value& value) { |
| switch (value.getType()) { |
| case Value::NULL_TYPE: |
| retVal = JSVAL_NULL; |
| break; |
| case Value::BOOLEAN: |
| retVal = BOOLEAN_TO_JSVAL(value.getBoolean()); |
| break; |
| case Value::BYTE: |
| retVal = INT_TO_JSVAL((int) value.getByte()); |
| break; |
| case Value::CHAR: |
| retVal = INT_TO_JSVAL((int) value.getChar()); |
| break; |
| case Value::SHORT: |
| retVal = INT_TO_JSVAL((int) value.getShort()); |
| break; |
| case Value::INT: { |
| int intValue = value.getInt(); |
| if (INT_FITS_IN_JSVAL(intValue)) { |
| retVal = INT_TO_JSVAL(intValue); |
| } else { |
| JS_NewNumberValue(ctx, (jsdouble) intValue, &retVal); |
| } |
| break; |
| } |
| // TODO(jat): do we still need long support in the wire format and Value? |
| // case Value::LONG: |
| // retVal = value.getLong(); |
| // break; |
| case Value::FLOAT: |
| JS_NewNumberValue(ctx, (jsdouble) value.getFloat(), &retVal); |
| break; |
| case Value::DOUBLE: |
| JS_NewNumberValue(ctx, (jsdouble) value.getDouble(), &retVal); |
| break; |
| case Value::STRING: |
| { |
| JSString* str = stringUtf8(ctx, value.getString()); |
| retVal = STRING_TO_JSVAL(str); |
| } |
| break; |
| case Value::JAVA_OBJECT: |
| { |
| int javaId = value.getJavaObjectId(); |
| std::map<int, JSObject*>::iterator i = javaObjectsById.find(javaId); |
| if (i == javaObjectsById.end()) { |
| JSObject* obj = JavaObject::construct(ctx, this, javaId); |
| javaObjectsById[javaId] = obj; |
| // We may have previously released the proxy for the same object id, |
| // but have not yet sent a free message back to the server. |
| javaObjectsToFree.erase(javaId); |
| retVal = OBJECT_TO_JSVAL(obj); |
| } else { |
| retVal = OBJECT_TO_JSVAL(i->second); |
| } |
| } |
| break; |
| case Value::JS_OBJECT: |
| { |
| int jsId = value.getJsObjectId(); |
| if (!JS_GetElement(ctx, jsObjectsById, jsId, &retVal)) { |
| Debug::log(Debug::Error) << "Error getting jsObject with id " << jsId << Debug::flush; |
| } |
| if (!JSVAL_IS_OBJECT(retVal)) { |
| Debug::log(Debug::Error) << "Missing jsObject with id " << jsId << Debug::flush; |
| } |
| } |
| break; |
| case Value::UNDEFINED: |
| retVal = JSVAL_VOID; |
| break; |
| default: |
| Debug::log(Debug::Error) << "Unknown Value type " << value.toString() << Debug::flush; |
| } |
| } |
| |
| void FFSessionHandler::freeJavaObject(int objectId) { |
| if (!javaObjectsById.erase(objectId)) { |
| Debug::log(Debug::Error) << "Trying to free unknown JavaObject: " << objectId << Debug::flush; |
| return; |
| } |
| javaObjectsToFree.insert(objectId); |
| } |
| |
| void FFSessionHandler::disconnect() { |
| Debug::log(Debug::Debugging) << "FFSessionHandler::disconnect" << Debug::flush; |
| JSContext* ctx = getJSContext(); |
| bool freeCtx = false; |
| if (!ctx) { |
| Debug::log(Debug::Debugging) << " creating temporary context" |
| << Debug::flush; |
| freeCtx = true; |
| ctx = JS_NewContext(runtime, 8192); |
| if (ctx) { |
| JS_SetOptions(ctx, JSOPTION_VAROBJFIX); |
| #ifdef JSVERSION_LATEST |
| JS_SetVersion(ctx, JSVERSION_LATEST); |
| #endif |
| } |
| } |
| if (ctx) { |
| JS_BeginRequest(ctx); |
| for (std::map<int, JSObject*>::iterator it = javaObjectsById.begin(); |
| it != javaObjectsById.end(); ++it) { |
| int javaId = it->first; |
| JSObject* obj = it->second; |
| if (JavaObject::isJavaObject(ctx, obj)) { |
| // clear the SessionData pointer -- JavaObject knows it is |
| // disconnected if this is null |
| JS_SetPrivate(ctx, obj, NULL); |
| javaObjectsToFree.erase(javaId); |
| } |
| } |
| JS_EndRequest(ctx); |
| if (freeCtx) { |
| JS_DestroyContext(ctx); |
| } |
| } else { |
| Debug::log(Debug::Warning) |
| << "FFSessionHandler::disconnect - no context available" |
| << Debug::flush; |
| } |
| HostChannel* channel = getHostChannel(); |
| if (channel->isConnected()) { |
| channel->disconnectFromHost(); |
| } |
| } |
| |
| void* FFSessionHandler::identityFromObject(JSObject* obj) { |
| JSContext* ctx = getJSContext(); |
| jsval rval; |
| void* returnValue = obj; |
| if (JS_GetProperty(ctx, obj, "wrappedJSObject", &rval) |
| && JSVAL_IS_OBJECT(rval)) { |
| returnValue = JSVAL_TO_OBJECT(rval); |
| Debug::log(Debug::Info) << "identityFromObject mapped " << obj << " to " |
| << returnValue << Debug::flush; |
| } |
| return returnValue; |
| } |