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 "LocalStorageManager.h"
8 #include "StorageUtils.h"
9 
10 #include "mozIStorageBindingParams.h"
11 #include "mozIStorageValueArray.h"
12 #include "mozIStorageFunction.h"
13 #include "mozilla/BasePrincipal.h"
14 #include "nsVariant.h"
15 #include "mozilla/Tokenizer.h"
16 #include "mozIStorageConnection.h"
17 #include "mozStorageHelper.h"
18 
19 // Current version of the database schema
20 #define CURRENT_SCHEMA_VERSION 2
21 
22 namespace mozilla {
23 namespace dom {
24 
25 using namespace StorageUtils;
26 
27 namespace {
28 
29 class nsReverseStringSQLFunction final : public mozIStorageFunction {
30   ~nsReverseStringSQLFunction() = default;
31 
32   NS_DECL_ISUPPORTS
33   NS_DECL_MOZISTORAGEFUNCTION
34 };
35 
NS_IMPL_ISUPPORTS(nsReverseStringSQLFunction,mozIStorageFunction)36 NS_IMPL_ISUPPORTS(nsReverseStringSQLFunction, mozIStorageFunction)
37 
38 NS_IMETHODIMP
39 nsReverseStringSQLFunction::OnFunctionCall(
40     mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
41   nsresult rv;
42 
43   nsAutoCString stringToReverse;
44   rv = aFunctionArguments->GetUTF8String(0, stringToReverse);
45   NS_ENSURE_SUCCESS(rv, rv);
46 
47   nsAutoCString result;
48   ReverseString(stringToReverse, result);
49 
50   RefPtr<nsVariant> outVar(new nsVariant());
51   rv = outVar->SetAsAUTF8String(result);
52   NS_ENSURE_SUCCESS(rv, rv);
53 
54   outVar.forget(aResult);
55   return NS_OK;
56 }
57 
58 // "scope" to "origin attributes suffix" and "origin key" convertor
59 
60 class ExtractOriginData : protected mozilla::Tokenizer {
61  public:
ExtractOriginData(const nsACString & scope,nsACString & suffix,nsACString & origin)62   ExtractOriginData(const nsACString& scope, nsACString& suffix,
63                     nsACString& origin)
64       : mozilla::Tokenizer(scope) {
65     using mozilla::OriginAttributes;
66 
67     // Parse optional appId:isInIsolatedMozBrowserElement: string, in case
68     // we don't find it, the scope is our new origin key and suffix
69     // is empty.
70     suffix.Truncate();
71     origin.Assign(scope);
72 
73     // Bail out if it isn't appId.
74     // AppId doesn't exist any more but we could have old storage data...
75     uint32_t appId;
76     if (!ReadInteger(&appId)) {
77       return;
78     }
79 
80     // Should be followed by a colon.
81     if (!CheckChar(':')) {
82       return;
83     }
84 
85     // Bail out if it isn't 'isolatedBrowserFlag'.
86     nsDependentCSubstring isolatedBrowserFlag;
87     if (!ReadWord(isolatedBrowserFlag)) {
88       return;
89     }
90 
91     bool inIsolatedMozBrowser = isolatedBrowserFlag == "t";
92     bool notInIsolatedBrowser = isolatedBrowserFlag == "f";
93     if (!inIsolatedMozBrowser && !notInIsolatedBrowser) {
94       return;
95     }
96 
97     // Should be followed by a colon.
98     if (!CheckChar(':')) {
99       return;
100     }
101 
102     // OK, we have found appId and inIsolatedMozBrowser flag, create the suffix
103     // from it and take the rest as the origin key.
104 
105     // If the profile went through schema 1 -> schema 0 -> schema 1 switching
106     // we may have stored the full attributes origin suffix when there were
107     // more than just appId and inIsolatedMozBrowser set on storage principal's
108     // OriginAttributes.
109     //
110     // To preserve full uniqueness we store this suffix to the scope key.
111     // Schema 0 code will just ignore it while keeping the scoping unique.
112     //
113     // The whole scope string is in one of the following forms (when we are
114     // here):
115     //
116     // "1001:f:^appId=1001&inBrowser=false&addonId=101:gro.allizom.rxd.:https:443"
117     // "1001:f:gro.allizom.rxd.:https:443"
118     //         |
119     //         +- the parser cursor position.
120     //
121     // If there is '^', the full origin attributes suffix follows.  We search
122     // for ':' since it is the delimiter used in the scope string and is never
123     // contained in the origin attributes suffix.  Remaining string after
124     // the comma is the reversed-domain+schema+port tuple.
125     Record();
126     if (CheckChar('^')) {
127       Token t;
128       while (Next(t)) {
129         if (t.Equals(Token::Char(':'))) {
130           Claim(suffix);
131           break;
132         }
133       }
134     } else {
135       OriginAttributes attrs(inIsolatedMozBrowser);
136       attrs.CreateSuffix(suffix);
137     }
138 
139     // Consume the rest of the input as "origin".
140     origin.Assign(Substring(mCursor, mEnd));
141   }
142 };
143 
144 class GetOriginParticular final : public mozIStorageFunction {
145  public:
146   enum EParticular { ORIGIN_ATTRIBUTES_SUFFIX, ORIGIN_KEY };
147 
GetOriginParticular(EParticular aParticular)148   explicit GetOriginParticular(EParticular aParticular)
149       : mParticular(aParticular) {}
150 
151  private:
152   GetOriginParticular() = delete;
153   ~GetOriginParticular() = default;
154 
155   EParticular mParticular;
156 
157   NS_DECL_ISUPPORTS
158   NS_DECL_MOZISTORAGEFUNCTION
159 };
160 
NS_IMPL_ISUPPORTS(GetOriginParticular,mozIStorageFunction)161 NS_IMPL_ISUPPORTS(GetOriginParticular, mozIStorageFunction)
162 
163 NS_IMETHODIMP
164 GetOriginParticular::OnFunctionCall(mozIStorageValueArray* aFunctionArguments,
165                                     nsIVariant** aResult) {
166   nsresult rv;
167 
168   nsAutoCString scope;
169   rv = aFunctionArguments->GetUTF8String(0, scope);
170   NS_ENSURE_SUCCESS(rv, rv);
171 
172   nsAutoCString suffix, origin;
173   ExtractOriginData extractor(scope, suffix, origin);
174 
175   nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
176 
177   switch (mParticular) {
178     case EParticular::ORIGIN_ATTRIBUTES_SUFFIX:
179       rv = outVar->SetAsAUTF8String(suffix);
180       break;
181     case EParticular::ORIGIN_KEY:
182       rv = outVar->SetAsAUTF8String(origin);
183       break;
184   }
185 
186   NS_ENSURE_SUCCESS(rv, rv);
187 
188   outVar.forget(aResult);
189   return NS_OK;
190 }
191 
192 class StripOriginAddonId final : public mozIStorageFunction {
193  public:
194   explicit StripOriginAddonId() = default;
195 
196  private:
197   ~StripOriginAddonId() = default;
198 
199   NS_DECL_ISUPPORTS
200   NS_DECL_MOZISTORAGEFUNCTION
201 };
202 
NS_IMPL_ISUPPORTS(StripOriginAddonId,mozIStorageFunction)203 NS_IMPL_ISUPPORTS(StripOriginAddonId, mozIStorageFunction)
204 
205 NS_IMETHODIMP
206 StripOriginAddonId::OnFunctionCall(mozIStorageValueArray* aFunctionArguments,
207                                    nsIVariant** aResult) {
208   nsresult rv;
209 
210   nsAutoCString suffix;
211   rv = aFunctionArguments->GetUTF8String(0, suffix);
212   NS_ENSURE_SUCCESS(rv, rv);
213 
214   // Deserialize and re-serialize to automatically drop any obsolete origin
215   // attributes.
216   OriginAttributes oa;
217   bool ok = oa.PopulateFromSuffix(suffix);
218   NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE);
219 
220   nsAutoCString newSuffix;
221   oa.CreateSuffix(newSuffix);
222 
223   nsCOMPtr<nsIWritableVariant> outVar = new nsVariant();
224   rv = outVar->SetAsAUTF8String(newSuffix);
225   NS_ENSURE_SUCCESS(rv, rv);
226 
227   outVar.forget(aResult);
228   return NS_OK;
229 }
230 
CreateSchema1Tables(mozIStorageConnection * aWorkerConnection)231 nsresult CreateSchema1Tables(mozIStorageConnection* aWorkerConnection) {
232   nsresult rv;
233 
234   rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
235       "CREATE TABLE IF NOT EXISTS webappsstore2 ("
236       "originAttributes TEXT, "
237       "originKey TEXT, "
238       "scope TEXT, "  // Only for schema0 downgrade compatibility
239       "key TEXT, "
240       "value TEXT)"));
241   NS_ENSURE_SUCCESS(rv, rv);
242 
243   rv = aWorkerConnection->ExecuteSimpleSQL(
244       nsLiteralCString("CREATE UNIQUE INDEX IF NOT EXISTS origin_key_index"
245                        " ON webappsstore2(originAttributes, originKey, key)"));
246   NS_ENSURE_SUCCESS(rv, rv);
247 
248   return NS_OK;
249 }
250 
TablesExist(mozIStorageConnection * aWorkerConnection,bool * aWebappsstore2Exists,bool * aWebappsstoreExists,bool * aMoz_webappsstoreExists)251 nsresult TablesExist(mozIStorageConnection* aWorkerConnection,
252                      bool* aWebappsstore2Exists, bool* aWebappsstoreExists,
253                      bool* aMoz_webappsstoreExists) {
254   nsresult rv =
255       aWorkerConnection->TableExists("webappsstore2"_ns, aWebappsstore2Exists);
256   NS_ENSURE_SUCCESS(rv, rv);
257   rv = aWorkerConnection->TableExists("webappsstore"_ns, aWebappsstoreExists);
258   NS_ENSURE_SUCCESS(rv, rv);
259   rv = aWorkerConnection->TableExists("moz_webappsstore"_ns,
260                                       aMoz_webappsstoreExists);
261   NS_ENSURE_SUCCESS(rv, rv);
262 
263   return NS_OK;
264 }
265 
CreateCurrentSchemaOnEmptyTableInternal(mozIStorageConnection * aWorkerConnection)266 nsresult CreateCurrentSchemaOnEmptyTableInternal(
267     mozIStorageConnection* aWorkerConnection) {
268   nsresult rv = CreateSchema1Tables(aWorkerConnection);
269   NS_ENSURE_SUCCESS(rv, rv);
270 
271   rv = aWorkerConnection->SetSchemaVersion(CURRENT_SCHEMA_VERSION);
272   NS_ENSURE_SUCCESS(rv, rv);
273 
274   return NS_OK;
275 }
276 
277 }  // namespace
278 
279 namespace StorageDBUpdater {
280 
CreateCurrentSchema(mozIStorageConnection * aConnection)281 nsresult CreateCurrentSchema(mozIStorageConnection* aConnection) {
282   mozStorageTransaction transaction(aConnection, false);
283 
284   nsresult rv = transaction.Start();
285   NS_ENSURE_SUCCESS(rv, rv);
286 
287 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
288   {
289     int32_t schemaVer;
290     nsresult rv = aConnection->GetSchemaVersion(&schemaVer);
291     NS_ENSURE_SUCCESS(rv, rv);
292 
293     MOZ_DIAGNOSTIC_ASSERT(0 == schemaVer);
294 
295     bool webappsstore2Exists, webappsstoreExists, moz_webappsstoreExists;
296     rv = TablesExist(aConnection, &webappsstore2Exists, &webappsstoreExists,
297                      &moz_webappsstoreExists);
298     NS_ENSURE_SUCCESS(rv, rv);
299 
300     MOZ_DIAGNOSTIC_ASSERT(!webappsstore2Exists && !webappsstoreExists &&
301                           !moz_webappsstoreExists);
302   }
303 #endif
304 
305   rv = CreateCurrentSchemaOnEmptyTableInternal(aConnection);
306   NS_ENSURE_SUCCESS(rv, rv);
307 
308   rv = transaction.Commit();
309   NS_ENSURE_SUCCESS(rv, rv);
310 
311   return NS_OK;
312 }
313 
Update(mozIStorageConnection * aWorkerConnection)314 nsresult Update(mozIStorageConnection* aWorkerConnection) {
315   mozStorageTransaction transaction(aWorkerConnection, false);
316 
317   nsresult rv = transaction.Start();
318   NS_ENSURE_SUCCESS(rv, rv);
319 
320   bool doVacuum = false;
321 
322   int32_t schemaVer;
323   rv = aWorkerConnection->GetSchemaVersion(&schemaVer);
324   NS_ENSURE_SUCCESS(rv, rv);
325 
326   // downgrade (v0) -> upgrade (v1+) specific code
327   if (schemaVer >= 1) {
328     bool schema0IndexExists;
329     rv = aWorkerConnection->IndexExists("scope_key_index"_ns,
330                                         &schema0IndexExists);
331     NS_ENSURE_SUCCESS(rv, rv);
332 
333     if (schema0IndexExists) {
334       // If this index exists, the database (already updated to schema >1)
335       // has been run again on schema 0 code.  That recreated that index
336       // and might store some new rows while updating only the 'scope' column.
337       // For such added rows we must fill the new 'origin*' columns correctly
338       // otherwise there would be a data loss.  The safest way to do it is to
339       // simply run the whole update to schema 1 again.
340       schemaVer = 0;
341     }
342   }
343 
344   switch (schemaVer) {
345     case 0: {
346       bool webappsstore2Exists, webappsstoreExists, moz_webappsstoreExists;
347       rv = TablesExist(aWorkerConnection, &webappsstore2Exists,
348                        &webappsstoreExists, &moz_webappsstoreExists);
349       NS_ENSURE_SUCCESS(rv, rv);
350 
351       if (!webappsstore2Exists && !webappsstoreExists &&
352           !moz_webappsstoreExists) {
353         // The database is empty, this is the first start.  Just create the
354         // schema table and break to the next version to update to, i.e. bypass
355         // update from the old version.
356 
357         // XXX What does "break to the next version to update to" mean here? It
358         // seems to refer to the 'break' statement below, but that breaks out of
359         // the 'switch' statement and continues with committing the transaction.
360         // Either this is wrong, or the comment above is misleading.
361 
362         rv = CreateCurrentSchemaOnEmptyTableInternal(aWorkerConnection);
363         NS_ENSURE_SUCCESS(rv, rv);
364 
365         break;
366       }
367 
368       doVacuum = true;
369 
370       // Ensure Gecko 1.9.1 storage table
371       rv = aWorkerConnection->ExecuteSimpleSQL(
372           nsLiteralCString("CREATE TABLE IF NOT EXISTS webappsstore2 ("
373                            "scope TEXT, "
374                            "key TEXT, "
375                            "value TEXT, "
376                            "secure INTEGER, "
377                            "owner TEXT)"));
378       NS_ENSURE_SUCCESS(rv, rv);
379 
380       rv = aWorkerConnection->ExecuteSimpleSQL(
381           nsLiteralCString("CREATE UNIQUE INDEX IF NOT EXISTS scope_key_index"
382                            " ON webappsstore2(scope, key)"));
383       NS_ENSURE_SUCCESS(rv, rv);
384 
385       nsCOMPtr<mozIStorageFunction> function1(new nsReverseStringSQLFunction());
386       NS_ENSURE_TRUE(function1, NS_ERROR_OUT_OF_MEMORY);
387 
388       rv = aWorkerConnection->CreateFunction("REVERSESTRING"_ns, 1, function1);
389       NS_ENSURE_SUCCESS(rv, rv);
390 
391       // Check if there is storage of Gecko 1.9.0 and if so, upgrade that
392       // storage to actual webappsstore2 table and drop the obsolete table.
393       // First process this newer table upgrade to priority potential duplicates
394       // from older storage table.
395       if (webappsstoreExists) {
396         rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
397             "INSERT OR IGNORE INTO "
398             "webappsstore2(scope, key, value, secure, owner) "
399             "SELECT REVERSESTRING(domain) || '.:', key, value, secure, owner "
400             "FROM webappsstore"));
401         NS_ENSURE_SUCCESS(rv, rv);
402 
403         rv = aWorkerConnection->ExecuteSimpleSQL("DROP TABLE webappsstore"_ns);
404         NS_ENSURE_SUCCESS(rv, rv);
405       }
406 
407       // Check if there is storage of Gecko 1.8 and if so, upgrade that storage
408       // to actual webappsstore2 table and drop the obsolete table. Potential
409       // duplicates will be ignored.
410       if (moz_webappsstoreExists) {
411         rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
412             "INSERT OR IGNORE INTO "
413             "webappsstore2(scope, key, value, secure, owner) "
414             "SELECT REVERSESTRING(domain) || '.:', key, value, secure, domain "
415             "FROM moz_webappsstore"));
416         NS_ENSURE_SUCCESS(rv, rv);
417 
418         rv = aWorkerConnection->ExecuteSimpleSQL(
419             "DROP TABLE moz_webappsstore"_ns);
420         NS_ENSURE_SUCCESS(rv, rv);
421       }
422 
423       aWorkerConnection->RemoveFunction("REVERSESTRING"_ns);
424 
425       // Update the scoping to match the new implememntation: split to oa suffix
426       // and origin key First rename the old table, we want to remove some
427       // columns no longer needed, but even before that drop all indexes from it
428       // (CREATE IF NOT EXISTS for index on the new table would falsely find the
429       // index!)
430       rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
431           "DROP INDEX IF EXISTS webappsstore2.origin_key_index"));
432       NS_ENSURE_SUCCESS(rv, rv);
433 
434       rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
435           "DROP INDEX IF EXISTS webappsstore2.scope_key_index"));
436       NS_ENSURE_SUCCESS(rv, rv);
437 
438       rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
439           "ALTER TABLE webappsstore2 RENAME TO webappsstore2_old"));
440       NS_ENSURE_SUCCESS(rv, rv);
441 
442       nsCOMPtr<mozIStorageFunction> oaSuffixFunc(new GetOriginParticular(
443           GetOriginParticular::ORIGIN_ATTRIBUTES_SUFFIX));
444       rv = aWorkerConnection->CreateFunction("GET_ORIGIN_SUFFIX"_ns, 1,
445                                              oaSuffixFunc);
446       NS_ENSURE_SUCCESS(rv, rv);
447 
448       nsCOMPtr<mozIStorageFunction> originKeyFunc(
449           new GetOriginParticular(GetOriginParticular::ORIGIN_KEY));
450       rv = aWorkerConnection->CreateFunction("GET_ORIGIN_KEY"_ns, 1,
451                                              originKeyFunc);
452       NS_ENSURE_SUCCESS(rv, rv);
453 
454       // Here we ensure this schema tables when we are updating.
455       rv = CreateSchema1Tables(aWorkerConnection);
456       NS_ENSURE_SUCCESS(rv, rv);
457 
458       rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
459           "INSERT OR IGNORE INTO "
460           "webappsstore2 (originAttributes, originKey, scope, key, value) "
461           "SELECT GET_ORIGIN_SUFFIX(scope), GET_ORIGIN_KEY(scope), scope, key, "
462           "value "
463           "FROM webappsstore2_old"));
464       NS_ENSURE_SUCCESS(rv, rv);
465 
466       rv = aWorkerConnection->ExecuteSimpleSQL(
467           "DROP TABLE webappsstore2_old"_ns);
468       NS_ENSURE_SUCCESS(rv, rv);
469 
470       aWorkerConnection->RemoveFunction("GET_ORIGIN_SUFFIX"_ns);
471       aWorkerConnection->RemoveFunction("GET_ORIGIN_KEY"_ns);
472 
473       rv = aWorkerConnection->SetSchemaVersion(1);
474       NS_ENSURE_SUCCESS(rv, rv);
475 
476       [[fallthrough]];
477     }
478     case 1: {
479       nsCOMPtr<mozIStorageFunction> oaStripAddonId(new StripOriginAddonId());
480       rv = aWorkerConnection->CreateFunction("STRIP_ADDON_ID"_ns, 1,
481                                              oaStripAddonId);
482       NS_ENSURE_SUCCESS(rv, rv);
483 
484       rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
485           "UPDATE webappsstore2 "
486           "SET originAttributes = STRIP_ADDON_ID(originAttributes) "
487           "WHERE originAttributes LIKE '^%'"));
488       NS_ENSURE_SUCCESS(rv, rv);
489 
490       aWorkerConnection->RemoveFunction("STRIP_ADDON_ID"_ns);
491 
492       rv = aWorkerConnection->SetSchemaVersion(2);
493       NS_ENSURE_SUCCESS(rv, rv);
494 
495       [[fallthrough]];
496     }
497     case CURRENT_SCHEMA_VERSION:
498       // Ensure the tables and indexes are up.  This is mostly a no-op
499       // in common scenarios.
500       rv = CreateSchema1Tables(aWorkerConnection);
501       NS_ENSURE_SUCCESS(rv, rv);
502 
503       // Nothing more to do here, this is the current schema version
504       break;
505 
506     default:
507       MOZ_ASSERT(false);
508       break;
509   }  // switch
510 
511   rv = transaction.Commit();
512   NS_ENSURE_SUCCESS(rv, rv);
513 
514   if (doVacuum) {
515     // In some cases this can make the disk file of the database significantly
516     // smaller.  VACUUM cannot be executed inside a transaction.
517     rv = aWorkerConnection->ExecuteSimpleSQL("VACUUM"_ns);
518     NS_ENSURE_SUCCESS(rv, rv);
519   }
520 
521   return NS_OK;
522 }
523 
524 }  // namespace StorageDBUpdater
525 }  // namespace dom
526 }  // namespace mozilla
527