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 = &currentVal.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