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