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