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