Source code

Revision control

Other Tools

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/. */
6
7
#include "KeyPath.h"
8
#include "IDBObjectStore.h"
9
#include "Key.h"
10
#include "ReportInternalError.h"
11
12
#include "nsCharSeparatedTokenizer.h"
13
#include "nsJSUtils.h"
14
#include "nsPrintfCString.h"
15
#include "xpcpublic.h"
16
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
23
namespace mozilla {
24
namespace dom {
25
namespace indexedDB {
26
27
namespace {
28
29
inline bool IgnoreWhitespace(char16_t c) { return false; }
30
31
typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;
32
33
bool IsValidKeyPathString(const nsAString& aKeyPath) {
34
NS_ASSERTION(!aKeyPath.IsVoid(), "What?");
35
36
KeyPathTokenizer tokenizer(aKeyPath, '.');
37
38
while (tokenizer.hasMoreTokens()) {
39
const auto& token = tokenizer.nextToken();
40
41
if (!token.Length()) {
42
return false;
43
}
44
45
if (!JS_IsIdentifier(token.Data(), token.Length())) {
46
return false;
47
}
48
}
49
50
// If the very last character was a '.', the tokenizer won't give us an empty
51
// token, but the keyPath is still invalid.
52
return aKeyPath.IsEmpty() || aKeyPath.CharAt(aKeyPath.Length() - 1) != '.';
53
}
54
55
enum KeyExtractionOptions { DoNotCreateProperties, CreateProperties };
56
57
nsresult GetJSValFromKeyPathString(
58
JSContext* aCx, const JS::Value& aValue, const nsAString& aKeyPathString,
59
JS::Value* aKeyJSVal, KeyExtractionOptions aOptions,
60
KeyPath::ExtractOrCreateKeyCallback aCallback, void* aClosure) {
61
NS_ASSERTION(aCx, "Null pointer!");
62
NS_ASSERTION(IsValidKeyPathString(aKeyPathString), "This will explode!");
63
NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties,
64
"This is not allowed!");
65
NS_ASSERTION(aOptions != CreateProperties || aCallback,
66
"If properties are created, there must be a callback!");
67
68
nsresult rv = NS_OK;
69
*aKeyJSVal = aValue;
70
71
KeyPathTokenizer tokenizer(aKeyPathString, '.');
72
73
nsString targetObjectPropName;
74
JS::Rooted<JSObject*> targetObject(aCx, nullptr);
75
JS::Rooted<JS::Value> currentVal(aCx, aValue);
76
JS::Rooted<JSObject*> obj(aCx);
77
78
while (tokenizer.hasMoreTokens()) {
79
const auto& token = tokenizer.nextToken();
80
81
NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath");
82
83
const char16_t* keyPathChars = token.BeginReading();
84
const size_t keyPathLen = token.Length();
85
86
if (!targetObject) {
87
// We're still walking the chain of existing objects
89
// step 4 substep 1: check for .length on a String value.
90
if (currentVal.isString() && !tokenizer.hasMoreTokens() &&
91
token.EqualsLiteral("length")) {
92
aKeyJSVal->setNumber(double(JS_GetStringLength(currentVal.toString())));
93
break;
94
}
95
96
if (!currentVal.isObject()) {
97
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
98
}
99
obj = &currentVal.toObject();
100
101
// We call JS_GetOwnUCPropertyDescriptor on purpose (as opposed to
102
// JS_GetUCPropertyDescriptor) to avoid searching the prototype chain.
103
JS::Rooted<JS::PropertyDescriptor> desc(aCx);
104
bool ok = JS_GetOwnUCPropertyDescriptor(aCx, obj, keyPathChars,
105
keyPathLen, &desc);
106
IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
107
108
JS::Rooted<JS::Value> intermediate(aCx);
109
bool hasProp = false;
110
111
if (desc.object()) {
112
intermediate = desc.value();
113
hasProp = true;
114
} else {
115
// If we get here it means the object doesn't have the property or the
116
// property is available throuch a getter. We don't want to call any
117
// getters to avoid potential re-entrancy.
118
// The blob object is special since its properties are available
119
// only through getters but we still want to support them for key
120
// extraction. So they need to be handled manually.
121
Blob* blob;
122
if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
123
if (token.EqualsLiteral("size")) {
124
ErrorResult rv;
125
uint64_t size = blob->GetSize(rv);
126
MOZ_ALWAYS_TRUE(!rv.Failed());
127
128
intermediate = JS_NumberValue(size);
129
hasProp = true;
130
} else if (token.EqualsLiteral("type")) {
131
nsString type;
132
blob->GetType(type);
133
134
JSString* string =
135
JS_NewUCStringCopyN(aCx, type.get(), type.Length());
136
137
intermediate = JS::StringValue(string);
138
hasProp = true;
139
} else {
140
RefPtr<File> file = blob->ToFile();
141
if (file) {
142
if (token.EqualsLiteral("name")) {
143
nsString name;
144
file->GetName(name);
145
146
JSString* string =
147
JS_NewUCStringCopyN(aCx, name.get(), name.Length());
148
149
intermediate = JS::StringValue(string);
150
hasProp = true;
151
} else if (token.EqualsLiteral("lastModified")) {
152
ErrorResult rv;
153
int64_t lastModifiedDate = file->GetLastModified(rv);
154
MOZ_ALWAYS_TRUE(!rv.Failed());
155
156
intermediate = JS_NumberValue(lastModifiedDate);
157
hasProp = true;
158
}
159
// The spec also lists "lastModifiedDate", but we deprecated and
160
// removed support for it.
161
}
162
}
163
}
164
}
165
166
if (hasProp) {
167
// Treat explicitly undefined as an error.
168
if (intermediate.isUndefined()) {
169
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
170
}
171
if (tokenizer.hasMoreTokens()) {
172
// ...and walk to it if there are more steps...
173
currentVal = intermediate;
174
} else {
175
// ...otherwise use it as key
176
*aKeyJSVal = intermediate;
177
}
178
} else {
179
// If the property doesn't exist, fall into below path of starting
180
// to define properties, if allowed.
181
if (aOptions == DoNotCreateProperties) {
182
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
183
}
184
185
targetObject = obj;
186
targetObjectPropName = token;
187
}
188
}
189
190
if (targetObject) {
191
// We have started inserting new objects or are about to just insert
192
// the first one.
193
194
aKeyJSVal->setUndefined();
195
196
if (tokenizer.hasMoreTokens()) {
197
// If we're not at the end, we need to add a dummy object to the
198
// chain.
199
JS::Rooted<JSObject*> dummy(aCx, JS_NewPlainObject(aCx));
200
if (!dummy) {
201
IDB_REPORT_INTERNAL_ERR();
202
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
203
break;
204
}
205
206
if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(),
207
dummy, JSPROP_ENUMERATE)) {
208
IDB_REPORT_INTERNAL_ERR();
209
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
210
break;
211
}
212
213
obj = dummy;
214
} else {
215
JS::Rooted<JSObject*> dummy(
216
aCx, JS_NewObject(aCx, IDBObjectStore::DummyPropClass()));
217
if (!dummy) {
218
IDB_REPORT_INTERNAL_ERR();
219
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
220
break;
221
}
222
223
if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(),
224
dummy, JSPROP_ENUMERATE)) {
225
IDB_REPORT_INTERNAL_ERR();
226
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
227
break;
228
}
229
230
obj = dummy;
231
}
232
}
233
}
234
235
// We guard on rv being a success because we need to run the property
236
// deletion code below even if we should not be running the callback.
237
if (NS_SUCCEEDED(rv) && aCallback) {
238
rv = (*aCallback)(aCx, aClosure);
239
}
240
241
if (targetObject) {
242
// If this fails, we lose, and the web page sees a magical property
243
// appear on the object :-(
244
JS::ObjectOpResult succeeded;
245
if (!JS_DeleteUCProperty(aCx, targetObject, targetObjectPropName.get(),
246
targetObjectPropName.Length(), succeeded)) {
247
IDB_REPORT_INTERNAL_ERR();
248
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
249
}
250
IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
251
}
252
253
NS_ENSURE_SUCCESS(rv, rv);
254
return rv;
255
}
256
257
} // namespace
258
259
// static
260
nsresult KeyPath::Parse(const nsAString& aString, KeyPath* aKeyPath) {
261
KeyPath keyPath(0);
262
keyPath.SetType(STRING);
263
264
if (!keyPath.AppendStringWithValidation(aString)) {
265
return NS_ERROR_FAILURE;
266
}
267
268
*aKeyPath = keyPath;
269
return NS_OK;
270
}
271
272
// static
273
nsresult KeyPath::Parse(const Sequence<nsString>& aStrings, KeyPath* aKeyPath) {
274
KeyPath keyPath(0);
275
keyPath.SetType(ARRAY);
276
277
for (uint32_t i = 0; i < aStrings.Length(); ++i) {
278
if (!keyPath.AppendStringWithValidation(aStrings[i])) {
279
return NS_ERROR_FAILURE;
280
}
281
}
282
283
*aKeyPath = keyPath;
284
return NS_OK;
285
}
286
287
// static
288
nsresult KeyPath::Parse(const Nullable<OwningStringOrStringSequence>& aValue,
289
KeyPath* aKeyPath) {
290
KeyPath keyPath(0);
291
292
aKeyPath->SetType(NONEXISTENT);
293
294
if (aValue.IsNull()) {
295
*aKeyPath = keyPath;
296
return NS_OK;
297
}
298
299
if (aValue.Value().IsString()) {
300
return Parse(aValue.Value().GetAsString(), aKeyPath);
301
}
302
303
MOZ_ASSERT(aValue.Value().IsStringSequence());
304
305
const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence();
306
if (seq.Length() == 0) {
307
return NS_ERROR_FAILURE;
308
}
309
return Parse(seq, aKeyPath);
310
}
311
312
void KeyPath::SetType(KeyPathType aType) {
313
mType = aType;
314
mStrings.Clear();
315
}
316
317
bool KeyPath::AppendStringWithValidation(const nsAString& aString) {
318
if (!IsValidKeyPathString(aString)) {
319
return false;
320
}
321
322
if (IsString()) {
323
NS_ASSERTION(mStrings.Length() == 0, "Too many strings!");
324
mStrings.AppendElement(aString);
325
return true;
326
}
327
328
if (IsArray()) {
329
mStrings.AppendElement(aString);
330
return true;
331
}
332
333
MOZ_ASSERT_UNREACHABLE("What?!");
334
return false;
335
}
336
337
nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue,
338
Key& aKey) const {
339
uint32_t len = mStrings.Length();
340
JS::Rooted<JS::Value> value(aCx);
341
342
aKey.Unset();
343
344
for (uint32_t i = 0; i < len; ++i) {
345
nsresult rv =
346
GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
347
DoNotCreateProperties, nullptr, nullptr);
348
if (NS_FAILED(rv)) {
349
return rv;
350
}
351
352
ErrorResult errorResult;
353
auto result = aKey.AppendItem(aCx, IsArray() && i == 0, value, errorResult);
354
if (!result.Is(Ok, errorResult)) {
355
NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
356
errorResult.SuppressException();
357
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
358
}
359
}
360
361
aKey.FinishArray();
362
363
return NS_OK;
364
}
365
366
nsresult KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue,
367
JS::Value* aOutVal) const {
368
NS_ASSERTION(IsValid(), "This doesn't make sense!");
369
370
if (IsString()) {
371
return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal,
372
DoNotCreateProperties, nullptr, nullptr);
373
}
374
375
const uint32_t len = mStrings.Length();
376
JS::Rooted<JSObject*> arrayObj(aCx, JS_NewArrayObject(aCx, len));
377
if (!arrayObj) {
378
return NS_ERROR_OUT_OF_MEMORY;
379
}
380
381
JS::Rooted<JS::Value> value(aCx);
382
for (uint32_t i = 0; i < len; ++i) {
383
nsresult rv =
384
GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
385
DoNotCreateProperties, nullptr, nullptr);
386
if (NS_FAILED(rv)) {
387
return rv;
388
}
389
390
if (!JS_DefineElement(aCx, arrayObj, i, value, JSPROP_ENUMERATE)) {
391
IDB_REPORT_INTERNAL_ERR();
392
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
393
}
394
}
395
396
aOutVal->setObject(*arrayObj);
397
return NS_OK;
398
}
399
400
nsresult KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue,
401
Key& aKey,
402
ExtractOrCreateKeyCallback aCallback,
403
void* aClosure) const {
404
NS_ASSERTION(IsString(), "This doesn't make sense!");
405
406
JS::Rooted<JS::Value> value(aCx);
407
408
aKey.Unset();
409
410
nsresult rv =
411
GetJSValFromKeyPathString(aCx, aValue, mStrings[0], value.address(),
412
CreateProperties, aCallback, aClosure);
413
if (NS_FAILED(rv)) {
414
return rv;
415
}
416
417
ErrorResult errorResult;
418
auto result = aKey.AppendItem(aCx, false, value, errorResult);
419
if (!result.Is(Ok, errorResult)) {
420
NS_ASSERTION(aKey.IsUnset(), "Should be unset");
421
errorResult.SuppressException();
422
return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
423
}
424
425
aKey.FinishArray();
426
427
return NS_OK;
428
}
429
430
void KeyPath::SerializeToString(nsAString& aString) const {
431
NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");
432
433
if (IsString()) {
434
aString = mStrings[0];
435
return;
436
}
437
438
if (IsArray()) {
439
// We use a comma in the beginning to indicate that it's an array of
440
// key paths. This is to be able to tell a string-keypath from an
441
// array-keypath which contains only one item.
442
// It also makes serializing easier :-)
443
uint32_t len = mStrings.Length();
444
for (uint32_t i = 0; i < len; ++i) {
445
aString.Append(',');
446
aString.Append(mStrings[i]);
447
}
448
449
return;
450
}
451
452
MOZ_ASSERT_UNREACHABLE("What?");
453
}
454
455
// static
456
KeyPath KeyPath::DeserializeFromString(const nsAString& aString) {
457
KeyPath keyPath(0);
458
459
if (!aString.IsEmpty() && aString.First() == ',') {
460
keyPath.SetType(ARRAY);
461
462
// We use a comma in the beginning to indicate that it's an array of
463
// key paths. This is to be able to tell a string-keypath from an
464
// array-keypath which contains only one item.
465
nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> tokenizer(aString, ',');
466
tokenizer.nextToken();
467
while (tokenizer.hasMoreTokens()) {
468
keyPath.mStrings.AppendElement(tokenizer.nextToken());
469
}
470
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
474
// empty string.
475
keyPath.mStrings.EmplaceBack();
476
}
477
478
return keyPath;
479
}
480
481
keyPath.SetType(STRING);
482
keyPath.mStrings.AppendElement(aString);
483
484
return keyPath;
485
}
486
487
nsresult KeyPath::ToJSVal(JSContext* aCx,
488
JS::MutableHandle<JS::Value> aValue) const {
489
if (IsArray()) {
490
uint32_t len = mStrings.Length();
491
JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, len));
492
if (!array) {
493
IDB_WARNING("Failed to make array!");
494
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
495
}
496
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;
503
}
504
505
if (!JS_DefineElement(aCx, array, i, val, JSPROP_ENUMERATE)) {
506
IDB_REPORT_INTERNAL_ERR();
507
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
508
}
509
}
510
511
aValue.setObject(*array);
512
return NS_OK;
513
}
514
515
if (IsString()) {
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;
520
}
521
return NS_OK;
522
}
523
524
aValue.setNull();
525
return NS_OK;
526
}
527
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)) {
532
aValue = value;
533
}
534
return rv;
535
}
536
537
bool KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const {
538
// Any keypath that passed validation is allowed for non-autoIncrement
539
// objectStores.
540
if (!aAutoIncrement) {
541
return true;
542
}
543
544
// Array keypaths are not allowed for autoIncrement objectStores.
545
if (IsArray()) {
546
return false;
547
}
548
549
// Neither are empty strings.
550
if (IsEmpty()) {
551
return false;
552
}
553
554
// Everything else is ok.
555
return true;
556
}
557
558
} // namespace indexedDB
559
} // namespace dom
560
} // namespace mozilla