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