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