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