Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(151)

Side by Side Diff: chrome/install_static/install_util.cc

Issue 2487783002: Make Crashpad use the user data dir, rather than always default location (Closed)
Patch Set: install_static for swarming Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 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 #include "chrome/install_static/install_util.h" 5 #include "chrome/install_static/install_util.h"
6 6
7 #include <windows.h> 7 #include <windows.h>
8 #include <assert.h> 8 #include <assert.h>
9 #include <stdlib.h>
9 #include <string.h> 10 #include <string.h>
10 11
11 #include <algorithm> 12 #include <algorithm>
12 #include <memory> 13 #include <memory>
13 #include <sstream> 14 #include <sstream>
14 15
15 #include "chrome/install_static/install_details.h" 16 #include "chrome/install_static/install_details.h"
16 #include "chrome/install_static/install_modes.h" 17 #include "chrome/install_static/install_modes.h"
18 #include "chrome/install_static/policy_path_parser.h"
19 #include "chrome/install_static/user_data_dir.h"
17 #include "chrome_elf/nt_registry/nt_registry.h" 20 #include "chrome_elf/nt_registry/nt_registry.h"
18 21
19 namespace install_static { 22 namespace install_static {
20 23
21 ProcessType g_process_type = ProcessType::UNINITIALIZED; 24 ProcessType g_process_type = ProcessType::UNINITIALIZED;
22 25
23 const wchar_t kRegValueChromeStatsSample[] = L"UsageStatsInSample"; 26 const wchar_t kRegValueChromeStatsSample[] = L"UsageStatsInSample";
24 27
25 // TODO(ananta) 28 // TODO(ananta)
26 // http://crbug.com/604923 29 // http://crbug.com/604923
27 // The constants defined in this file are also defined in chrome/installer and 30 // The constants defined in this file are also defined in chrome/installer and
28 // other places. we need to unify them. 31 // other places. we need to unify them.
29 const wchar_t kHeadless[] = L"CHROME_HEADLESS"; 32 const wchar_t kHeadless[] = L"CHROME_HEADLESS";
30 const wchar_t kShowRestart[] = L"CHROME_CRASHED"; 33 const wchar_t kShowRestart[] = L"CHROME_CRASHED";
31 const wchar_t kRestartInfo[] = L"CHROME_RESTART"; 34 const wchar_t kRestartInfo[] = L"CHROME_RESTART";
32 const wchar_t kRtlLocale[] = L"RIGHT_TO_LEFT"; 35 const wchar_t kRtlLocale[] = L"RIGHT_TO_LEFT";
33 36
34 const char kGpuProcess[] = "gpu-process"; 37 const wchar_t kCrashpadHandler[] = L"crashpad-handler";
35 const char kPpapiPluginProcess[] = "ppapi"; 38 const wchar_t kProcessType[] = L"type";
36 const char kRendererProcess[] = "renderer"; 39 const wchar_t kUserDataDirSwitch[] = L"user-data-dir";
37 const char kUtilityProcess[] = "utility"; 40 const wchar_t kUtilityProcess[] = L"utility";
38 const char kProcessType[] = "type";
39 const char kCrashpadHandler[] = "crashpad-handler";
40 41
41 namespace { 42 namespace {
42 43
43 // TODO(ananta) 44 // TODO(ananta)
44 // http://crbug.com/604923 45 // http://crbug.com/604923
45 // The constants defined in this file are also defined in chrome/installer and 46 // The constants defined in this file are also defined in chrome/installer and
46 // other places. we need to unify them. 47 // other places. we need to unify them.
47 // Chrome channel display names. 48 // Chrome channel display names.
48 constexpr wchar_t kChromeChannelDev[] = L"dev"; 49 constexpr wchar_t kChromeChannelDev[] = L"dev";
49 constexpr wchar_t kChromeChannelBeta[] = L"beta"; 50 constexpr wchar_t kChromeChannelBeta[] = L"beta";
50 constexpr wchar_t kChromeChannelStableExplicit[] = L"stable"; 51 constexpr wchar_t kChromeChannelStableExplicit[] = L"stable";
51 52
52 // TODO(ananta) 53 // TODO(ananta)
53 // http://crbug.com/604923 54 // http://crbug.com/604923
54 // These constants are defined in the chrome/installer directory as well. We 55 // These constants are defined in the chrome/installer directory as well. We
55 // need to unify them. 56 // need to unify them.
56 constexpr wchar_t kRegValueAp[] = L"ap"; 57 constexpr wchar_t kRegValueAp[] = L"ap";
57 constexpr wchar_t kRegValueUsageStats[] = L"usagestats"; 58 constexpr wchar_t kRegValueUsageStats[] = L"usagestats";
58 constexpr wchar_t kMetricsReportingEnabled[] = L"MetricsReportingEnabled"; 59 constexpr wchar_t kMetricsReportingEnabled[] = L"MetricsReportingEnabled";
59 60
60 constexpr wchar_t kUserDataDirname[] = L"User Data";
61 constexpr wchar_t kBrowserCrashDumpMetricsSubKey[] = 61 constexpr wchar_t kBrowserCrashDumpMetricsSubKey[] =
62 L"\\BrowserCrashDumpAttempts"; 62 L"\\BrowserCrashDumpAttempts";
63 63
64 void Trace(const wchar_t* format_string, ...) { 64 void Trace(const wchar_t* format_string, ...) {
65 static const int kMaxLogBufferSize = 1024; 65 static const int kMaxLogBufferSize = 1024;
66 static wchar_t buffer[kMaxLogBufferSize] = {}; 66 static wchar_t buffer[kMaxLogBufferSize] = {};
67 67
68 va_list args = {}; 68 va_list args = {};
69 69
70 va_start(args, format_string); 70 va_start(args, format_string);
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
144 uint32_t size = 0; 144 uint32_t size = 0;
145 BOOL r = ::VerQueryValueW(version_resource, sub_block, &value, &size); 145 BOOL r = ::VerQueryValueW(version_resource, sub_block, &value, &size);
146 if (r && value) { 146 if (r && value) {
147 value_str->assign(static_cast<wchar_t*>(value)); 147 value_str->assign(static_cast<wchar_t*>(value));
148 return true; 148 return true;
149 } 149 }
150 } 150 }
151 return false; 151 return false;
152 } 152 }
153 153
154 bool RecursiveDirectoryCreate(const std::wstring& full_path) { 154 bool DirectoryExists(const std::wstring& path) {
155 // If the path exists, we've succeeded if it's a directory, failed otherwise. 155 DWORD file_attributes = ::GetFileAttributes(path.c_str());
156 const wchar_t* full_path_str = full_path.c_str(); 156 if (file_attributes == INVALID_FILE_ATTRIBUTES)
157 DWORD file_attributes = ::GetFileAttributes(full_path_str);
158 if (file_attributes != INVALID_FILE_ATTRIBUTES) {
159 if ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
160 Trace(L"%hs( %ls directory exists )\n", __func__, full_path_str);
161 return true;
162 }
163 Trace(L"%hs( %ls directory conflicts with an existing file. )\n", __func__,
164 full_path_str);
165 return false; 157 return false;
166 } 158 return (file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
167
168 // Invariant: Path does not exist as file or directory.
169
170 // Attempt to create the parent recursively. This will immediately return
171 // true if it already exists, otherwise will create all required parent
172 // directories starting with the highest-level missing parent.
173 std::wstring parent_path;
174 std::size_t pos = full_path.find_last_of(L"/\\");
175 if (pos != std::wstring::npos) {
176 parent_path = full_path.substr(0, pos);
177 if (!RecursiveDirectoryCreate(parent_path)) {
178 Trace(L"Failed to create one of the parent directories");
179 return false;
180 }
181 }
182 if (!::CreateDirectory(full_path_str, nullptr)) {
183 DWORD error_code = ::GetLastError();
184 if (error_code == ERROR_ALREADY_EXISTS) {
185 DWORD file_attributes = ::GetFileAttributes(full_path_str);
186 if ((file_attributes != INVALID_FILE_ATTRIBUTES) &&
187 ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)) {
188 // This error code ERROR_ALREADY_EXISTS doesn't indicate whether we
189 // were racing with someone creating the same directory, or a file
190 // with the same path. If the directory exists, we lost the
191 // race to create the same directory.
192 return true;
193 } else {
194 Trace(L"Failed to create directory %ls, last error is %d\n",
195 full_path_str, error_code);
196 return false;
197 }
198 }
199 }
200 return true;
201 }
202
203 // Appends "[kCompanyPathName\]kProductPathName[install_suffix]" to |path|,
204 // returning a reference to |path|.
205 std::wstring& AppendChromeInstallSubDirectory(std::wstring* path,
206 bool include_suffix) {
207 if (*kCompanyPathName) {
208 path->append(kCompanyPathName);
209 path->push_back(L'\\');
210 }
211 path->append(kProductPathName, kProductPathNameLength);
212 if (!include_suffix)
213 return *path;
214 return path->append(InstallDetails::Get().install_suffix());
215 } 159 }
216 160
217 std::wstring GetChromeInstallRegistryPath() { 161 std::wstring GetChromeInstallRegistryPath() {
218 std::wstring registry_path = L"Software\\"; 162 std::wstring registry_path = L"Software\\";
219 return AppendChromeInstallSubDirectory(&registry_path, 163 return AppendChromeInstallSubDirectory(
220 true /* include_suffix */); 164 InstallDetails::Get().mode(), true /* include_suffix */, &registry_path);
221 } 165 }
222 166
223 // Returns true if the |source| string matches the |pattern|. The pattern 167 // Returns true if the |source| string matches the |pattern|. The pattern
224 // may contain wildcards like '?' which matches one character or a '*' 168 // may contain wildcards like '?' which matches one character or a '*'
225 // which matches 0 or more characters. 169 // which matches 0 or more characters.
226 // Please note that pattern matches the whole string. If you want to find 170 // Please note that pattern matches the whole string. If you want to find
227 // something in the middle of the string then you need to specify the pattern 171 // something in the middle of the string then you need to specify the pattern
228 // as '*xyz*'. 172 // as '*xyz*'.
229 // |source_index| is the index of the current character being matched in 173 // |source_index| is the index of the current character being matched in
230 // |source|. 174 // |source|.
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
417 KEY_SET_VALUE | KEY_WOW64_32KEY, &key_handle)) { 361 KEY_SET_VALUE | KEY_WOW64_32KEY, &key_handle)) {
418 return false; 362 return false;
419 } 363 }
420 364
421 bool success = nt::SetRegValueDWORD(key_handle, kRegValueChromeStatsSample, 365 bool success = nt::SetRegValueDWORD(key_handle, kRegValueChromeStatsSample,
422 in_sample ? 1 : 0); 366 in_sample ? 1 : 0);
423 nt::CloseRegKey(key_handle); 367 nt::CloseRegKey(key_handle);
424 return success; 368 return success;
425 } 369 }
426 370
371 // Appends "[kCompanyPathName\]kProductPathName[install_suffix]" to |path|,
372 // returning a reference to |path|.
373 std::wstring& AppendChromeInstallSubDirectory(const InstallConstants& mode,
374 bool include_suffix,
375 std::wstring* path) {
376 if (*kCompanyPathName) {
377 path->append(kCompanyPathName);
378 path->push_back(L'\\');
379 }
380 path->append(kProductPathName, kProductPathNameLength);
381 if (!include_suffix)
382 return *path;
383 return path->append(mode.install_suffix);
384 }
385
427 bool ReportingIsEnforcedByPolicy(bool* crash_reporting_enabled) { 386 bool ReportingIsEnforcedByPolicy(bool* crash_reporting_enabled) {
428 std::wstring policies_path = L"SOFTWARE\\Policies\\"; 387 std::wstring policies_path = L"SOFTWARE\\Policies\\";
429 AppendChromeInstallSubDirectory(&policies_path, false /* !include_suffix */); 388 AppendChromeInstallSubDirectory(InstallDetails::Get().mode(),
389 false /* !include_suffix */, &policies_path);
430 DWORD value = 0; 390 DWORD value = 0;
431 391
432 // First, try HKLM. 392 // First, try HKLM.
433 if (nt::QueryRegValueDWORD(nt::HKLM, nt::NONE, policies_path.c_str(), 393 if (nt::QueryRegValueDWORD(nt::HKLM, nt::NONE, policies_path.c_str(),
434 kMetricsReportingEnabled, &value)) { 394 kMetricsReportingEnabled, &value)) {
435 *crash_reporting_enabled = (value != 0); 395 *crash_reporting_enabled = (value != 0);
436 return true; 396 return true;
437 } 397 }
438 398
439 // Second, try HKCU. 399 // Second, try HKCU.
(...skipping 26 matching lines...) Expand all
466 } 426 }
467 427
468 g_process_type = ProcessType::BROWSER_PROCESS; 428 g_process_type = ProcessType::BROWSER_PROCESS;
469 } 429 }
470 430
471 bool IsNonBrowserProcess() { 431 bool IsNonBrowserProcess() {
472 assert(g_process_type != ProcessType::UNINITIALIZED); 432 assert(g_process_type != ProcessType::UNINITIALIZED);
473 return g_process_type == ProcessType::NON_BROWSER_PROCESS; 433 return g_process_type == ProcessType::NON_BROWSER_PROCESS;
474 } 434 }
475 435
476 bool GetDefaultUserDataDirectory(std::wstring* result) { 436 std::wstring GetCrashDumpLocation() {
477 // This environment variable should be set on Windows Vista and later 437 // In order to be able to start crash handling very early and in chrome_elf,
478 // (https://msdn.microsoft.com/library/windows/desktop/dd378457.aspx). 438 // we cannot rely on chrome's PathService entries (for DIR_CRASH_DUMPS) being
479 std::wstring user_data_dir = GetEnvironmentString16(L"LOCALAPPDATA"); 439 // available on Windows. See https://crbug.com/564398.
480 440 std::wstring user_data_dir;
481 if (user_data_dir.empty()) { 441 bool ret = GetUserDataDirectory(&user_data_dir, nullptr);
482 // LOCALAPPDATA was not set; fallback to the temporary files path. 442 assert(ret);
483 DWORD size = ::GetTempPath(0, nullptr); 443 IgnoreUnused(ret);
484 if (!size) 444 return user_data_dir.append(L"\\Crashpad");
485 return false;
486 user_data_dir.resize(size + 1);
487 size = ::GetTempPath(size + 1, &user_data_dir[0]);
488 if (!size || size >= user_data_dir.size())
489 return false;
490 user_data_dir.resize(size);
491 }
492
493 result->swap(user_data_dir);
494 if ((*result)[result->length() - 1] != L'\\')
495 result->push_back(L'\\');
496 AppendChromeInstallSubDirectory(result, true /* include_suffix */);
497 result->push_back(L'\\');
498 result->append(kUserDataDirname);
499 return true;
500 }
501
502 bool GetDefaultCrashDumpLocation(std::wstring* crash_dir) {
503 // In order to be able to start crash handling very early, we do not rely on
504 // chrome's PathService entries (for DIR_CRASH_DUMPS) being available on
505 // Windows. See https://crbug.com/564398.
506 if (!GetDefaultUserDataDirectory(crash_dir))
507 return false;
508
509 // We have to make sure the user data dir exists on first run. See
510 // http://crbug.com/591504.
511 if (!RecursiveDirectoryCreate(*crash_dir))
512 return false;
513 crash_dir->append(L"\\Crashpad");
514 return true;
515 } 445 }
516 446
517 std::string GetEnvironmentString(const std::string& variable_name) { 447 std::string GetEnvironmentString(const std::string& variable_name) {
518 return UTF16ToUTF8( 448 return UTF16ToUTF8(
519 GetEnvironmentString16(UTF8ToUTF16(variable_name).c_str())); 449 GetEnvironmentString16(UTF8ToUTF16(variable_name).c_str()));
520 } 450 }
521 451
522 std::wstring GetEnvironmentString16(const wchar_t* variable_name) { 452 std::wstring GetEnvironmentString16(const wchar_t* variable_name) {
523 DWORD value_length = ::GetEnvironmentVariableW(variable_name, nullptr, 0); 453 DWORD value_length = ::GetEnvironmentVariableW(variable_name, nullptr, 0);
524 if (!value_length) 454 if (!value_length)
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
644 bool trim_spaces) { 574 bool trim_spaces) {
645 return TokenizeStringT<std::string>(str, delimiter, trim_spaces); 575 return TokenizeStringT<std::string>(str, delimiter, trim_spaces);
646 } 576 }
647 577
648 std::vector<std::wstring> TokenizeString16(const std::wstring& str, 578 std::vector<std::wstring> TokenizeString16(const std::wstring& str,
649 wchar_t delimiter, 579 wchar_t delimiter,
650 bool trim_spaces) { 580 bool trim_spaces) {
651 return TokenizeStringT<std::wstring>(str, delimiter, trim_spaces); 581 return TokenizeStringT<std::wstring>(str, delimiter, trim_spaces);
652 } 582 }
653 583
654 std::string GetSwitchValueFromCommandLine(const std::string& command_line, 584 std::wstring GetSwitchValueFromCommandLine(const std::wstring& command_line,
655 const std::string& switch_name) { 585 const std::wstring& switch_name) {
656 assert(!command_line.empty()); 586 assert(!command_line.empty());
657 assert(!switch_name.empty()); 587 assert(!switch_name.empty());
658 588
659 std::string command_line_copy = command_line; 589 std::wstring command_line_copy = command_line;
660 // Remove leading and trailing spaces. 590 // Remove leading and trailing spaces.
661 TrimT<std::string>(&command_line_copy); 591 TrimT<std::wstring>(&command_line_copy);
662 592
663 // Find the switch in the command line. If we don't find the switch, return 593 // Find the switch in the command line. If we don't find the switch, return
664 // an empty string. 594 // an empty string.
665 std::string switch_token = "--"; 595 std::wstring switch_token = L"--";
666 switch_token += switch_name; 596 switch_token += switch_name;
667 switch_token += "="; 597 switch_token += L"=";
668 size_t switch_offset = command_line_copy.find(switch_token); 598 size_t switch_offset = command_line_copy.find(switch_token);
669 if (switch_offset == std::string::npos) 599 if (switch_offset == std::string::npos)
670 return std::string(); 600 return std::wstring();
671 601
672 // The format is "--<switch name>=blah". Look for a space after the 602 // The format is "--<switch name>=blah". Look for a space after the
673 // "--<switch name>=" string. If we don't find a space assume that the switch 603 // "--<switch name>=" string. If we don't find a space assume that the switch
674 // value ends at the end of the command line. 604 // value ends at the end of the command line.
675 size_t switch_value_start_offset = switch_offset + switch_token.length(); 605 size_t switch_value_start_offset = switch_offset + switch_token.length();
676 if (std::string(kWhiteSpaces).find( 606 if (std::wstring(kWhiteSpaces16).find(
677 command_line_copy[switch_value_start_offset]) != std::string::npos) { 607 command_line_copy[switch_value_start_offset]) != std::wstring::npos) {
678 switch_value_start_offset = command_line_copy.find_first_not_of( 608 switch_value_start_offset = command_line_copy.find_first_not_of(
679 GetWhiteSpacesForType<std::string>(), switch_value_start_offset); 609 GetWhiteSpacesForType<std::wstring>(), switch_value_start_offset);
680 if (switch_value_start_offset == std::string::npos) 610 if (switch_value_start_offset == std::wstring::npos)
681 return std::string(); 611 return std::wstring();
682 } 612 }
683 size_t switch_value_end_offset = 613 size_t switch_value_end_offset =
684 command_line_copy.find_first_of(GetWhiteSpacesForType<std::string>(), 614 command_line_copy.find_first_of(GetWhiteSpacesForType<std::wstring>(),
685 switch_value_start_offset); 615 switch_value_start_offset);
686 if (switch_value_end_offset == std::string::npos) 616 if (switch_value_end_offset == std::wstring::npos)
687 switch_value_end_offset = command_line_copy.length(); 617 switch_value_end_offset = command_line_copy.length();
688 618
689 std::string switch_value = command_line_copy.substr( 619 std::wstring switch_value = command_line_copy.substr(
690 switch_value_start_offset, 620 switch_value_start_offset,
691 switch_value_end_offset - (switch_offset + switch_token.length())); 621 switch_value_end_offset - (switch_offset + switch_token.length()));
692 TrimT<std::string>(&switch_value); 622 TrimT<std::wstring>(&switch_value);
693 return switch_value; 623 return switch_value;
694 } 624 }
695 625
626 bool RecursiveDirectoryCreate(const std::wstring& full_path) {
627 // If the path exists, we've succeeded if it's a directory, failed otherwise.
628 const wchar_t* full_path_str = full_path.c_str();
629 DWORD file_attributes = ::GetFileAttributes(full_path_str);
630 if (file_attributes != INVALID_FILE_ATTRIBUTES) {
631 if ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
632 Trace(L"%hs( %ls directory exists )\n", __func__, full_path_str);
633 return true;
634 }
635 Trace(L"%hs( %ls directory conflicts with an existing file. )\n", __func__,
636 full_path_str);
637 return false;
638 }
639
640 // Invariant: Path does not exist as file or directory.
641
642 // Attempt to create the parent recursively. This will immediately return
643 // true if it already exists, otherwise will create all required parent
644 // directories starting with the highest-level missing parent.
645 std::wstring parent_path;
646 std::size_t pos = full_path.find_last_of(L"/\\");
647 if (pos != std::wstring::npos) {
648 parent_path = full_path.substr(0, pos);
649 if (!RecursiveDirectoryCreate(parent_path)) {
650 Trace(L"Failed to create one of the parent directories");
651 return false;
652 }
653 }
654 if (!::CreateDirectory(full_path_str, nullptr)) {
655 DWORD error_code = ::GetLastError();
656 if (error_code == ERROR_ALREADY_EXISTS && DirectoryExists(full_path_str)) {
657 // This error code ERROR_ALREADY_EXISTS doesn't indicate whether we
658 // were racing with someone creating the same directory, or a file
659 // with the same path. If the directory exists, we lost the
660 // race to create the same directory.
661 return true;
662 } else {
663 Trace(L"Failed to create directory %ls, last error is %d\n",
664 full_path_str, error_code);
665 return false;
666 }
667 }
668 return true;
669 }
670
696 // This function takes these inputs rather than accessing the module's 671 // This function takes these inputs rather than accessing the module's
697 // InstallDetails instance since it is used to bootstrap InstallDetails. 672 // InstallDetails instance since it is used to bootstrap InstallDetails.
698 std::wstring DetermineChannel(const InstallConstants& mode, 673 std::wstring DetermineChannel(const InstallConstants& mode,
699 bool system_level, 674 bool system_level,
700 bool multi_install) { 675 bool multi_install) {
701 if (!kUseGoogleUpdateIntegration) 676 if (!kUseGoogleUpdateIntegration)
702 return std::wstring(); 677 return std::wstring();
703 678
704 switch (mode.channel_strategy) { 679 switch (mode.channel_strategy) {
705 case ChannelStrategy::UNSUPPORTED: 680 case ChannelStrategy::UNSUPPORTED:
706 assert(false); 681 assert(false);
707 break; 682 break;
708 case ChannelStrategy::ADDITIONAL_PARAMETERS: 683 case ChannelStrategy::ADDITIONAL_PARAMETERS:
709 return ChannelFromAdditionalParameters(mode, system_level, multi_install); 684 return ChannelFromAdditionalParameters(mode, system_level, multi_install);
710 case ChannelStrategy::FIXED: 685 case ChannelStrategy::FIXED:
711 return mode.default_channel_name; 686 return mode.default_channel_name;
712 } 687 }
713 688
714 return std::wstring(); 689 return std::wstring();
715 } 690 }
716 691
717 } // namespace install_static 692 } // namespace install_static
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698