blob: 85e5e9f0d0a776b14a40436c8c8ca38762daf73f [file] [log] [blame]
/*
* Copyright (C) 2010 Google Inc. All rights reserved.
* Copyright (C) 2012 Michael Pruett <michael@68k.org>
* Copyright (C) 2014-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 AND ITS CONTRIBUTORS "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 OR ITS 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.
*/
#include "config.h"
#if ENABLE(INDEXED_DATABASE)
#include "IDBBindingUtilities.h"
#include "ExceptionCode.h"
#include "IDBIndexInfo.h"
#include "IDBKey.h"
#include "IDBKeyData.h"
#include "IDBKeyPath.h"
#include "IDBValue.h"
#include "IndexKey.h"
#include "JSBlob.h"
#include "JSDOMBinding.h"
#include "JSDOMConvertDate.h"
#include "JSDOMConvertNullable.h"
#include "JSDOMExceptionHandling.h"
#include "JSFile.h"
#include "Logging.h"
#include "MessagePort.h"
#include "ScriptExecutionContext.h"
#include "SerializedScriptValue.h"
#include "SharedBuffer.h"
#include "ThreadSafeDataBuffer.h"
#include <JavaScriptCore/ArrayBuffer.h>
#include <JavaScriptCore/DateInstance.h>
#include <JavaScriptCore/ObjectConstructor.h>
namespace WebCore {
using namespace JSC;
static bool get(ExecState& exec, JSValue object, const String& keyPathElement, JSValue& result)
{
if (object.isString() && keyPathElement == "length") {
result = jsNumber(asString(object)->length());
return true;
}
if (!object.isObject())
return false;
VM& vm = exec.vm();
auto* obj = asObject(object);
Identifier identifier = Identifier::fromString(vm, keyPathElement);
if (obj->inherits<JSArray>(vm) && keyPathElement == "length") {
result = obj->get(&exec, identifier);
return true;
}
if (obj->inherits<JSBlob>(vm) && (keyPathElement == "size" || keyPathElement == "type")) {
if (keyPathElement == "size") {
result = jsNumber(jsCast<JSBlob*>(obj)->wrapped().size());
return true;
}
if (keyPathElement == "type") {
result = jsString(vm, jsCast<JSBlob*>(obj)->wrapped().type());
return true;
}
}
if (obj->inherits<JSFile>(vm)) {
if (keyPathElement == "name") {
result = jsString(vm, jsCast<JSFile*>(obj)->wrapped().name());
return true;
}
if (keyPathElement == "lastModified") {
result = jsNumber(jsCast<JSFile*>(obj)->wrapped().lastModified());
return true;
}
if (keyPathElement == "lastModifiedDate") {
result = jsDate(exec, jsCast<JSFile*>(obj)->wrapped().lastModified());
return true;
}
}
PropertyDescriptor descriptor;
if (!obj->getOwnPropertyDescriptor(&exec, identifier, descriptor))
return false;
if (!descriptor.enumerable())
return false;
result = obj->get(&exec, identifier);
return true;
}
static bool canSet(JSValue object, const String& keyPathElement)
{
UNUSED_PARAM(keyPathElement);
return object.isObject();
}
static bool set(ExecState& exec, JSValue& object, const String& keyPathElement, JSValue jsValue)
{
if (!canSet(object, keyPathElement))
return false;
VM& vm = exec.vm();
Identifier identifier = Identifier::fromString(vm, keyPathElement);
asObject(object)->putDirect(vm, identifier, jsValue);
return true;
}
JSValue toJS(ExecState& state, JSGlobalObject& globalObject, IDBKey* key)
{
if (!key) {
// This must be undefined, not null.
// Spec: http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#idl-def-IDBKeyRange
return jsUndefined();
}
VM& vm = state.vm();
Locker<JSLock> locker(vm.apiLock());
auto scope = DECLARE_THROW_SCOPE(vm);
switch (key->type()) {
case IndexedDB::KeyType::Array: {
auto& inArray = key->array();
unsigned size = inArray.size();
auto outArray = constructEmptyArray(&state, 0, &globalObject, size);
RETURN_IF_EXCEPTION(scope, JSValue());
for (size_t i = 0; i < size; ++i) {
outArray->putDirectIndex(&state, i, toJS(state, globalObject, inArray.at(i).get()));
RETURN_IF_EXCEPTION(scope, JSValue());
}
return outArray;
}
case IndexedDB::KeyType::Binary: {
auto* data = key->binary().data();
if (!data) {
ASSERT_NOT_REACHED();
return jsNull();
}
auto arrayBuffer = ArrayBuffer::create(data->data(), data->size());
Structure* structure = globalObject.arrayBufferStructure(arrayBuffer->sharingMode());
if (!structure)
return jsNull();
return JSArrayBuffer::create(state.vm(), structure, WTFMove(arrayBuffer));
}
case IndexedDB::KeyType::String:
return jsStringWithCache(&state, key->string());
case IndexedDB::KeyType::Date:
// FIXME: This should probably be toJS<IDLDate>(...) as per:
// http://w3c.github.io/IndexedDB/#request-convert-a-key-to-a-value
return toJS<IDLNullable<IDLDate>>(state, key->date());
case IndexedDB::KeyType::Number:
return jsNumber(key->number());
case IndexedDB::KeyType::Min:
case IndexedDB::KeyType::Max:
case IndexedDB::KeyType::Invalid:
ASSERT_NOT_REACHED();
return jsUndefined();
}
ASSERT_NOT_REACHED();
return jsUndefined();
}
static const size_t maximumDepth = 2000;
static RefPtr<IDBKey> createIDBKeyFromValue(ExecState& exec, JSValue value, Vector<JSArray*>& stack)
{
VM& vm = exec.vm();
if (value.isNumber() && !std::isnan(value.toNumber(&exec)))
return IDBKey::createNumber(value.toNumber(&exec));
if (value.isString())
return IDBKey::createString(asString(value)->value(&exec));
if (value.inherits<DateInstance>(vm)) {
auto dateValue = valueToDate(exec, value);
if (!std::isnan(dateValue))
return IDBKey::createDate(dateValue);
}
if (value.isObject()) {
JSObject* object = asObject(value);
if (auto* array = jsDynamicCast<JSArray*>(vm, object)) {
size_t length = array->length();
if (stack.contains(array))
return nullptr;
if (stack.size() >= maximumDepth)
return nullptr;
stack.append(array);
Vector<RefPtr<IDBKey>> subkeys;
for (size_t i = 0; i < length; i++) {
JSValue item = array->getIndex(&exec, i);
RefPtr<IDBKey> subkey = createIDBKeyFromValue(exec, item, stack);
if (!subkey)
subkeys.append(IDBKey::createInvalid());
else
subkeys.append(subkey);
}
stack.removeLast();
return IDBKey::createArray(subkeys);
}
if (auto* arrayBuffer = jsDynamicCast<JSArrayBuffer*>(vm, value))
return IDBKey::createBinary(*arrayBuffer);
if (auto* arrayBufferView = jsDynamicCast<JSArrayBufferView*>(vm, value))
return IDBKey::createBinary(*arrayBufferView);
}
return nullptr;
}
static Ref<IDBKey> createIDBKeyFromValue(ExecState& exec, JSValue value)
{
Vector<JSArray*> stack;
RefPtr<IDBKey> key = createIDBKeyFromValue(exec, value, stack);
if (key)
return *key;
return IDBKey::createInvalid();
}
static JSValue getNthValueOnKeyPath(ExecState& exec, JSValue rootValue, const Vector<String>& keyPathElements, size_t index)
{
JSValue currentValue(rootValue);
ASSERT(index <= keyPathElements.size());
for (size_t i = 0; i < index; i++) {
JSValue parentValue(currentValue);
if (!get(exec, parentValue, keyPathElements[i], currentValue))
return jsUndefined();
}
return currentValue;
}
static RefPtr<IDBKey> internalCreateIDBKeyFromScriptValueAndKeyPath(ExecState& exec, const JSValue& value, const String& keyPath)
{
Vector<String> keyPathElements;
IDBKeyPathParseError error;
IDBParseKeyPath(keyPath, keyPathElements, error);
ASSERT(error == IDBKeyPathParseError::None);
JSValue jsValue = value;
jsValue = getNthValueOnKeyPath(exec, jsValue, keyPathElements, keyPathElements.size());
if (jsValue.isUndefined())
return nullptr;
return createIDBKeyFromValue(exec, jsValue);
}
static JSValue ensureNthValueOnKeyPath(ExecState& exec, JSValue rootValue, const Vector<String>& keyPathElements, size_t index)
{
JSValue currentValue(rootValue);
ASSERT(index <= keyPathElements.size());
for (size_t i = 0; i < index; i++) {
JSValue parentValue(currentValue);
const String& keyPathElement = keyPathElements[i];
if (!get(exec, parentValue, keyPathElement, currentValue)) {
JSObject* object = constructEmptyObject(&exec);
if (!set(exec, parentValue, keyPathElement, JSValue(object)))
return jsUndefined();
currentValue = JSValue(object);
}
}
return currentValue;
}
static bool canInjectNthValueOnKeyPath(ExecState& exec, JSValue rootValue, const Vector<String>& keyPathElements, size_t index)
{
if (!rootValue.isObject())
return false;
JSValue currentValue(rootValue);
ASSERT(index <= keyPathElements.size());
for (size_t i = 0; i <= index; ++i) {
JSValue parentValue(currentValue);
const String& keyPathElement = keyPathElements[i];
if (!get(exec, parentValue, keyPathElement, currentValue))
return canSet(parentValue, keyPathElement);
}
return true;
}
bool injectIDBKeyIntoScriptValue(ExecState& exec, const IDBKeyData& keyData, JSValue value, const IDBKeyPath& keyPath)
{
LOG(IndexedDB, "injectIDBKeyIntoScriptValue");
ASSERT(WTF::holds_alternative<String>(keyPath));
Vector<String> keyPathElements;
IDBKeyPathParseError error;
IDBParseKeyPath(WTF::get<String>(keyPath), keyPathElements, error);
ASSERT(error == IDBKeyPathParseError::None);
if (keyPathElements.isEmpty())
return false;
JSValue parent = ensureNthValueOnKeyPath(exec, value, keyPathElements, keyPathElements.size() - 1);
if (parent.isUndefined())
return false;
auto key = keyData.maybeCreateIDBKey();
if (!key)
return false;
// Do not set if object already has the correct property value.
JSValue existingKey;
if (get(exec, parent, keyPathElements.last(), existingKey) && !key->compare(createIDBKeyFromValue(exec, existingKey)))
return true;
if (!set(exec, parent, keyPathElements.last(), toJS(exec, *exec.lexicalGlobalObject(), key.get())))
return false;
return true;
}
RefPtr<IDBKey> maybeCreateIDBKeyFromScriptValueAndKeyPath(ExecState& exec, const JSValue& value, const IDBKeyPath& keyPath)
{
if (WTF::holds_alternative<Vector<String>>(keyPath)) {
auto& array = WTF::get<Vector<String>>(keyPath);
Vector<RefPtr<IDBKey>> result;
result.reserveInitialCapacity(array.size());
for (auto& string : array) {
RefPtr<IDBKey> key = internalCreateIDBKeyFromScriptValueAndKeyPath(exec, value, string);
if (!key)
return nullptr;
result.uncheckedAppend(WTFMove(key));
}
return IDBKey::createArray(WTFMove(result));
}
return internalCreateIDBKeyFromScriptValueAndKeyPath(exec, value, WTF::get<String>(keyPath));
}
bool canInjectIDBKeyIntoScriptValue(ExecState& exec, const JSValue& scriptValue, const IDBKeyPath& keyPath)
{
LOG(StorageAPI, "canInjectIDBKeyIntoScriptValue");
ASSERT(WTF::holds_alternative<String>(keyPath));
Vector<String> keyPathElements;
IDBKeyPathParseError error;
IDBParseKeyPath(WTF::get<String>(keyPath), keyPathElements, error);
ASSERT(error == IDBKeyPathParseError::None);
if (!keyPathElements.size())
return false;
return canInjectNthValueOnKeyPath(exec, scriptValue, keyPathElements, keyPathElements.size() - 1);
}
static JSValue deserializeIDBValueToJSValue(ExecState& state, JSC::JSGlobalObject& globalObject, const IDBValue& value)
{
// FIXME: I think it's peculiar to use undefined to mean "null data" and null to mean "empty data".
// But I am not changing this at the moment because at least some callers are specifically checking isUndefined.
if (!value.data().data())
return jsUndefined();
auto& data = *value.data().data();
if (data.isEmpty())
return jsNull();
auto serializedValue = SerializedScriptValue::createFromWireBytes(Vector<uint8_t>(data));
state.vm().apiLock().lock();
Vector<RefPtr<MessagePort>> messagePorts;
JSValue result = serializedValue->deserialize(state, &globalObject, messagePorts, value.blobURLs(), value.blobFilePaths(), SerializationErrorMode::NonThrowing);
state.vm().apiLock().unlock();
return result;
}
JSValue deserializeIDBValueToJSValue(ExecState& state, const IDBValue& value)
{
return deserializeIDBValueToJSValue(state, *state.lexicalGlobalObject(), value);
}
JSC::JSValue toJS(JSC::ExecState* state, JSDOMGlobalObject* globalObject, const IDBValue& value)
{
ASSERT(state);
return deserializeIDBValueToJSValue(*state, *globalObject, value);
}
Ref<IDBKey> scriptValueToIDBKey(ExecState& exec, const JSValue& scriptValue)
{
return createIDBKeyFromValue(exec, scriptValue);
}
JSC::JSValue toJS(JSC::ExecState* state, JSDOMGlobalObject* globalObject, const IDBKeyData& keyData)
{
ASSERT(state);
ASSERT(globalObject);
return toJS(*state, *globalObject, keyData.maybeCreateIDBKey().get());
}
static Vector<IDBKeyData> createKeyPathArray(ExecState& exec, JSValue value, const IDBIndexInfo& info, Optional<IDBKeyPath> objectStoreKeyPath, const IDBKeyData& objectStoreKey)
{
auto visitor = WTF::makeVisitor([&](const String& string) -> Vector<IDBKeyData> {
// Value doesn't contain auto-generated key, so we need to manually add key if it is possibly auto-generated.
if (objectStoreKeyPath && WTF::holds_alternative<String>(objectStoreKeyPath.value()) && IDBKeyPath(string) == objectStoreKeyPath.value())
return { objectStoreKey };
auto idbKey = internalCreateIDBKeyFromScriptValueAndKeyPath(exec, value, string);
if (!idbKey)
return { };
Vector<IDBKeyData> keys;
if (info.multiEntry() && idbKey->type() == IndexedDB::Array) {
for (auto& key : idbKey->array())
keys.append(key.get());
} else
keys.append(idbKey.get());
return keys;
}, [&](const Vector<String>& vector) -> Vector<IDBKeyData> {
Vector<IDBKeyData> keys;
for (auto& entry : vector) {
if (objectStoreKeyPath && WTF::holds_alternative<String>(objectStoreKeyPath.value()) && IDBKeyPath(entry) == objectStoreKeyPath.value())
keys.append(objectStoreKey);
else {
auto key = internalCreateIDBKeyFromScriptValueAndKeyPath(exec, value, entry);
if (!key || !key->isValid())
return { };
keys.append(key.get());
}
}
return keys;
});
return WTF::visit(visitor, info.keyPath());
}
void generateIndexKeyForValue(ExecState& exec, const IDBIndexInfo& info, JSValue value, IndexKey& outKey, const Optional<IDBKeyPath>& objectStoreKeyPath, const IDBKeyData& objectStoreKey)
{
auto keyDatas = createKeyPathArray(exec, value, info, objectStoreKeyPath, objectStoreKey);
if (keyDatas.isEmpty())
return;
outKey = IndexKey(WTFMove(keyDatas));
}
Optional<JSC::JSValue> deserializeIDBValueWithKeyInjection(ExecState& state, const IDBValue& value, const IDBKeyData& key, const Optional<IDBKeyPath>& keyPath)
{
auto jsValue = deserializeIDBValueToJSValue(state, value);
if (jsValue.isUndefined() || !keyPath || !WTF::holds_alternative<String>(keyPath.value()) || !isIDBKeyPathValid(keyPath.value()))
return jsValue;
JSLockHolder locker(state.vm());
if (!injectIDBKeyIntoScriptValue(state, key, jsValue, keyPath.value())) {
auto throwScope = DECLARE_THROW_SCOPE(state.vm());
propagateException(state, throwScope, Exception(UnknownError, "Cannot inject key into script value"_s));
return WTF::nullopt;
}
return jsValue;
}
} // namespace WebCore
#endif // ENABLE(INDEXED_DATABASE)