OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 // | 4 // |
5 // This class isn't pretty. It's just a step better than globals, which is what | 5 // This class isn't pretty. It's just a step better than globals, which is what |
6 // these were previously. | 6 // these were previously. |
7 | 7 |
8 #include "chrome/browser/sync/util/user_settings.h" | 8 #include "chrome/browser/sync/util/user_settings.h" |
9 | 9 |
10 #include "build/build_config.h" | 10 #include "build/build_config.h" |
(...skipping 17 matching lines...) Expand all Loading... |
28 | 28 |
29 using std::numeric_limits; | 29 using std::numeric_limits; |
30 using std::string; | 30 using std::string; |
31 using std::vector; | 31 using std::vector; |
32 | 32 |
33 using syncable::DirectoryManager; | 33 using syncable::DirectoryManager; |
34 | 34 |
35 namespace browser_sync { | 35 namespace browser_sync { |
36 | 36 |
37 void ExecOrDie(sqlite3* dbhandle, const char *query) { | 37 void ExecOrDie(sqlite3* dbhandle, const char *query) { |
38 SQLStatement statement; | 38 sqlite_utils::SQLStatement statement; |
39 statement.prepare(dbhandle, query); | 39 statement.prepare(dbhandle, query); |
40 if (SQLITE_DONE != statement.step()) { | 40 if (SQLITE_DONE != statement.step()) { |
41 LOG(FATAL) << query << "\n" << sqlite3_errmsg(dbhandle); | 41 LOG(FATAL) << query << "\n" << sqlite3_errmsg(dbhandle); |
42 } | 42 } |
43 } | 43 } |
44 | 44 |
45 // Useful for encoding any sequence of bytes into a string that can be used in | 45 // Useful for encoding any sequence of bytes into a string that can be used in |
46 // a table name. Kind of like hex encoding, except that A is zero and P is 15. | 46 // a table name. Kind of like hex encoding, except that A is zero and P is 15. |
47 string APEncode(const string& in) { | 47 string APEncode(const string& in) { |
48 string result; | 48 string result; |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
107 LOG(WARNING) << "UserSettings database version " << current_version << | 107 LOG(WARNING) << "UserSettings database version " << current_version << |
108 " is too old to handle."; | 108 " is too old to handle."; |
109 return; | 109 return; |
110 case 10: | 110 case 10: |
111 { | 111 { |
112 // Scrape the 'shares' table to find the syncable DB. 'shares' had a | 112 // Scrape the 'shares' table to find the syncable DB. 'shares' had a |
113 // pair of string columns that mapped the username to the filename of | 113 // pair of string columns that mapped the username to the filename of |
114 // the sync data sqlite3 file. Version 11 switched to a constant | 114 // the sync data sqlite3 file. Version 11 switched to a constant |
115 // filename, so here we read the string, copy the file to the new name, | 115 // filename, so here we read the string, copy the file to the new name, |
116 // delete the old one, and then drop the unused shares table. | 116 // delete the old one, and then drop the unused shares table. |
117 SQLStatement share_query; | 117 sqlite_utils::SQLStatement share_query; |
118 share_query.prepare(handle, "SELECT share_name, file_name FROM shares"); | 118 share_query.prepare(handle, "SELECT share_name, file_name FROM shares"); |
119 int query_result = share_query.step(); | 119 int query_result = share_query.step(); |
120 CHECK(SQLITE_ROW == query_result); | 120 CHECK(SQLITE_ROW == query_result); |
121 FilePath::StringType share_name, file_name; | 121 FilePath::StringType share_name, file_name; |
122 #if defined(OS_POSIX) | 122 #if defined(OS_POSIX) |
123 share_name = share_query.column_string(0); | 123 share_name = share_query.column_string(0); |
124 file_name = share_query.column_string(1); | 124 file_name = share_query.column_string(1); |
125 #else | 125 #else |
126 share_name = share_query.column_wstring(0); | 126 share_name = share_query.column_wstring(0); |
127 file_name = share_query.column_wstring(1); | 127 file_name = share_query.column_wstring(1); |
(...skipping 30 matching lines...) Expand all Loading... |
158 "CREATE TABLE cookies" | 158 "CREATE TABLE cookies" |
159 " (email, service_name, service_token, " | 159 " (email, service_name, service_token, " |
160 " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)"); | 160 " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)"); |
161 } | 161 } |
162 | 162 |
163 static void MakeClientIDTable(sqlite3* const dbhandle) { | 163 static void MakeClientIDTable(sqlite3* const dbhandle) { |
164 // Stores a single client ID value that can be used as the client id, if | 164 // Stores a single client ID value that can be used as the client id, if |
165 // there's not another such ID provided on the install. | 165 // there's not another such ID provided on the install. |
166 ExecOrDie(dbhandle, "CREATE TABLE client_id (id) "); | 166 ExecOrDie(dbhandle, "CREATE TABLE client_id (id) "); |
167 { | 167 { |
168 SQLStatement statement; | 168 sqlite_utils::SQLStatement statement; |
169 statement.prepare(dbhandle, | 169 statement.prepare(dbhandle, |
170 "INSERT INTO client_id values ( ? )"); | 170 "INSERT INTO client_id values ( ? )"); |
171 statement.bind_string(0, Generate128BitRandomBase64String()); | 171 statement.bind_string(0, Generate128BitRandomBase64String()); |
172 if (SQLITE_DONE != statement.step()) { | 172 if (SQLITE_DONE != statement.step()) { |
173 LOG(FATAL) << "INSERT INTO client_id\n" << sqlite3_errmsg(dbhandle); | 173 LOG(FATAL) << "INSERT INTO client_id\n" << sqlite3_errmsg(dbhandle); |
174 } | 174 } |
175 } | 175 } |
176 } | 176 } |
177 | 177 |
178 bool UserSettings::Init(const FilePath& settings_path) { | 178 bool UserSettings::Init(const FilePath& settings_path) { |
179 { // Scope the handle. | 179 { // Scope the handle. |
180 ScopedDBHandle dbhandle(this); | 180 ScopedDBHandle dbhandle(this); |
181 if (dbhandle_) | 181 if (dbhandle_) |
182 sqlite3_close(dbhandle_); | 182 sqlite3_close(dbhandle_); |
183 | 183 |
184 if (SQLITE_OK != sqlite_utils::OpenSqliteDb(settings_path, &dbhandle_)) | 184 if (SQLITE_OK != sqlite_utils::OpenSqliteDb(settings_path, &dbhandle_)) |
185 return false; | 185 return false; |
186 | 186 |
187 // In the worst case scenario, the user may hibernate his computer during | 187 // In the worst case scenario, the user may hibernate his computer during |
188 // one of our transactions. | 188 // one of our transactions. |
189 sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max()); | 189 sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max()); |
190 ExecOrDie(dbhandle.get(), "PRAGMA fullfsync = 1"); | 190 ExecOrDie(dbhandle.get(), "PRAGMA fullfsync = 1"); |
191 ExecOrDie(dbhandle.get(), "PRAGMA synchronous = 2"); | 191 ExecOrDie(dbhandle.get(), "PRAGMA synchronous = 2"); |
192 | 192 |
193 SQLTransaction transaction(dbhandle.get()); | 193 sqlite_utils::SQLTransaction transaction(dbhandle.get()); |
194 transaction.BeginExclusive(); | 194 transaction.BeginExclusive(); |
195 SQLStatement table_query; | 195 sqlite_utils::SQLStatement table_query; |
196 table_query.prepare(dbhandle.get(), | 196 table_query.prepare(dbhandle.get(), |
197 "select count(*) from sqlite_master" | 197 "select count(*) from sqlite_master" |
198 " where type = 'table' and name = 'db_version'"); | 198 " where type = 'table' and name = 'db_version'"); |
199 int query_result = table_query.step(); | 199 int query_result = table_query.step(); |
200 CHECK(SQLITE_ROW == query_result); | 200 CHECK(SQLITE_ROW == query_result); |
201 int table_count = table_query.column_int(0); | 201 int table_count = table_query.column_int(0); |
202 table_query.reset(); | 202 table_query.reset(); |
203 if (table_count > 0) { | 203 if (table_count > 0) { |
204 SQLStatement version_query; | 204 sqlite_utils::SQLStatement version_query; |
205 version_query.prepare(dbhandle.get(), | 205 version_query.prepare(dbhandle.get(), |
206 "SELECT version FROM db_version"); | 206 "SELECT version FROM db_version"); |
207 query_result = version_query.step(); | 207 query_result = version_query.step(); |
208 CHECK(SQLITE_ROW == query_result); | 208 CHECK(SQLITE_ROW == query_result); |
209 const int version = version_query.column_int(0); | 209 const int version = version_query.column_int(0); |
210 version_query.reset(); | 210 version_query.reset(); |
211 if (version > kCurrentDBVersion) { | 211 if (version > kCurrentDBVersion) { |
212 LOG(WARNING) << "UserSettings database is too new."; | 212 LOG(WARNING) << "UserSettings database is too new."; |
213 return false; | 213 return false; |
214 } | 214 } |
215 | 215 |
216 MigrateOldVersionsAsNeeded(dbhandle.get(), version); | 216 MigrateOldVersionsAsNeeded(dbhandle.get(), version); |
217 } else { | 217 } else { |
218 // Create settings table. | 218 // Create settings table. |
219 { | 219 { |
220 SQLStatement statement; | 220 sqlite_utils::SQLStatement statement; |
221 statement.prepare(dbhandle.get(), | 221 statement.prepare(dbhandle.get(), |
222 "CREATE TABLE settings" | 222 "CREATE TABLE settings" |
223 " (email, key, value, " | 223 " (email, key, value, " |
224 " PRIMARY KEY(email, key) ON CONFLICT REPLACE)"); | 224 " PRIMARY KEY(email, key) ON CONFLICT REPLACE)"); |
225 if (SQLITE_DONE != statement.step()) { | 225 if (SQLITE_DONE != statement.step()) { |
226 return false; | 226 return false; |
227 } | 227 } |
228 } | 228 } |
229 // Create and populate version table. | 229 // Create and populate version table. |
230 { | 230 { |
231 SQLStatement statement; | 231 sqlite_utils::SQLStatement statement; |
232 statement.prepare(dbhandle.get(), | 232 statement.prepare(dbhandle.get(), |
233 "CREATE TABLE db_version ( version )"); | 233 "CREATE TABLE db_version ( version )"); |
234 if (SQLITE_DONE != statement.step()) { | 234 if (SQLITE_DONE != statement.step()) { |
235 return false; | 235 return false; |
236 } | 236 } |
237 } | 237 } |
238 { | 238 { |
239 SQLStatement statement; | 239 sqlite_utils::SQLStatement statement; |
240 statement.prepare(dbhandle.get(), | 240 statement.prepare(dbhandle.get(), |
241 "INSERT INTO db_version values ( ? )"); | 241 "INSERT INTO db_version values ( ? )"); |
242 statement.bind_int(0, kCurrentDBVersion); | 242 statement.bind_int(0, kCurrentDBVersion); |
243 if (SQLITE_DONE != statement.step()) { | 243 if (SQLITE_DONE != statement.step()) { |
244 return false; | 244 return false; |
245 } | 245 } |
246 } | 246 } |
247 | 247 |
248 MakeSigninsTable(dbhandle.get()); | 248 MakeSigninsTable(dbhandle.get()); |
249 MakeCookiesTable(dbhandle.get()); | 249 MakeCookiesTable(dbhandle.get()); |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
281 mask = mask >> 8; | 281 mask = mask >> 8; |
282 if (0 == mask) | 282 if (0 == mask) |
283 break; | 283 break; |
284 } | 284 } |
285 return hash; | 285 return hash; |
286 } | 286 } |
287 | 287 |
288 void UserSettings::StoreEmailForSignin(const string& signin, | 288 void UserSettings::StoreEmailForSignin(const string& signin, |
289 const string& primary_email) { | 289 const string& primary_email) { |
290 ScopedDBHandle dbhandle(this); | 290 ScopedDBHandle dbhandle(this); |
291 SQLTransaction transaction(dbhandle.get()); | 291 sqlite_utils::SQLTransaction transaction(dbhandle.get()); |
292 int sqlite_result = transaction.BeginExclusive(); | 292 int sqlite_result = transaction.BeginExclusive(); |
293 CHECK(SQLITE_OK == sqlite_result); | 293 CHECK(SQLITE_OK == sqlite_result); |
294 SQLStatement query; | 294 sqlite_utils::SQLStatement query; |
295 query.prepare(dbhandle.get(), | 295 query.prepare(dbhandle.get(), |
296 "SELECT COUNT(*) FROM signins" | 296 "SELECT COUNT(*) FROM signins" |
297 " WHERE signin = ? AND primary_email = ?"); | 297 " WHERE signin = ? AND primary_email = ?"); |
298 query.bind_string(0, signin); | 298 query.bind_string(0, signin); |
299 query.bind_string(1, primary_email); | 299 query.bind_string(1, primary_email); |
300 int query_result = query.step(); | 300 int query_result = query.step(); |
301 CHECK(SQLITE_ROW == query_result); | 301 CHECK(SQLITE_ROW == query_result); |
302 int32 count = query.column_int(0); | 302 int32 count = query.column_int(0); |
303 query.reset(); | 303 query.reset(); |
304 if (0 == count) { | 304 if (0 == count) { |
305 // Migrate any settings the user might have from earlier versions. | 305 // Migrate any settings the user might have from earlier versions. |
306 { | 306 { |
307 SQLStatement statement; | 307 sqlite_utils::SQLStatement statement; |
308 statement.prepare(dbhandle.get(), | 308 statement.prepare(dbhandle.get(), |
309 "UPDATE settings SET email = ? WHERE email = ?"); | 309 "UPDATE settings SET email = ? WHERE email = ?"); |
310 statement.bind_string(0, signin); | 310 statement.bind_string(0, signin); |
311 statement.bind_string(1, primary_email); | 311 statement.bind_string(1, primary_email); |
312 if (SQLITE_DONE != statement.step()) { | 312 if (SQLITE_DONE != statement.step()) { |
313 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | 313 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); |
314 } | 314 } |
315 } | 315 } |
316 // Store this signin:email mapping. | 316 // Store this signin:email mapping. |
317 { | 317 { |
318 SQLStatement statement; | 318 sqlite_utils::SQLStatement statement; |
319 statement.prepare(dbhandle.get(), | 319 statement.prepare(dbhandle.get(), |
320 "INSERT INTO signins(signin, primary_email)" | 320 "INSERT INTO signins(signin, primary_email)" |
321 " values ( ?, ? )"); | 321 " values ( ?, ? )"); |
322 statement.bind_string(0, signin); | 322 statement.bind_string(0, signin); |
323 statement.bind_string(1, primary_email); | 323 statement.bind_string(1, primary_email); |
324 if (SQLITE_DONE != statement.step()) { | 324 if (SQLITE_DONE != statement.step()) { |
325 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | 325 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); |
326 } | 326 } |
327 } | 327 } |
328 } | 328 } |
329 transaction.Commit(); | 329 transaction.Commit(); |
330 } | 330 } |
331 | 331 |
332 // string* signin is both the input and the output of this function. | 332 // string* signin is both the input and the output of this function. |
333 bool UserSettings::GetEmailForSignin(string* signin) { | 333 bool UserSettings::GetEmailForSignin(string* signin) { |
334 ScopedDBHandle dbhandle(this); | 334 ScopedDBHandle dbhandle(this); |
335 string result; | 335 string result; |
336 SQLStatement query; | 336 sqlite_utils::SQLStatement query; |
337 query.prepare(dbhandle.get(), | 337 query.prepare(dbhandle.get(), |
338 "SELECT primary_email FROM signins WHERE signin = ?"); | 338 "SELECT primary_email FROM signins WHERE signin = ?"); |
339 query.bind_string(0, *signin); | 339 query.bind_string(0, *signin); |
340 int query_result = query.step(); | 340 int query_result = query.step(); |
341 if (SQLITE_ROW == query_result) { | 341 if (SQLITE_ROW == query_result) { |
342 query.column_string(0, &result); | 342 query.column_string(0, &result); |
343 if (!result.empty()) { | 343 if (!result.empty()) { |
344 swap(result, *signin); | 344 swap(result, *signin); |
345 return true; | 345 return true; |
346 } | 346 } |
347 } | 347 } |
348 return false; | 348 return false; |
349 } | 349 } |
350 | 350 |
351 void UserSettings::StoreHashedPassword(const string& email, | 351 void UserSettings::StoreHashedPassword(const string& email, |
352 const string& password) { | 352 const string& password) { |
353 // Save one-way hashed password: | 353 // Save one-way hashed password: |
354 char binary_salt[kSaltSize]; | 354 char binary_salt[kSaltSize]; |
355 base::RandBytes(binary_salt, sizeof(binary_salt)); | 355 base::RandBytes(binary_salt, sizeof(binary_salt)); |
356 | 356 |
357 const string salt = APEncode(string(binary_salt, sizeof(binary_salt))); | 357 const string salt = APEncode(string(binary_salt, sizeof(binary_salt))); |
358 base::MD5Context md5_context; | 358 base::MD5Context md5_context; |
359 base::MD5Init(&md5_context); | 359 base::MD5Init(&md5_context); |
360 base::MD5Update(&md5_context, salt); | 360 base::MD5Update(&md5_context, salt); |
361 base::MD5Update(&md5_context, password); | 361 base::MD5Update(&md5_context, password); |
362 base::MD5Digest md5_digest; | 362 base::MD5Digest md5_digest; |
363 base::MD5Final(&md5_digest, &md5_context); | 363 base::MD5Final(&md5_digest, &md5_context); |
364 | 364 |
365 ScopedDBHandle dbhandle(this); | 365 ScopedDBHandle dbhandle(this); |
366 SQLTransaction transaction(dbhandle.get()); | 366 sqlite_utils::SQLTransaction transaction(dbhandle.get()); |
367 transaction.BeginExclusive(); | 367 transaction.BeginExclusive(); |
368 { | 368 { |
369 SQLStatement statement; | 369 sqlite_utils::SQLStatement statement; |
370 statement.prepare(dbhandle.get(), | 370 statement.prepare(dbhandle.get(), |
371 "INSERT INTO settings(email, key, value)" | 371 "INSERT INTO settings(email, key, value)" |
372 " values ( ?, ?, ? )"); | 372 " values ( ?, ?, ? )"); |
373 statement.bind_string(0, email); | 373 statement.bind_string(0, email); |
374 statement.bind_string(1, PASSWORD_HASH); | 374 statement.bind_string(1, PASSWORD_HASH); |
375 statement.bind_int(2, GetHashFromDigest(md5_digest)); | 375 statement.bind_int(2, GetHashFromDigest(md5_digest)); |
376 if (SQLITE_DONE != statement.step()) { | 376 if (SQLITE_DONE != statement.step()) { |
377 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | 377 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); |
378 } | 378 } |
379 } | 379 } |
380 { | 380 { |
381 SQLStatement statement; | 381 sqlite_utils::SQLStatement statement; |
382 statement.prepare(dbhandle.get(), | 382 statement.prepare(dbhandle.get(), |
383 "INSERT INTO settings(email, key, value)" | 383 "INSERT INTO settings(email, key, value)" |
384 " values ( ?, ?, ? )"); | 384 " values ( ?, ?, ? )"); |
385 statement.bind_string(0, email); | 385 statement.bind_string(0, email); |
386 statement.bind_string(1, SALT); | 386 statement.bind_string(1, SALT); |
387 statement.bind_string(2, salt); | 387 statement.bind_string(2, salt); |
388 if (SQLITE_DONE != statement.step()) { | 388 if (SQLITE_DONE != statement.step()) { |
389 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | 389 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); |
390 } | 390 } |
391 } | 391 } |
392 transaction.Commit(); | 392 transaction.Commit(); |
393 } | 393 } |
394 | 394 |
395 bool UserSettings::VerifyAgainstStoredHash(const string& email, | 395 bool UserSettings::VerifyAgainstStoredHash(const string& email, |
396 const string& password) { | 396 const string& password) { |
397 ScopedDBHandle dbhandle(this); | 397 ScopedDBHandle dbhandle(this); |
398 string salt_and_digest; | 398 string salt_and_digest; |
399 | 399 |
400 SQLStatement query; | 400 sqlite_utils::SQLStatement query; |
401 query.prepare(dbhandle.get(), | 401 query.prepare(dbhandle.get(), |
402 "SELECT key, value FROM settings" | 402 "SELECT key, value FROM settings" |
403 " WHERE email = ? AND (key = ? OR key = ?)"); | 403 " WHERE email = ? AND (key = ? OR key = ?)"); |
404 query.bind_string(0, email); | 404 query.bind_string(0, email); |
405 query.bind_string(1, PASSWORD_HASH); | 405 query.bind_string(1, PASSWORD_HASH); |
406 query.bind_string(2, SALT); | 406 query.bind_string(2, SALT); |
407 int query_result = query.step(); | 407 int query_result = query.step(); |
408 string salt; | 408 string salt; |
409 int32 hash = kInvalidHash; | 409 int32 hash = kInvalidHash; |
410 while (SQLITE_ROW == query_result) { | 410 while (SQLITE_ROW == query_result) { |
(...skipping 18 matching lines...) Expand all Loading... |
429 | 429 |
430 void UserSettings::SwitchUser(const string& username) { | 430 void UserSettings::SwitchUser(const string& username) { |
431 { | 431 { |
432 base::AutoLock lock(mutex_); | 432 base::AutoLock lock(mutex_); |
433 email_ = username; | 433 email_ = username; |
434 } | 434 } |
435 } | 435 } |
436 | 436 |
437 string UserSettings::GetClientId() { | 437 string UserSettings::GetClientId() { |
438 ScopedDBHandle dbhandle(this); | 438 ScopedDBHandle dbhandle(this); |
439 SQLStatement statement; | 439 sqlite_utils::SQLStatement statement; |
440 statement.prepare(dbhandle.get(), "SELECT id FROM client_id"); | 440 statement.prepare(dbhandle.get(), "SELECT id FROM client_id"); |
441 int query_result = statement.step(); | 441 int query_result = statement.step(); |
442 string client_id; | 442 string client_id; |
443 if (query_result == SQLITE_ROW) | 443 if (query_result == SQLITE_ROW) |
444 client_id = statement.column_string(0); | 444 client_id = statement.column_string(0); |
445 return client_id; | 445 return client_id; |
446 } | 446 } |
447 | 447 |
448 void UserSettings::ClearAllServiceTokens() { | 448 void UserSettings::ClearAllServiceTokens() { |
449 ScopedDBHandle dbhandle(this); | 449 ScopedDBHandle dbhandle(this); |
450 ExecOrDie(dbhandle.get(), "DELETE FROM cookies"); | 450 ExecOrDie(dbhandle.get(), "DELETE FROM cookies"); |
451 } | 451 } |
452 | 452 |
453 bool UserSettings::GetLastUser(string* username) { | 453 bool UserSettings::GetLastUser(string* username) { |
454 ScopedDBHandle dbhandle(this); | 454 ScopedDBHandle dbhandle(this); |
455 SQLStatement query; | 455 sqlite_utils::SQLStatement query; |
456 query.prepare(dbhandle.get(), "SELECT email FROM cookies"); | 456 query.prepare(dbhandle.get(), "SELECT email FROM cookies"); |
457 if (SQLITE_ROW == query.step()) { | 457 if (SQLITE_ROW == query.step()) { |
458 *username = query.column_string(0); | 458 *username = query.column_string(0); |
459 return true; | 459 return true; |
460 } else { | 460 } else { |
461 return false; | 461 return false; |
462 } | 462 } |
463 } | 463 } |
464 | 464 |
465 } // namespace browser_sync | 465 } // namespace browser_sync |
OLD | NEW |