1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 #include "IDBObjectStore.h"
10 #include "IndexedDBCommon.h"
12 #include "ReportInternalError.h"
13 #include "js/Array.h" // JS::NewArrayObject
14 #include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_DeleteUCProperty
15 #include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnUCPropertyDescriptor
16 #include "mozilla/ResultExtensions.h"
17 #include "mozilla/dom/BindingDeclarations.h"
18 #include "mozilla/dom/Blob.h"
19 #include "mozilla/dom/BlobBinding.h"
20 #include "mozilla/dom/File.h"
21 #include "mozilla/dom/IDBObjectStoreBinding.h"
22 #include "mozilla/dom/quota/ResultExtensions.h"
23 #include "nsCharSeparatedTokenizer.h"
24 #include "nsJSUtils.h"
25 #include "nsPrintfCString.h"
26 #include "xpcpublic.h"
28 namespace mozilla::dom::indexedDB
{
32 using KeyPathTokenizer
=
33 nsCharSeparatedTokenizerTemplate
<NS_TokenizerIgnoreNothing
>;
35 bool IsValidKeyPathString(const nsAString
& aKeyPath
) {
36 NS_ASSERTION(!aKeyPath
.IsVoid(), "What?");
38 for (const auto& token
: KeyPathTokenizer(aKeyPath
, '.').ToRange()) {
39 if (token
.IsEmpty()) {
43 if (!JS_IsIdentifier(token
.Data(), token
.Length())) {
48 // If the very last character was a '.', the tokenizer won't give us an empty
49 // token, but the keyPath is still invalid.
50 return aKeyPath
.IsEmpty() || aKeyPath
.CharAt(aKeyPath
.Length() - 1) != '.';
53 enum KeyExtractionOptions
{ DoNotCreateProperties
, CreateProperties
};
55 nsresult
GetJSValFromKeyPathString(
56 JSContext
* aCx
, const JS::Value
& aValue
, const nsAString
& aKeyPathString
,
57 JS::Value
* aKeyJSVal
, KeyExtractionOptions aOptions
,
58 KeyPath::ExtractOrCreateKeyCallback aCallback
, void* aClosure
) {
59 NS_ASSERTION(aCx
, "Null pointer!");
60 NS_ASSERTION(IsValidKeyPathString(aKeyPathString
), "This will explode!");
61 NS_ASSERTION(!(aCallback
|| aClosure
) || aOptions
== CreateProperties
,
62 "This is not allowed!");
63 NS_ASSERTION(aOptions
!= CreateProperties
|| aCallback
,
64 "If properties are created, there must be a callback!");
69 KeyPathTokenizer
tokenizer(aKeyPathString
, '.');
71 nsString targetObjectPropName
;
72 JS::Rooted
<JSObject
*> targetObject(aCx
, nullptr);
73 JS::Rooted
<JS::Value
> currentVal(aCx
, aValue
);
74 JS::Rooted
<JSObject
*> obj(aCx
);
76 while (tokenizer
.hasMoreTokens()) {
77 const auto& token
= tokenizer
.nextToken();
79 NS_ASSERTION(!token
.IsEmpty(), "Should be a valid keypath");
81 const char16_t
* keyPathChars
= token
.BeginReading();
82 const size_t keyPathLen
= token
.Length();
85 // We're still walking the chain of existing objects
86 // http://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
87 // step 4 substep 1: check for .length on a String value.
88 if (currentVal
.isString() && !tokenizer
.hasMoreTokens() &&
89 token
.EqualsLiteral("length")) {
90 aKeyJSVal
->setNumber(double(JS_GetStringLength(currentVal
.toString())));
94 if (!currentVal
.isObject()) {
95 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR
;
97 obj
= ¤tVal
.toObject();
99 // We call JS_GetOwnUCPropertyDescriptor on purpose (as opposed to
100 // JS_GetUCPropertyDescriptor) to avoid searching the prototype chain.
101 JS::Rooted
<mozilla::Maybe
<JS::PropertyDescriptor
>> desc(aCx
);
102 QM_TRY(OkIf(JS_GetOwnUCPropertyDescriptor(aCx
, obj
, keyPathChars
,
104 NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
,
105 IDB_REPORT_INTERNAL_ERR_LAMBDA
);
107 JS::Rooted
<JS::Value
> intermediate(aCx
);
108 bool hasProp
= false;
110 if (desc
.isSome() && desc
->isDataDescriptor()) {
111 intermediate
= desc
->value();
114 // If we get here it means the object doesn't have the property or the
115 // property is available throuch a getter. We don't want to call any
116 // getters to avoid potential re-entrancy.
117 // The blob object is special since its properties are available
118 // only through getters but we still want to support them for key
119 // extraction. So they need to be handled manually.
121 if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob
, &obj
, blob
))) {
122 if (token
.EqualsLiteral("size")) {
124 uint64_t size
= blob
->GetSize(rv
);
125 MOZ_ALWAYS_TRUE(!rv
.Failed());
127 intermediate
= JS_NumberValue(size
);
129 } else if (token
.EqualsLiteral("type")) {
134 JS_NewUCStringCopyN(aCx
, type
.get(), type
.Length());
136 intermediate
= JS::StringValue(string
);
139 RefPtr
<File
> file
= blob
->ToFile();
141 if (token
.EqualsLiteral("name")) {
146 JS_NewUCStringCopyN(aCx
, name
.get(), name
.Length());
148 intermediate
= JS::StringValue(string
);
150 } else if (token
.EqualsLiteral("lastModified")) {
152 int64_t lastModifiedDate
= file
->GetLastModified(rv
);
153 MOZ_ALWAYS_TRUE(!rv
.Failed());
155 intermediate
= JS_NumberValue(lastModifiedDate
);
158 // The spec also lists "lastModifiedDate", but we deprecated and
159 // removed support for it.
166 // Treat explicitly undefined as an error.
167 if (intermediate
.isUndefined()) {
168 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR
;
170 if (tokenizer
.hasMoreTokens()) {
171 // ...and walk to it if there are more steps...
172 currentVal
= intermediate
;
174 // ...otherwise use it as key
175 *aKeyJSVal
= intermediate
;
178 // If the property doesn't exist, fall into below path of starting
179 // to define properties, if allowed.
180 if (aOptions
== DoNotCreateProperties
) {
181 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR
;
185 targetObjectPropName
= token
;
190 // We have started inserting new objects or are about to just insert
193 aKeyJSVal
->setUndefined();
195 if (tokenizer
.hasMoreTokens()) {
196 // If we're not at the end, we need to add a dummy object to the
198 JS::Rooted
<JSObject
*> dummy(aCx
, JS_NewPlainObject(aCx
));
200 IDB_REPORT_INTERNAL_ERR();
201 rv
= NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
205 if (!JS_DefineUCProperty(aCx
, obj
, token
.BeginReading(), token
.Length(),
206 dummy
, JSPROP_ENUMERATE
)) {
207 IDB_REPORT_INTERNAL_ERR();
208 rv
= NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
214 JS::Rooted
<JSObject
*> dummy(
215 aCx
, JS_NewObject(aCx
, IDBObjectStore::DummyPropClass()));
217 IDB_REPORT_INTERNAL_ERR();
218 rv
= NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
222 if (!JS_DefineUCProperty(aCx
, obj
, token
.BeginReading(), token
.Length(),
223 dummy
, JSPROP_ENUMERATE
)) {
224 IDB_REPORT_INTERNAL_ERR();
225 rv
= NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
234 // We guard on rv being a success because we need to run the property
235 // deletion code below even if we should not be running the callback.
236 if (NS_SUCCEEDED(rv
) && aCallback
) {
237 rv
= (*aCallback
)(aCx
, aClosure
);
241 // If this fails, we lose, and the web page sees a magical property
242 // appear on the object :-(
243 JS::ObjectOpResult succeeded
;
244 if (!JS_DeleteUCProperty(aCx
, targetObject
, targetObjectPropName
.get(),
245 targetObjectPropName
.Length(), succeeded
)) {
246 IDB_REPORT_INTERNAL_ERR();
247 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
249 QM_TRY(OkIf(succeeded
.ok()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
,
250 IDB_REPORT_INTERNAL_ERR_LAMBDA
);
253 // TODO: It would be nicer to do the cleanup using a RAII class or something.
254 // This last QM_TRY could be removed then.
255 QM_TRY(MOZ_TO_RESULT(rv
));
262 Result
<KeyPath
, nsresult
> KeyPath::Parse(const nsAString
& aString
) {
264 keyPath
.SetType(KeyPathType::String
);
266 if (!keyPath
.AppendStringWithValidation(aString
)) {
267 return Err(NS_ERROR_FAILURE
);
274 Result
<KeyPath
, nsresult
> KeyPath::Parse(const Sequence
<nsString
>& aStrings
) {
276 keyPath
.SetType(KeyPathType::Array
);
278 for (uint32_t i
= 0; i
< aStrings
.Length(); ++i
) {
279 if (!keyPath
.AppendStringWithValidation(aStrings
[i
])) {
280 return Err(NS_ERROR_FAILURE
);
288 Result
<KeyPath
, nsresult
> KeyPath::Parse(
289 const Nullable
<OwningStringOrStringSequence
>& aValue
) {
290 if (aValue
.IsNull()) {
294 if (aValue
.Value().IsString()) {
295 return Parse(aValue
.Value().GetAsString());
298 MOZ_ASSERT(aValue
.Value().IsStringSequence());
300 const Sequence
<nsString
>& seq
= aValue
.Value().GetAsStringSequence();
301 if (seq
.Length() == 0) {
302 return Err(NS_ERROR_FAILURE
);
307 void KeyPath::SetType(KeyPathType aType
) {
312 bool KeyPath::AppendStringWithValidation(const nsAString
& aString
) {
313 if (!IsValidKeyPathString(aString
)) {
318 NS_ASSERTION(mStrings
.Length() == 0, "Too many strings!");
319 mStrings
.AppendElement(aString
);
324 mStrings
.AppendElement(aString
);
328 MOZ_ASSERT_UNREACHABLE("What?!");
332 nsresult
KeyPath::ExtractKey(JSContext
* aCx
, const JS::Value
& aValue
,
334 uint32_t len
= mStrings
.Length();
335 JS::Rooted
<JS::Value
> value(aCx
);
339 for (uint32_t i
= 0; i
< len
; ++i
) {
341 GetJSValFromKeyPathString(aCx
, aValue
, mStrings
[i
], value
.address(),
342 DoNotCreateProperties
, nullptr, nullptr);
347 auto result
= aKey
.AppendItem(aCx
, IsArray() && i
== 0, value
);
348 if (result
.isErr()) {
349 NS_ASSERTION(aKey
.IsUnset(), "Encoding error should unset");
350 if (result
.inspectErr().Is(SpecialValues::Exception
)) {
351 result
.unwrapErr().AsException().SuppressException();
353 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR
;
362 nsresult
KeyPath::ExtractKeyAsJSVal(JSContext
* aCx
, const JS::Value
& aValue
,
363 JS::Value
* aOutVal
) const {
364 NS_ASSERTION(IsValid(), "This doesn't make sense!");
367 return GetJSValFromKeyPathString(aCx
, aValue
, mStrings
[0], aOutVal
,
368 DoNotCreateProperties
, nullptr, nullptr);
371 const uint32_t len
= mStrings
.Length();
372 JS::Rooted
<JSObject
*> arrayObj(aCx
, JS::NewArrayObject(aCx
, len
));
374 return NS_ERROR_OUT_OF_MEMORY
;
377 JS::Rooted
<JS::Value
> value(aCx
);
378 for (uint32_t i
= 0; i
< len
; ++i
) {
380 GetJSValFromKeyPathString(aCx
, aValue
, mStrings
[i
], value
.address(),
381 DoNotCreateProperties
, nullptr, nullptr);
386 if (!JS_DefineElement(aCx
, arrayObj
, i
, value
, JSPROP_ENUMERATE
)) {
387 IDB_REPORT_INTERNAL_ERR();
388 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
392 aOutVal
->setObject(*arrayObj
);
396 nsresult
KeyPath::ExtractOrCreateKey(JSContext
* aCx
, const JS::Value
& aValue
,
398 ExtractOrCreateKeyCallback aCallback
,
399 void* aClosure
) const {
400 NS_ASSERTION(IsString(), "This doesn't make sense!");
402 JS::Rooted
<JS::Value
> value(aCx
);
407 GetJSValFromKeyPathString(aCx
, aValue
, mStrings
[0], value
.address(),
408 CreateProperties
, aCallback
, aClosure
);
413 auto result
= aKey
.AppendItem(aCx
, false, value
);
414 if (result
.isErr()) {
415 NS_ASSERTION(aKey
.IsUnset(), "Should be unset");
416 if (result
.inspectErr().Is(SpecialValues::Exception
)) {
417 result
.unwrapErr().AsException().SuppressException();
419 return value
.isUndefined() ? NS_OK
: NS_ERROR_DOM_INDEXEDDB_DATA_ERR
;
427 nsAutoString
KeyPath::SerializeToString() const {
428 NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");
431 return nsAutoString
{mStrings
[0]};
437 // We use a comma in the beginning to indicate that it's an array of
438 // key paths. This is to be able to tell a string-keypath from an
439 // array-keypath which contains only one item.
440 // It also makes serializing easier :-)
441 const uint32_t len
= mStrings
.Length();
442 for (uint32_t i
= 0; i
< len
; ++i
) {
444 res
.Append(mStrings
[i
]);
450 MOZ_ASSERT_UNREACHABLE("What?");
455 KeyPath
KeyPath::DeserializeFromString(const nsAString
& aString
) {
458 if (!aString
.IsEmpty() && aString
.First() == ',') {
459 keyPath
.SetType(KeyPathType::Array
);
461 // We use a comma in the beginning to indicate that it's an array of
462 // key paths. This is to be able to tell a string-keypath from an
463 // array-keypath which contains only one item.
464 nsCharSeparatedTokenizerTemplate
<NS_TokenizerIgnoreNothing
> tokenizer(
466 tokenizer
.nextToken();
467 while (tokenizer
.hasMoreTokens()) {
468 keyPath
.mStrings
.AppendElement(tokenizer
.nextToken());
471 if (tokenizer
.separatorAfterCurrentToken()) {
472 // There is a trailing comma, indicating the original KeyPath has
473 // a trailing empty string, i.e. [..., '']. We should append this
475 keyPath
.mStrings
.EmplaceBack();
481 keyPath
.SetType(KeyPathType::String
);
482 keyPath
.mStrings
.AppendElement(aString
);
487 nsresult
KeyPath::ToJSVal(JSContext
* aCx
,
488 JS::MutableHandle
<JS::Value
> aValue
) const {
490 uint32_t len
= mStrings
.Length();
491 JS::Rooted
<JSObject
*> array(aCx
, JS::NewArrayObject(aCx
, len
));
493 IDB_WARNING("Failed to make array!");
494 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
497 for (uint32_t i
= 0; i
< len
; ++i
) {
498 JS::Rooted
<JS::Value
> val(aCx
);
499 nsString
tmp(mStrings
[i
]);
500 if (!xpc::StringToJsval(aCx
, tmp
, &val
)) {
501 IDB_REPORT_INTERNAL_ERR();
502 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
505 if (!JS_DefineElement(aCx
, array
, i
, val
, JSPROP_ENUMERATE
)) {
506 IDB_REPORT_INTERNAL_ERR();
507 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
511 aValue
.setObject(*array
);
516 nsString
tmp(mStrings
[0]);
517 if (!xpc::StringToJsval(aCx
, tmp
, aValue
)) {
518 IDB_REPORT_INTERNAL_ERR();
519 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR
;
528 nsresult
KeyPath::ToJSVal(JSContext
* aCx
, JS::Heap
<JS::Value
>& aValue
) const {
529 JS::Rooted
<JS::Value
> value(aCx
);
530 nsresult rv
= ToJSVal(aCx
, &value
);
531 if (NS_SUCCEEDED(rv
)) {
537 bool KeyPath::IsAllowedForObjectStore(bool aAutoIncrement
) const {
538 // Any keypath that passed validation is allowed for non-autoIncrement
540 if (!aAutoIncrement
) {
544 // Array keypaths are not allowed for autoIncrement objectStores.
549 // Neither are empty strings.
554 // Everything else is ok.
558 } // namespace mozilla::dom::indexedDB