summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/crashreporter/client
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/crashreporter/client')
-rw-r--r--toolkit/crashreporter/client/Makefile.in19
-rw-r--r--toolkit/crashreporter/client/Throbber-small.avibin0 -> 3584 bytes
-rw-r--r--toolkit/crashreporter/client/Throbber-small.gifbin0 -> 825 bytes
-rw-r--r--toolkit/crashreporter/client/crashreporter.cpp833
-rw-r--r--toolkit/crashreporter/client/crashreporter.exe.manifest42
-rw-r--r--toolkit/crashreporter/client/crashreporter.h159
-rw-r--r--toolkit/crashreporter/client/crashreporter.icobin0 -> 25214 bytes
-rwxr-xr-xtoolkit/crashreporter/client/crashreporter.rc143
-rw-r--r--toolkit/crashreporter/client/crashreporter_gtk_common.cpp361
-rw-r--r--toolkit/crashreporter/client/crashreporter_gtk_common.h50
-rw-r--r--toolkit/crashreporter/client/crashreporter_linux.cpp525
-rw-r--r--toolkit/crashreporter/client/crashreporter_osx.h107
-rw-r--r--toolkit/crashreporter/client/crashreporter_osx.mm762
-rw-r--r--toolkit/crashreporter/client/crashreporter_unix_common.cpp139
-rw-r--r--toolkit/crashreporter/client/crashreporter_win.cpp1295
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Info.plist36
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/PkgInfo2
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in8
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib100
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib18
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nibbin0 -> 25518 bytes
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib100
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib18
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nibbin0 -> 27032 bytes
-rw-r--r--toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icnsbin0 -> 61743 bytes
-rw-r--r--toolkit/crashreporter/client/moz.build96
-rw-r--r--toolkit/crashreporter/client/ping.cpp324
-rw-r--r--toolkit/crashreporter/client/resource.h35
28 files changed, 5172 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/Makefile.in b/toolkit/crashreporter/client/Makefile.in
new file mode 100644
index 0000000000..a3110090b2
--- /dev/null
+++ b/toolkit/crashreporter/client/Makefile.in
@@ -0,0 +1,19 @@
+# vim:set ts=8 sw=8 sts=8 noet:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ifeq ($(OS_ARCH),WINNT)
+MOZ_WINCONSOLE = 0
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+ifeq ($(OS_ARCH),Darwin)
+libs::
+ $(NSINSTALL) -D $(DIST)/bin/crashreporter.app
+ rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/crashreporter.app
+ $(call py_action,preprocessor,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/crashreporter.app/Contents/Resources/English.lproj/InfoPlist.strings)
+ $(NSINSTALL) -D $(DIST)/bin/crashreporter.app/Contents/MacOS
+ $(NSINSTALL) $(DIST)/bin/crashreporter $(DIST)/bin/crashreporter.app/Contents/MacOS
+endif
diff --git a/toolkit/crashreporter/client/Throbber-small.avi b/toolkit/crashreporter/client/Throbber-small.avi
new file mode 100644
index 0000000000..640ea62c0e
--- /dev/null
+++ b/toolkit/crashreporter/client/Throbber-small.avi
Binary files differ
diff --git a/toolkit/crashreporter/client/Throbber-small.gif b/toolkit/crashreporter/client/Throbber-small.gif
new file mode 100644
index 0000000000..cce32f20f4
--- /dev/null
+++ b/toolkit/crashreporter/client/Throbber-small.gif
Binary files differ
diff --git a/toolkit/crashreporter/client/crashreporter.cpp b/toolkit/crashreporter/client/crashreporter.cpp
new file mode 100644
index 0000000000..4426e6446c
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter.cpp
@@ -0,0 +1,833 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "crashreporter.h"
+
+#ifdef _MSC_VER
+// Disable exception handler warnings.
+# pragma warning(disable : 4530)
+#endif
+
+#include <fstream>
+#include <iomanip>
+#include <sstream>
+#include <memory>
+#include <ctime>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+#include <utility>
+
+#ifdef XP_LINUX
+# include <dlfcn.h>
+#endif
+
+#include "json/json.h"
+#include "nss.h"
+#include "sechash.h"
+
+using std::ifstream;
+using std::ios;
+using std::istream;
+using std::istringstream;
+using std::ofstream;
+using std::ostream;
+using std::ostringstream;
+using std::string;
+using std::unique_ptr;
+using std::vector;
+
+namespace CrashReporter {
+
+StringTable gStrings;
+Json::Value gData;
+string gSettingsPath;
+string gEventsPath;
+string gPingPath;
+int gArgc;
+char** gArgv;
+bool gAutoSubmit;
+
+enum SubmissionResult { Succeeded, Failed };
+
+static unique_ptr<ofstream> gLogStream(nullptr);
+static string gReporterDumpFile;
+static string gExtraFile;
+static string gMemoryFile;
+
+static const char kExtraDataExtension[] = ".extra";
+static const char kMemoryReportExtension[] = ".memory.json.gz";
+
+void UIError(const string& message) {
+ if (gAutoSubmit) {
+ return;
+ }
+
+ string errorMessage;
+ if (!gStrings[ST_CRASHREPORTERERROR].empty()) {
+ char buf[2048];
+ snprintf(buf, 2048, gStrings[ST_CRASHREPORTERERROR].c_str(),
+ message.c_str());
+ errorMessage = buf;
+ } else {
+ errorMessage = message;
+ }
+
+ UIError_impl(errorMessage);
+}
+
+static string Unescape(const string& str) {
+ string ret;
+ for (string::const_iterator iter = str.begin(); iter != str.end(); iter++) {
+ if (*iter == '\\') {
+ iter++;
+ if (*iter == '\\') {
+ ret.push_back('\\');
+ } else if (*iter == 'n') {
+ ret.push_back('\n');
+ } else if (*iter == 't') {
+ ret.push_back('\t');
+ }
+ } else {
+ ret.push_back(*iter);
+ }
+ }
+
+ return ret;
+}
+
+bool ReadStrings(istream& in, StringTable& strings, bool unescape) {
+ string currentSection;
+ while (!in.eof()) {
+ string line;
+ std::getline(in, line);
+ int sep = line.find('=');
+ if (sep >= 0) {
+ string key, value;
+ key = line.substr(0, sep);
+ value = line.substr(sep + 1);
+ if (unescape) value = Unescape(value);
+ strings[key] = value;
+ }
+ }
+
+ return true;
+}
+
+bool ReadStringsFromFile(const string& path, StringTable& strings,
+ bool unescape) {
+ ifstream* f = UIOpenRead(path, ios::in);
+ bool success = false;
+ if (f->is_open()) {
+ success = ReadStrings(*f, strings, unescape);
+ f->close();
+ }
+
+ delete f;
+ return success;
+}
+
+static bool ReadExtraFile(const string& aExtraDataPath, Json::Value& aExtra) {
+ bool success = false;
+ ifstream* f = UIOpenRead(aExtraDataPath, ios::in);
+ if (f->is_open()) {
+ Json::CharReaderBuilder builder;
+ success = parseFromStream(builder, *f, &aExtra, nullptr);
+ }
+
+ delete f;
+ return success;
+}
+
+static string Basename(const string& file) {
+ string::size_type slashIndex = file.rfind(UI_DIR_SEPARATOR);
+ if (slashIndex != string::npos) {
+ return file.substr(slashIndex + 1);
+ }
+ return file;
+}
+
+static bool ReadEventFile(const string& aPath, string& aEventVersion,
+ string& aTime, string& aUuid, Json::Value& aData) {
+ bool res = false;
+ ifstream* f = UIOpenRead(aPath, ios::binary);
+
+ if (f->is_open()) {
+ std::getline(*f, aEventVersion, '\n');
+ res = f->good();
+ std::getline(*f, aTime, '\n');
+ res &= f->good();
+ std::getline(*f, aUuid, '\n');
+ res &= f->good();
+
+ if (res) {
+ Json::CharReaderBuilder builder;
+ res = parseFromStream(builder, *f, &aData, nullptr);
+ }
+ }
+
+ delete f;
+ return res;
+}
+
+static void OverwriteEventFile(const string& aPath, const string& aEventVersion,
+ const string& aTime, const string& aUuid,
+ const Json::Value& aData) {
+ ofstream* f = UIOpenWrite(aPath, ios::binary | ios::trunc);
+ if (f->is_open()) {
+ f->write(aEventVersion.c_str(), aEventVersion.length()) << '\n';
+ f->write(aTime.c_str(), aTime.length()) << '\n';
+ f->write(aUuid.c_str(), aUuid.length()) << '\n';
+
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
+ writer->write(aData, f);
+ *f << "\n";
+ }
+
+ delete f;
+}
+
+static void UpdateEventFile(const Json::Value& aExtraData, const string& aHash,
+ const string& aPingUuid) {
+ if (gEventsPath.empty()) {
+ // If there is no path for finding the crash event, skip this step.
+ return;
+ }
+
+ string localId = CrashReporter::GetDumpLocalID();
+ string path = gEventsPath + UI_DIR_SEPARATOR + localId;
+ string eventVersion;
+ string crashTime;
+ string crashUuid;
+ Json::Value eventData;
+
+ if (!ReadEventFile(path, eventVersion, crashTime, crashUuid, eventData)) {
+ return;
+ }
+
+ if (!aHash.empty()) {
+ eventData["MinidumpSha256Hash"] = aHash;
+ }
+
+ if (!aPingUuid.empty()) {
+ eventData["CrashPingUUID"] = aPingUuid;
+ }
+
+ if (aExtraData.isMember("StackTraces")) {
+ eventData["StackTraces"] = aExtraData["StackTraces"];
+ }
+
+ OverwriteEventFile(path, eventVersion, crashTime, crashUuid, eventData);
+}
+
+static void WriteSubmissionEvent(SubmissionResult result,
+ const string& remoteId) {
+ if (gEventsPath.empty()) {
+ // If there is no path for writing the submission event, skip it.
+ return;
+ }
+
+ string localId = CrashReporter::GetDumpLocalID();
+ string fpath = gEventsPath + UI_DIR_SEPARATOR + localId + "-submission";
+ ofstream* f = UIOpenWrite(fpath, ios::binary);
+ time_t tm;
+ time(&tm);
+
+ if (f->is_open()) {
+ *f << "crash.submission.1\n";
+ *f << tm << "\n";
+ *f << localId << "\n";
+ *f << (result == Succeeded ? "true" : "false") << "\n";
+ *f << remoteId;
+
+ f->close();
+ }
+
+ delete f;
+}
+
+void LogMessage(const std::string& message) {
+ if (gLogStream.get()) {
+ char date[64];
+ time_t tm;
+ time(&tm);
+ if (strftime(date, sizeof(date) - 1, "%c", localtime(&tm)) == 0)
+ date[0] = '\0';
+ (*gLogStream) << "[" << date << "] " << message << std::endl;
+ }
+}
+
+static void OpenLogFile() {
+ string logPath = gSettingsPath + UI_DIR_SEPARATOR + "submit.log";
+ gLogStream.reset(UIOpenWrite(logPath, ios::app));
+}
+
+static bool ReadConfig() {
+ string iniPath;
+ if (!UIGetIniPath(iniPath)) {
+ return false;
+ }
+
+ if (!ReadStringsFromFile(iniPath, gStrings, true)) return false;
+
+ // See if we have a string override file, if so process it
+ char* overrideEnv = getenv("MOZ_CRASHREPORTER_STRINGS_OVERRIDE");
+ if (overrideEnv && *overrideEnv && UIFileExists(overrideEnv))
+ ReadStringsFromFile(overrideEnv, gStrings, true);
+
+ return true;
+}
+
+static string GetAdditionalFilename(const string& dumpfile,
+ const char* extension) {
+ string filename(dumpfile);
+ int dot = filename.rfind('.');
+ if (dot < 0) return "";
+
+ filename.replace(dot, filename.length() - dot, extension);
+ return filename;
+}
+
+static bool MoveCrashData(const string& toDir, string& dumpfile,
+ string& extrafile, string& memoryfile) {
+ if (!UIEnsurePathExists(toDir)) {
+ UIError(gStrings[ST_ERROR_CREATEDUMPDIR]);
+ return false;
+ }
+
+ string newDump = toDir + UI_DIR_SEPARATOR + Basename(dumpfile);
+ string newExtra = toDir + UI_DIR_SEPARATOR + Basename(extrafile);
+ string newMemory = toDir + UI_DIR_SEPARATOR + Basename(memoryfile);
+
+ if (!UIMoveFile(dumpfile, newDump)) {
+ UIError(gStrings[ST_ERROR_DUMPFILEMOVE]);
+ return false;
+ }
+
+ if (!UIMoveFile(extrafile, newExtra)) {
+ UIError(gStrings[ST_ERROR_EXTRAFILEMOVE]);
+ return false;
+ }
+
+ if (!memoryfile.empty()) {
+ // Ignore errors from moving the memory file
+ if (!UIMoveFile(memoryfile, newMemory)) {
+ UIDeleteFile(memoryfile);
+ newMemory.erase();
+ }
+ memoryfile = newMemory;
+ }
+
+ dumpfile = newDump;
+ extrafile = newExtra;
+
+ return true;
+}
+
+static bool AddSubmittedReport(const string& serverResponse) {
+ StringTable responseItems;
+ istringstream in(serverResponse);
+ ReadStrings(in, responseItems, false);
+
+ if (responseItems.find("StopSendingReportsFor") != responseItems.end()) {
+ // server wants to tell us to stop sending reports for a certain version
+ string reportPath = gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" +
+ responseItems["StopSendingReportsFor"];
+
+ ofstream* reportFile = UIOpenWrite(reportPath, ios::trunc);
+ if (reportFile->is_open()) {
+ // don't really care about the contents
+ *reportFile << 1 << "\n";
+ reportFile->close();
+ }
+ delete reportFile;
+ }
+
+ if (responseItems.find("Discarded") != responseItems.end()) {
+ // server discarded this report... save it so the user can resubmit it
+ // manually
+ return false;
+ }
+
+ if (responseItems.find("CrashID") == responseItems.end()) return false;
+
+ string submittedDir = gSettingsPath + UI_DIR_SEPARATOR + "submitted";
+ if (!UIEnsurePathExists(submittedDir)) {
+ return false;
+ }
+
+ string path =
+ submittedDir + UI_DIR_SEPARATOR + responseItems["CrashID"] + ".txt";
+
+ ofstream* file = UIOpenWrite(path, ios::trunc);
+ if (!file->is_open()) {
+ delete file;
+ return false;
+ }
+
+ char buf[1024];
+ snprintf(buf, 1024, gStrings["CrashID"].c_str(),
+ responseItems["CrashID"].c_str());
+ *file << buf << "\n";
+
+ if (responseItems.find("ViewURL") != responseItems.end()) {
+ snprintf(buf, 1024, gStrings["CrashDetailsURL"].c_str(),
+ responseItems["ViewURL"].c_str());
+ *file << buf << "\n";
+ }
+
+ file->close();
+ delete file;
+
+ WriteSubmissionEvent(Succeeded, responseItems["CrashID"]);
+ return true;
+}
+
+void DeleteDump() {
+ const char* noDelete = getenv("MOZ_CRASHREPORTER_NO_DELETE_DUMP");
+ if (!noDelete || *noDelete == '\0') {
+ if (!gReporterDumpFile.empty()) UIDeleteFile(gReporterDumpFile);
+ if (!gExtraFile.empty()) UIDeleteFile(gExtraFile);
+ if (!gMemoryFile.empty()) UIDeleteFile(gMemoryFile);
+ }
+}
+
+void SendCompleted(bool success, const string& serverResponse) {
+ if (success) {
+ if (AddSubmittedReport(serverResponse)) {
+ DeleteDump();
+ } else {
+ string directory = gReporterDumpFile;
+ int slashpos = directory.find_last_of("/\\");
+ if (slashpos < 2) return;
+ directory.resize(slashpos);
+ UIPruneSavedDumps(directory);
+ WriteSubmissionEvent(Failed, "");
+ }
+ } else {
+ WriteSubmissionEvent(Failed, "");
+ }
+}
+
+static string ComputeDumpHash() {
+#ifdef XP_LINUX
+ // On Linux we rely on the system-provided libcurl which uses nss so we have
+ // to also use the system-provided nss instead of the ones we have bundled.
+ const char* libnssNames[] = {
+ "libnss3.so",
+# ifndef HAVE_64BIT_BUILD
+ // 32-bit versions on 64-bit hosts
+ "/usr/lib32/libnss3.so",
+# endif
+ };
+ void* lib = nullptr;
+
+ for (const char* libname : libnssNames) {
+ lib = dlopen(libname, RTLD_NOW);
+
+ if (lib) {
+ break;
+ }
+ }
+
+ if (!lib) {
+ return "";
+ }
+
+ SECStatus (*NSS_Initialize)(const char*, const char*, const char*,
+ const char*, PRUint32);
+ HASHContext* (*HASH_Create)(HASH_HashType);
+ void (*HASH_Destroy)(HASHContext*);
+ void (*HASH_Begin)(HASHContext*);
+ void (*HASH_Update)(HASHContext*, const unsigned char*, unsigned int);
+ void (*HASH_End)(HASHContext*, unsigned char*, unsigned int*, unsigned int);
+
+ *(void**)(&NSS_Initialize) = dlsym(lib, "NSS_Initialize");
+ *(void**)(&HASH_Create) = dlsym(lib, "HASH_Create");
+ *(void**)(&HASH_Destroy) = dlsym(lib, "HASH_Destroy");
+ *(void**)(&HASH_Begin) = dlsym(lib, "HASH_Begin");
+ *(void**)(&HASH_Update) = dlsym(lib, "HASH_Update");
+ *(void**)(&HASH_End) = dlsym(lib, "HASH_End");
+
+ if (!HASH_Create || !HASH_Destroy || !HASH_Begin || !HASH_Update ||
+ !HASH_End) {
+ return "";
+ }
+#endif
+ // Minimal NSS initialization so we can use the hash functions
+ const PRUint32 kNssFlags = NSS_INIT_READONLY | NSS_INIT_NOROOTINIT |
+ NSS_INIT_NOMODDB | NSS_INIT_NOCERTDB;
+ if (NSS_Initialize(nullptr, "", "", "", kNssFlags) != SECSuccess) {
+ return "";
+ }
+
+ HASHContext* hashContext = HASH_Create(HASH_AlgSHA256);
+
+ if (!hashContext) {
+ return "";
+ }
+
+ HASH_Begin(hashContext);
+
+ ifstream* f = UIOpenRead(gReporterDumpFile, ios::binary);
+ bool error = false;
+
+ // Read the minidump contents
+ if (f->is_open()) {
+ uint8_t buff[4096];
+
+ do {
+ f->read((char*)buff, sizeof(buff));
+
+ if (f->bad()) {
+ error = true;
+ break;
+ }
+
+ HASH_Update(hashContext, buff, f->gcount());
+ } while (!f->eof());
+
+ f->close();
+ } else {
+ error = true;
+ }
+
+ delete f;
+
+ // Finalize the hash computation
+ uint8_t result[SHA256_LENGTH];
+ uint32_t resultLen = 0;
+
+ HASH_End(hashContext, result, &resultLen, SHA256_LENGTH);
+
+ if (resultLen != SHA256_LENGTH) {
+ error = true;
+ }
+
+ HASH_Destroy(hashContext);
+
+ if (!error) {
+ ostringstream hash;
+
+ for (size_t i = 0; i < SHA256_LENGTH; i++) {
+ hash << std::setw(2) << std::setfill('0') << std::hex
+ << static_cast<unsigned int>(result[i]);
+ }
+
+ return hash.str();
+ }
+ return ""; // If we encountered an error, return an empty hash
+}
+
+string GetDumpLocalID() {
+ string localId = Basename(gReporterDumpFile);
+ string::size_type dot = localId.rfind('.');
+
+ if (dot == string::npos) return "";
+
+ return localId.substr(0, dot);
+}
+
+string GetProgramPath(const string& exename) {
+ string path = gArgv[0];
+ size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX);
+ path.erase(pos);
+#ifdef XP_MACOSX
+ // On macOS the crash reporter client is shipped as an application bundle
+ // contained within Firefox' main application bundle. So when it's invoked
+ // its current working directory looks like:
+ // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/
+ // The other applications we ship with Firefox are stored in the main bundle
+ // (Firefox.app/Contents/MacOS/) so we we need to go back three directories
+ // to reach them.
+ path.append("../../../");
+#endif // XP_MACOSX
+ path.append(exename + BIN_SUFFIX);
+
+ return path;
+}
+
+} // namespace CrashReporter
+
+using namespace CrashReporter;
+
+Json::Value kEmptyJsonString("");
+
+void RewriteStrings(Json::Value& aExtraData) {
+ // rewrite some UI strings with the values from the query parameters
+ string product = aExtraData.get("ProductName", kEmptyJsonString).asString();
+ Json::Value mozilla("Mozilla");
+ string vendor = aExtraData.get("Vendor", mozilla).asString();
+
+ char buf[4096];
+ snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERVENDORTITLE].c_str(),
+ vendor.c_str());
+ gStrings[ST_CRASHREPORTERTITLE] = buf;
+
+ string str = gStrings[ST_CRASHREPORTERPRODUCTERROR];
+ // Only do the replacement here if the string has two
+ // format specifiers to start. Otherwise
+ // we assume it has the product name hardcoded.
+ string::size_type pos = str.find("%s");
+ if (pos != string::npos) pos = str.find("%s", pos + 2);
+ if (pos != string::npos) {
+ // Leave a format specifier for UIError to fill in
+ snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERPRODUCTERROR].c_str(),
+ product.c_str(), "%s");
+ gStrings[ST_CRASHREPORTERERROR] = buf;
+ } else {
+ // product name is hardcoded
+ gStrings[ST_CRASHREPORTERERROR] = str;
+ }
+
+ snprintf(buf, sizeof(buf), gStrings[ST_CRASHREPORTERDESCRIPTION].c_str(),
+ product.c_str());
+ gStrings[ST_CRASHREPORTERDESCRIPTION] = buf;
+
+ snprintf(buf, sizeof(buf), gStrings[ST_CHECKSUBMIT].c_str(), vendor.c_str());
+ gStrings[ST_CHECKSUBMIT] = buf;
+
+ snprintf(buf, sizeof(buf), gStrings[ST_RESTART].c_str(), product.c_str());
+ gStrings[ST_RESTART] = buf;
+
+ snprintf(buf, sizeof(buf), gStrings[ST_QUIT].c_str(), product.c_str());
+ gStrings[ST_QUIT] = buf;
+
+ snprintf(buf, sizeof(buf), gStrings[ST_ERROR_ENDOFLIFE].c_str(),
+ product.c_str());
+ gStrings[ST_ERROR_ENDOFLIFE] = buf;
+}
+
+bool CheckEndOfLifed(const Json::Value& aVersion) {
+ if (!aVersion.isString()) {
+ return false;
+ }
+
+ string reportPath =
+ gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + aVersion.asString();
+ return UIFileExists(reportPath);
+}
+
+int main(int argc, char** argv) {
+ gArgc = argc;
+ gArgv = argv;
+
+ string autoSubmitEnv = UIGetEnv("MOZ_CRASHREPORTER_AUTO_SUBMIT");
+ gAutoSubmit = !autoSubmitEnv.empty();
+
+ if (!ReadConfig()) {
+ UIError("Couldn't read configuration.");
+ return 0;
+ }
+
+ if (!UIInit()) {
+ return 0;
+ }
+
+ if (argc > 1) {
+ gReporterDumpFile = argv[1];
+ }
+
+ if (gReporterDumpFile.empty()) {
+ // no dump file specified, run the default UI
+ if (!gAutoSubmit) {
+ UIShowDefaultUI();
+ }
+ } else {
+ // Start by running minidump analyzer to gather stack traces.
+ string reporterDumpFile = gReporterDumpFile;
+ vector<string> args = {reporterDumpFile};
+ string dumpAllThreadsEnv = UIGetEnv("MOZ_CRASHREPORTER_DUMP_ALL_THREADS");
+ if (!dumpAllThreadsEnv.empty()) {
+ args.insert(args.begin(), "--full");
+ }
+ UIRunProgram(CrashReporter::GetProgramPath(UI_MINIDUMP_ANALYZER_FILENAME),
+ args,
+ /* wait */ true);
+
+ // go ahead with the crash reporter
+ gExtraFile = GetAdditionalFilename(gReporterDumpFile, kExtraDataExtension);
+ if (gExtraFile.empty()) {
+ UIError(gStrings[ST_ERROR_BADARGUMENTS]);
+ return 0;
+ }
+
+ if (!UIFileExists(gExtraFile)) {
+ UIError(gStrings[ST_ERROR_EXTRAFILEEXISTS]);
+ return 0;
+ }
+
+ gMemoryFile =
+ GetAdditionalFilename(gReporterDumpFile, kMemoryReportExtension);
+ if (!UIFileExists(gMemoryFile)) {
+ gMemoryFile.erase();
+ }
+
+ Json::Value extraData;
+ if (!ReadExtraFile(gExtraFile, extraData)) {
+ UIError(gStrings[ST_ERROR_EXTRAFILEREAD]);
+ return 0;
+ }
+
+ if (!extraData.isMember("ProductName")) {
+ UIError(gStrings[ST_ERROR_NOPRODUCTNAME]);
+ return 0;
+ }
+
+ // There is enough information in the extra file to rewrite strings
+ // to be product specific
+ RewriteStrings(extraData);
+
+ if (!extraData.isMember("ServerURL")) {
+ UIError(gStrings[ST_ERROR_NOSERVERURL]);
+ return 0;
+ }
+
+ // Hopefully the settings path exists in the environment. Try that before
+ // asking the platform-specific code to guess.
+ gSettingsPath = UIGetEnv("MOZ_CRASHREPORTER_DATA_DIRECTORY");
+ if (gSettingsPath.empty()) {
+ string product =
+ extraData.get("ProductName", kEmptyJsonString).asString();
+ string vendor = extraData.get("Vendor", kEmptyJsonString).asString();
+
+ if (!UIGetSettingsPath(vendor, product, gSettingsPath)) {
+ gSettingsPath.clear();
+ }
+ }
+
+ if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) {
+ UIError(gStrings[ST_ERROR_NOSETTINGSPATH]);
+ return 0;
+ }
+
+ OpenLogFile();
+
+ gEventsPath = UIGetEnv("MOZ_CRASHREPORTER_EVENTS_DIRECTORY");
+ gPingPath = UIGetEnv("MOZ_CRASHREPORTER_PING_DIRECTORY");
+
+ // Assemble and send the crash ping
+ string hash = ComputeDumpHash();
+
+ string pingUuid;
+ SendCrashPing(extraData, hash, pingUuid, gPingPath);
+ UpdateEventFile(extraData, hash, pingUuid);
+
+ if (!UIFileExists(gReporterDumpFile)) {
+ UIError(gStrings[ST_ERROR_DUMPFILEEXISTS]);
+ return 0;
+ }
+
+ string pendingDir = gSettingsPath + UI_DIR_SEPARATOR + "pending";
+ if (!MoveCrashData(pendingDir, gReporterDumpFile, gExtraFile,
+ gMemoryFile)) {
+ return 0;
+ }
+
+ string sendURL = extraData.get("ServerURL", kEmptyJsonString).asString();
+ // we don't need to actually send these
+ extraData.removeMember("ServerURL");
+ extraData.removeMember("StackTraces");
+
+ extraData["SubmittedFrom"] = "Client";
+ extraData["Throttleable"] = "1";
+
+ // re-set XUL_APP_FILE for xulrunner wrapped apps
+ const char* appfile = getenv("MOZ_CRASHREPORTER_RESTART_XUL_APP_FILE");
+ if (appfile && *appfile) {
+ const char prefix[] = "XUL_APP_FILE=";
+ char* env = (char*)malloc(strlen(appfile) + strlen(prefix) + 1);
+ if (!env) {
+ UIError("Out of memory");
+ return 0;
+ }
+ strcpy(env, prefix);
+ strcat(env, appfile);
+ putenv(env);
+ free(env);
+ }
+
+ vector<string> restartArgs;
+
+ ostringstream paramName;
+ int i = 0;
+ paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++;
+ const char* param = getenv(paramName.str().c_str());
+ while (param && *param) {
+ restartArgs.push_back(param);
+
+ paramName.str("");
+ paramName << "MOZ_CRASHREPORTER_RESTART_ARG_" << i++;
+ param = getenv(paramName.str().c_str());
+ }
+
+ // allow override of the server url via environment variable
+ // XXX: remove this in the far future when our robot
+ // masters force everyone to use XULRunner
+ char* urlEnv = getenv("MOZ_CRASHREPORTER_URL");
+ if (urlEnv && *urlEnv) {
+ sendURL = urlEnv;
+ }
+
+ // see if this version has been end-of-lifed
+
+ if (extraData.isMember("Version") &&
+ CheckEndOfLifed(extraData["Version"])) {
+ UIError(gStrings[ST_ERROR_ENDOFLIFE]);
+ DeleteDump();
+ return 0;
+ }
+
+ StringTable files;
+ files["upload_file_minidump"] = gReporterDumpFile;
+ if (!gMemoryFile.empty()) {
+ files["memory_report"] = gMemoryFile;
+ }
+
+ if (!UIShowCrashUI(files, extraData, sendURL, restartArgs)) {
+ DeleteDump();
+ }
+ }
+
+ UIShutdown();
+
+ return 0;
+}
+
+#if defined(XP_WIN) && !defined(__GNUC__)
+# include <windows.h>
+
+// We need WinMain in order to not be a console app. This function is unused
+// if we are a console application.
+int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR args, int) {
+ // Remove everything except close window from the context menu
+ {
+ HKEY hkApp;
+ RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0,
+ nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr,
+ &hkApp, nullptr);
+ RegCloseKey(hkApp);
+ if (RegCreateKeyExW(HKEY_CURRENT_USER,
+ L"Software\\Classes\\Applications\\crashreporter.exe",
+ 0, nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr,
+ &hkApp, nullptr) == ERROR_SUCCESS) {
+ RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0);
+ RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0);
+ RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0);
+ RegCloseKey(hkApp);
+ }
+ }
+
+ char** argv = static_cast<char**>(malloc(__argc * sizeof(char*)));
+ for (int i = 0; i < __argc; i++) {
+ argv[i] = strdup(WideToUTF8(__wargv[i]).c_str());
+ }
+
+ // Do the real work.
+ return main(__argc, argv);
+}
+#endif
diff --git a/toolkit/crashreporter/client/crashreporter.exe.manifest b/toolkit/crashreporter/client/crashreporter.exe.manifest
new file mode 100644
index 0000000000..81aa1465c6
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter.exe.manifest
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+<assemblyIdentity
+ version="1.0.0.0"
+ processorArchitecture="*"
+ name="CrashReporter"
+ type="win32"
+/>
+<description>Crash Reporter</description>
+<dependency>
+ <dependentAssembly>
+ <assemblyIdentity
+ type="win32"
+ name="Microsoft.Windows.Common-Controls"
+ version="6.0.0.0"
+ processorArchitecture="*"
+ publicKeyToken="6595b64144ccf1df"
+ language="*"
+ />
+ </dependentAssembly>
+</dependency>
+<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:security>
+ <ms_asmv3:requestedPrivileges>
+ <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
+ </ms_asmv3:requestedPrivileges>
+ </ms_asmv3:security>
+</ms_asmv3:trustInfo>
+ <ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+ <gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>
+ </ms_asmv3:windowsSettings>
+ </ms_asmv3:application>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/toolkit/crashreporter/client/crashreporter.h b/toolkit/crashreporter/client/crashreporter.h
new file mode 100644
index 0000000000..d0ac7a8626
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter.h
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CRASHREPORTER_H__
+#define CRASHREPORTER_H__
+
+#ifdef _MSC_VER
+# pragma warning(push)
+// Disable exception handler warnings.
+# pragma warning(disable : 4530)
+#endif
+
+#include <string>
+#include <map>
+#include <vector>
+#include <stdlib.h>
+#include <stdio.h>
+#include <iostream>
+#include <fstream>
+
+#define MAX_COMMENT_LENGTH 10000
+
+#if defined(XP_WIN)
+
+# include <windows.h>
+
+# define UI_DIR_SEPARATOR "\\"
+
+std::string WideToUTF8(const std::wstring& wide, bool* success = 0);
+
+#else
+
+# define UI_DIR_SEPARATOR "/"
+
+#endif
+
+#include "json/json.h"
+
+#define UI_CRASH_REPORTER_FILENAME "crashreporter"
+#define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer"
+#define UI_PING_SENDER_FILENAME "pingsender"
+
+typedef std::map<std::string, std::string> StringTable;
+
+#define ST_CRASHREPORTERTITLE "CrashReporterTitle"
+#define ST_CRASHREPORTERVENDORTITLE "CrashReporterVendorTitle"
+#define ST_CRASHREPORTERERROR "CrashReporterErrorText"
+#define ST_CRASHREPORTERPRODUCTERROR "CrashReporterProductErrorText2"
+#define ST_CRASHREPORTERHEADER "CrashReporterSorry"
+#define ST_CRASHREPORTERDESCRIPTION "CrashReporterDescriptionText2"
+#define ST_CRASHREPORTERDEFAULT "CrashReporterDefault"
+#define ST_VIEWREPORT "Details"
+#define ST_VIEWREPORTTITLE "ViewReportTitle"
+#define ST_COMMENTGRAYTEXT "CommentGrayText"
+#define ST_EXTRAREPORTINFO "ExtraReportInfo"
+#define ST_CHECKSUBMIT "CheckSendReport"
+#define ST_CHECKURL "CheckIncludeURL"
+#define ST_REPORTPRESUBMIT "ReportPreSubmit2"
+#define ST_REPORTDURINGSUBMIT "ReportDuringSubmit2"
+#define ST_REPORTSUBMITSUCCESS "ReportSubmitSuccess"
+#define ST_SUBMITFAILED "ReportSubmitFailed"
+#define ST_QUIT "Quit2"
+#define ST_RESTART "Restart"
+#define ST_OK "Ok"
+#define ST_CLOSE "Close"
+
+#define ST_ERROR_BADARGUMENTS "ErrorBadArguments"
+#define ST_ERROR_EXTRAFILEEXISTS "ErrorExtraFileExists"
+#define ST_ERROR_EXTRAFILEREAD "ErrorExtraFileRead"
+#define ST_ERROR_EXTRAFILEMOVE "ErrorExtraFileMove"
+#define ST_ERROR_DUMPFILEEXISTS "ErrorDumpFileExists"
+#define ST_ERROR_DUMPFILEMOVE "ErrorDumpFileMove"
+#define ST_ERROR_NOPRODUCTNAME "ErrorNoProductName"
+#define ST_ERROR_NOSERVERURL "ErrorNoServerURL"
+#define ST_ERROR_NOSETTINGSPATH "ErrorNoSettingsPath"
+#define ST_ERROR_CREATEDUMPDIR "ErrorCreateDumpDir"
+#define ST_ERROR_ENDOFLIFE "ErrorEndOfLife"
+
+//=============================================================================
+// implemented in crashreporter.cpp and ping.cpp
+//=============================================================================
+
+namespace CrashReporter {
+extern StringTable gStrings;
+extern std::string gSettingsPath;
+extern std::string gEventsPath;
+extern int gArgc;
+extern char** gArgv;
+extern bool gAutoSubmit;
+
+void UIError(const std::string& message);
+
+// The UI finished sending the report
+void SendCompleted(bool success, const std::string& serverResponse);
+
+bool ReadStrings(std::istream& in, StringTable& strings, bool unescape);
+bool ReadStringsFromFile(const std::string& path, StringTable& strings,
+ bool unescape);
+void LogMessage(const std::string& message);
+void DeleteDump();
+
+std::string GetDumpLocalID();
+std::string GetProgramPath(const std::string& exename);
+
+// Telemetry ping
+bool SendCrashPing(Json::Value& extra, const std::string& hash,
+ std::string& pingUuid, const std::string& pingDir);
+
+static const unsigned int kSaveCount = 10;
+} // namespace CrashReporter
+
+//=============================================================================
+// implemented in the platform-specific files
+//=============================================================================
+
+bool UIInit();
+void UIShutdown();
+
+// Run the UI for when the app was launched without a dump file
+void UIShowDefaultUI();
+
+// Run the UI for when the app was launched with a dump file
+// Return true if the user sent (or tried to send) the crash report,
+// false if they chose not to, and it should be deleted.
+bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
+ const std::string& sendURL,
+ const std::vector<std::string>& restartArgs);
+
+void UIError_impl(const std::string& message);
+
+bool UIGetIniPath(std::string& path);
+bool UIGetSettingsPath(const std::string& vendor, const std::string& product,
+ std::string& settingsPath);
+bool UIEnsurePathExists(const std::string& path);
+bool UIFileExists(const std::string& path);
+bool UIMoveFile(const std::string& oldfile, const std::string& newfile);
+bool UIDeleteFile(const std::string& oldfile);
+std::ifstream* UIOpenRead(const std::string& filename,
+ std::ios_base::openmode mode);
+std::ofstream* UIOpenWrite(const std::string& filename,
+ std::ios_base::openmode mode);
+void UIPruneSavedDumps(const std::string& directory);
+
+// Run the program specified by exename, passing it the parameters in arg.
+// If wait is true, wait for the program to terminate execution before
+// returning. Returns true if the program was launched correctly, false
+// otherwise.
+bool UIRunProgram(const std::string& exename,
+ const std::vector<std::string>& args, bool wait = false);
+
+// Read the environment variable specified by name
+std::string UIGetEnv(const std::string& name);
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#endif
+
+#endif
diff --git a/toolkit/crashreporter/client/crashreporter.ico b/toolkit/crashreporter/client/crashreporter.ico
new file mode 100644
index 0000000000..29ac3c6189
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter.ico
Binary files differ
diff --git a/toolkit/crashreporter/client/crashreporter.rc b/toolkit/crashreporter/client/crashreporter.rc
new file mode 100755
index 0000000000..f6042bf2e5
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter.rc
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Microsoft Visual C++ generated resource script.
+//
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winresrc.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (U.S.) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+#ifdef _WIN32
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+#endif //_WIN32
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winresrc.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_MAINICON ICON "crashreporter.ico"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// AVI
+//
+
+IDR_THROBBER AVI "Throbber-small.avi"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Dialog
+//
+
+IDD_SENDDIALOG DIALOGEX 0, 0, 241, 187
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
+EXSTYLE WS_EX_APPWINDOW
+CAPTION "Sending Crash Report..."
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ CONTROL "",IDC_DESCRIPTIONTEXT,"RICHEDIT50W",ES_MULTILINE | ES_READONLY,8,7,226,12,WS_EX_TRANSPARENT
+ CONTROL "tell mozilla about this crash so they can fix it",IDC_SUBMITREPORTCHECK,
+ "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,25,222,10
+ CHECKBOX "details...",IDC_VIEWREPORTBUTTON,24,40,54,14,BS_PUSHLIKE
+ EDITTEXT IDC_COMMENTTEXT,24,59,210,43,ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL
+ CONTROL "include the address of the page i was on",IDC_INCLUDEURLCHECK,
+ "Button",BS_AUTOCHECKBOX | WS_TABSTOP,24,107,210,10
+ CONTROL "",IDC_THROBBER,"SysAnimate32",ACS_TRANSPARENT | NOT WS_VISIBLE | WS_TABSTOP,4,152,16,16
+ LTEXT "your crash report will be submitted when you restart",IDC_PROGRESSTEXT,24,152,210,10,SS_NOPREFIX
+ DEFPUSHBUTTON "restart firefox",IDC_RESTARTBUTTON,84,166,68,14
+ PUSHBUTTON "quit without sending",IDC_CLOSEBUTTON,157,166,77,14
+END
+
+IDD_VIEWREPORTDIALOG DIALOGEX 0, 0, 208, 126
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION
+CAPTION "view report"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ CONTROL "",IDC_VIEWREPORTTEXT,"RICHEDIT50W",ES_MULTILINE | ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP,7,7,194,92
+ DEFPUSHBUTTON "OK",IDOK,151,105,50,14
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+ IDD_SENDDIALOG, DIALOG
+ BEGIN
+ LEFTMARGIN, 8
+ RIGHTMARGIN, 234
+ TOPMARGIN, 7
+ BOTTOMMARGIN, 180
+ END
+
+ IDD_VIEWREPORTDIALOG, DIALOG
+ BEGIN
+ LEFTMARGIN, 7
+ RIGHTMARGIN, 201
+ TOPMARGIN, 7
+ BOTTOMMARGIN, 119
+ END
+END
+#endif // APSTUDIO_INVOKED
+
+#endif // English (U.S.) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.cpp b/toolkit/crashreporter/client/crashreporter_gtk_common.cpp
new file mode 100644
index 0000000000..d4bed0209c
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_gtk_common.cpp
@@ -0,0 +1,361 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "crashreporter.h"
+
+#include <unistd.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#include "common/linux/http_upload.h"
+#include "crashreporter.h"
+#include "crashreporter_gtk_common.h"
+
+#ifndef GDK_KEY_Escape
+# define GDK_KEY_Escape GDK_Escape
+#endif
+
+using std::string;
+using std::vector;
+
+using namespace CrashReporter;
+
+GtkWidget* gWindow = 0;
+GtkWidget* gSubmitReportCheck = 0;
+GtkWidget* gIncludeURLCheck = 0;
+GtkWidget* gThrobber = 0;
+GtkWidget* gProgressLabel = 0;
+GtkWidget* gCloseButton = 0;
+GtkWidget* gRestartButton = 0;
+
+bool gInitialized = false;
+bool gDidTrySend = false;
+StringTable gFiles;
+Json::Value gQueryParameters;
+string gHttpProxy;
+string gAuth;
+string gCACertificateFile;
+string gSendURL;
+string gURLParameter;
+vector<string> gRestartArgs;
+GThread* gSendThreadID;
+
+// From crashreporter_linux.cpp
+void SendReport();
+void DisableGUIAndSendReport();
+void TryInitGnome();
+void UpdateSubmit();
+
+static bool RestartApplication() {
+ char** argv = reinterpret_cast<char**>(
+ malloc(sizeof(char*) * (gRestartArgs.size() + 1)));
+
+ if (!argv) return false;
+
+ unsigned int i;
+ for (i = 0; i < gRestartArgs.size(); i++) {
+ argv[i] = (char*)gRestartArgs[i].c_str();
+ }
+ argv[i] = 0;
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ free(argv);
+ return false;
+ } else if (pid == 0) {
+ (void)execv(argv[0], argv);
+ _exit(1);
+ }
+
+ free(argv);
+
+ return true;
+}
+
+// Quit the app, used as a timeout callback
+gboolean CloseApp(gpointer data) {
+ if (!gAutoSubmit) {
+ gtk_main_quit();
+ }
+ g_thread_join(gSendThreadID);
+ return FALSE;
+}
+
+static gboolean ReportCompleted(gpointer success) {
+ gtk_widget_hide(gThrobber);
+ string str =
+ success ? gStrings[ST_REPORTSUBMITSUCCESS] : gStrings[ST_SUBMITFAILED];
+ gtk_label_set_text(GTK_LABEL(gProgressLabel), str.c_str());
+ g_timeout_add(5000, CloseApp, 0);
+ return FALSE;
+}
+
+#define HTTP_PROXY_DIR "/system/http_proxy"
+
+void LoadProxyinfo() {
+ class GConfClient;
+ typedef GConfClient* (*_gconf_default_fn)();
+ typedef gboolean (*_gconf_bool_fn)(GConfClient*, const gchar*, GError**);
+ typedef gint (*_gconf_int_fn)(GConfClient*, const gchar*, GError**);
+ typedef gchar* (*_gconf_string_fn)(GConfClient*, const gchar*, GError**);
+
+ if (getenv("http_proxy"))
+ return; // libcurl can use the value from the environment
+
+ static void* gconfLib = dlopen("libgconf-2.so.4", RTLD_LAZY);
+ if (!gconfLib) return;
+
+ _gconf_default_fn gconf_client_get_default =
+ (_gconf_default_fn)dlsym(gconfLib, "gconf_client_get_default");
+ _gconf_bool_fn gconf_client_get_bool =
+ (_gconf_bool_fn)dlsym(gconfLib, "gconf_client_get_bool");
+ _gconf_int_fn gconf_client_get_int =
+ (_gconf_int_fn)dlsym(gconfLib, "gconf_client_get_int");
+ _gconf_string_fn gconf_client_get_string =
+ (_gconf_string_fn)dlsym(gconfLib, "gconf_client_get_string");
+
+ if (!(gconf_client_get_default && gconf_client_get_bool &&
+ gconf_client_get_int && gconf_client_get_string)) {
+ dlclose(gconfLib);
+ return;
+ }
+
+ GConfClient* conf = gconf_client_get_default();
+
+ if (gconf_client_get_bool(conf, HTTP_PROXY_DIR "/use_http_proxy", nullptr)) {
+ gint port;
+ gchar *host = nullptr, *httpproxy = nullptr;
+
+ host = gconf_client_get_string(conf, HTTP_PROXY_DIR "/host", nullptr);
+ port = gconf_client_get_int(conf, HTTP_PROXY_DIR "/port", nullptr);
+
+ if (port && host && *host != '\0') {
+ httpproxy = g_strdup_printf("http://%s:%d/", host, port);
+ gHttpProxy = httpproxy;
+ }
+
+ g_free(host);
+ g_free(httpproxy);
+
+ if (gconf_client_get_bool(conf, HTTP_PROXY_DIR "/use_authentication",
+ nullptr)) {
+ gchar *user, *password, *auth = nullptr;
+
+ user = gconf_client_get_string(
+ conf, HTTP_PROXY_DIR "/authentication_user", nullptr);
+ password = gconf_client_get_string(
+ conf, HTTP_PROXY_DIR "/authentication_password", nullptr);
+
+ if (user && password) {
+ auth = g_strdup_printf("%s:%s", user, password);
+ gAuth = auth;
+ }
+
+ g_free(user);
+ g_free(password);
+ g_free(auth);
+ }
+ }
+
+ g_object_unref(conf);
+
+ // Don't dlclose gconfLib as libORBit-2 uses atexit().
+}
+
+gpointer SendThread(gpointer args) {
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ string parameters(writeString(builder, gQueryParameters));
+
+ string response, error;
+ long response_code;
+
+ bool success = google_breakpad::HTTPUpload::SendRequest(
+ gSendURL, parameters, gFiles, gHttpProxy, gAuth, gCACertificateFile,
+ &response, &response_code, &error);
+ if (success) {
+ LogMessage("Crash report submitted successfully");
+ } else {
+ LogMessage("Crash report submission failed: " + error);
+ }
+
+ SendCompleted(success, response);
+
+ if (!gAutoSubmit) {
+ // Apparently glib is threadsafe, and will schedule this
+ // on the main thread, see:
+ // http://library.gnome.org/devel/gtk-faq/stable/x499.html
+ g_idle_add(ReportCompleted, (gpointer)success);
+ }
+
+ return nullptr;
+}
+
+gboolean WindowDeleted(GtkWidget* window, GdkEvent* event, gpointer userData) {
+ SaveSettings();
+ gtk_main_quit();
+ return TRUE;
+}
+
+gboolean check_escape(GtkWidget* window, GdkEventKey* event,
+ gpointer userData) {
+ if (event->keyval == GDK_KEY_Escape) {
+ gtk_main_quit();
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void MaybeSubmitReport() {
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck))) {
+ gDidTrySend = true;
+ DisableGUIAndSendReport();
+ } else {
+ gtk_main_quit();
+ }
+}
+
+void CloseClicked(GtkButton* button, gpointer userData) {
+ SaveSettings();
+ MaybeSubmitReport();
+}
+
+void RestartClicked(GtkButton* button, gpointer userData) {
+ SaveSettings();
+ RestartApplication();
+ MaybeSubmitReport();
+}
+
+static void UpdateURL() {
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck))) {
+ gQueryParameters["URL"] = gURLParameter;
+ } else {
+ gQueryParameters.removeMember("URL");
+ }
+}
+
+void SubmitReportChecked(GtkButton* sender, gpointer userData) {
+ UpdateSubmit();
+}
+
+void IncludeURLClicked(GtkButton* sender, gpointer userData) { UpdateURL(); }
+
+/* === Crashreporter UI Functions === */
+
+bool UIInit() {
+ // breakpad probably left us with blocked signals, unblock them here
+ sigset_t signals, old;
+ sigfillset(&signals);
+ sigprocmask(SIG_UNBLOCK, &signals, &old);
+
+ // tell glib we're going to use threads
+ g_thread_init(nullptr);
+
+ if (gtk_init_check(&gArgc, &gArgv)) {
+ gInitialized = true;
+
+ if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes")
+ gtk_widget_set_default_direction(GTK_TEXT_DIR_RTL);
+
+ return true;
+ }
+
+ return false;
+}
+
+void UIShowDefaultUI() {
+ GtkWidget* errorDialog = gtk_message_dialog_new(
+ nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
+ gStrings[ST_CRASHREPORTERDEFAULT].c_str());
+
+ gtk_window_set_title(GTK_WINDOW(errorDialog),
+ gStrings[ST_CRASHREPORTERTITLE].c_str());
+ gtk_dialog_run(GTK_DIALOG(errorDialog));
+}
+
+void UIError_impl(const string& message) {
+ if (!gInitialized) {
+ // Didn't initialize, this is the best we can do
+ printf("Error: %s\n", message.c_str());
+ return;
+ }
+
+ GtkWidget* errorDialog =
+ gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE, "%s", message.c_str());
+
+ gtk_window_set_title(GTK_WINDOW(errorDialog),
+ gStrings[ST_CRASHREPORTERTITLE].c_str());
+ gtk_dialog_run(GTK_DIALOG(errorDialog));
+}
+
+bool UIGetIniPath(string& path) {
+ path = gArgv[0];
+ path.append(".ini");
+
+ return true;
+}
+
+/*
+ * Settings are stored in ~/.vendor/product, or
+ * ~/.product if vendor is empty.
+ */
+bool UIGetSettingsPath(const string& vendor, const string& product,
+ string& settingsPath) {
+ char* home = getenv("HOME");
+
+ if (!home) return false;
+
+ settingsPath = home;
+ settingsPath += "/.";
+ if (!vendor.empty()) {
+ string lc_vendor;
+ std::transform(vendor.begin(), vendor.end(), back_inserter(lc_vendor),
+ (int (*)(int))std::tolower);
+ settingsPath += lc_vendor + "/";
+ }
+ string lc_product;
+ std::transform(product.begin(), product.end(), back_inserter(lc_product),
+ (int (*)(int))std::tolower);
+ settingsPath += lc_product + "/Crash Reports";
+ return true;
+}
+
+bool UIMoveFile(const string& file, const string& newfile) {
+ if (!rename(file.c_str(), newfile.c_str())) return true;
+ if (errno != EXDEV) return false;
+
+ // use system /bin/mv instead, time to fork
+ pid_t pID = vfork();
+ if (pID < 0) {
+ // Failed to fork
+ return false;
+ }
+ if (pID == 0) {
+ char* const args[4] = {const_cast<char*>("mv"), strdup(file.c_str()),
+ strdup(newfile.c_str()), 0};
+ if (args[1] && args[2]) execve("/bin/mv", args, 0);
+ free(args[1]);
+ free(args[2]);
+ exit(-1);
+ }
+ int status;
+ waitpid(pID, &status, 0);
+ return UIFileExists(newfile);
+}
diff --git a/toolkit/crashreporter/client/crashreporter_gtk_common.h b/toolkit/crashreporter/client/crashreporter_gtk_common.h
new file mode 100644
index 0000000000..208c7ba6b0
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_gtk_common.h
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CRASHREPORTER_GTK_COMMON_H__
+#define CRASHREPORTER_GTK_COMMON_H__
+
+#include <glib.h>
+#include <gtk/gtk.h>
+
+#include <string>
+#include <vector>
+
+#include "json/json.h"
+
+const char kIniFile[] = "crashreporter.ini";
+
+extern GtkWidget* gWindow;
+extern GtkWidget* gSubmitReportCheck;
+extern GtkWidget* gIncludeURLCheck;
+extern GtkWidget* gThrobber;
+extern GtkWidget* gProgressLabel;
+extern GtkWidget* gCloseButton;
+extern GtkWidget* gRestartButton;
+
+extern std::vector<std::string> gRestartArgs;
+extern GThread* gSendThreadID;
+
+extern bool gInitialized;
+extern bool gDidTrySend;
+extern StringTable gFiles;
+extern Json::Value gQueryParameters;
+extern std::string gHttpProxy;
+extern std::string gAuth;
+extern std::string gCACertificateFile;
+extern std::string gSendURL;
+extern std::string gURLParameter;
+
+void LoadProxyinfo();
+gboolean CloseApp(gpointer data);
+gpointer SendThread(gpointer args);
+gboolean WindowDeleted(GtkWidget* window, GdkEvent* event, gpointer userData);
+gboolean check_escape(GtkWidget* window, GdkEventKey* event, gpointer data);
+void SubmitReportChecked(GtkButton* sender, gpointer userData);
+void IncludeURLClicked(GtkButton* sender, gpointer userData);
+void CloseClicked(GtkButton* button, gpointer userData);
+void RestartClicked(GtkButton* button, gpointer userData);
+void SaveSettings(void);
+
+#endif // CRASHREPORTER_GTK_COMMON_H__
diff --git a/toolkit/crashreporter/client/crashreporter_linux.cpp b/toolkit/crashreporter/client/crashreporter_linux.cpp
new file mode 100644
index 0000000000..d11f10a472
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_linux.cpp
@@ -0,0 +1,525 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <dlfcn.h>
+#include <fcntl.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <string.h>
+
+#include <cctype>
+
+#include "crashreporter.h"
+#include "crashreporter_gtk_common.h"
+
+#define LABEL_MAX_CHAR_WIDTH 48
+
+using std::ios;
+using std::string;
+using std::vector;
+
+using namespace CrashReporter;
+
+static GtkWidget* gViewReportButton = 0;
+static GtkWidget* gCommentTextLabel = 0;
+static GtkWidget* gCommentText = 0;
+
+static bool gCommentFieldHint = true;
+
+// handle from dlopen'ing libgnome
+static void* gnomeLib = nullptr;
+// handle from dlopen'ing libgnomeui
+static void* gnomeuiLib = nullptr;
+
+static void LoadSettings() {
+ /*
+ * NOTE! This code needs to stay in sync with the preference checking
+ * code in in nsExceptionHandler.cpp.
+ */
+
+ bool includeURL = true;
+ bool submitReport = true;
+ StringTable settings;
+ if (ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true)) {
+ if (settings.find("IncludeURL") != settings.end()) {
+ includeURL = settings["IncludeURL"][0] != '0';
+ }
+ if (settings.find("SubmitReport") != settings.end()) {
+ submitReport = settings["SubmitReport"][0] != '0';
+ }
+ }
+
+ if (gIncludeURLCheck) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck),
+ includeURL);
+ }
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck),
+ submitReport);
+}
+
+static string Escape(const string& str) {
+ string ret;
+ for (auto c : str) {
+ if (c == '\\') {
+ ret += "\\\\";
+ } else if (c == '\n') {
+ ret += "\\n";
+ } else if (c == '\t') {
+ ret += "\\t";
+ } else {
+ ret.push_back(c);
+ }
+ }
+
+ return ret;
+}
+
+static bool WriteStrings(std::ostream& out, const string& header,
+ StringTable& strings, bool escape) {
+ out << "[" << header << "]" << std::endl;
+ for (const auto& iter : strings) {
+ out << iter.first << "=";
+ if (escape) {
+ out << Escape(iter.second);
+ } else {
+ out << iter.second;
+ }
+
+ out << std::endl;
+ }
+
+ return true;
+}
+
+static bool WriteStringsToFile(const string& path, const string& header,
+ StringTable& strings, bool escape) {
+ std::ofstream* f = UIOpenWrite(path, ios::trunc);
+ bool success = false;
+ if (f->is_open()) {
+ success = WriteStrings(*f, header, strings, escape);
+ f->close();
+ }
+
+ delete f;
+ return success;
+}
+
+void SaveSettings() {
+ /*
+ * NOTE! This code needs to stay in sync with the preference setting
+ * code in in nsExceptionHandler.cpp.
+ */
+
+ StringTable settings;
+
+ ReadStringsFromFile(gSettingsPath + "/" + kIniFile, settings, true);
+ if (gIncludeURLCheck != 0)
+ settings["IncludeURL"] =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck)) ? "1"
+ : "0";
+ settings["SubmitReport"] =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck)) ? "1"
+ : "0";
+
+ WriteStringsToFile(gSettingsPath + "/" + kIniFile, "Crash Reporter", settings,
+ true);
+}
+
+void SendReport() {
+ LoadProxyinfo();
+
+ // spawn a thread to do the sending
+ gSendThreadID = g_thread_create(SendThread, nullptr, TRUE, nullptr);
+}
+
+void DisableGUIAndSendReport() {
+ // disable all our gui controls, show the throbber + change the progress text
+ gtk_widget_set_sensitive(gSubmitReportCheck, FALSE);
+ gtk_widget_set_sensitive(gViewReportButton, FALSE);
+ gtk_widget_set_sensitive(gCommentText, FALSE);
+ if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, FALSE);
+ gtk_widget_set_sensitive(gCloseButton, FALSE);
+ if (gRestartButton) gtk_widget_set_sensitive(gRestartButton, FALSE);
+ gtk_widget_show_all(gThrobber);
+ gtk_label_set_text(GTK_LABEL(gProgressLabel),
+ gStrings[ST_REPORTDURINGSUBMIT].c_str());
+
+ SendReport();
+}
+
+static void ShowReportInfo(GtkTextView* viewReportTextView) {
+ GtkTextBuffer* buffer = gtk_text_view_get_buffer(viewReportTextView);
+
+ GtkTextIter start, end;
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_end_iter(buffer, &end);
+
+ gtk_text_buffer_delete(buffer, &start, &end);
+
+ for (Json::ValueConstIterator iter = gQueryParameters.begin();
+ iter != gQueryParameters.end(); ++iter) {
+ gtk_text_buffer_insert(buffer, &end, iter.name().c_str(),
+ iter.name().length());
+ gtk_text_buffer_insert(buffer, &end, ": ", -1);
+ string value;
+ if (iter->isString()) {
+ value = iter->asString();
+ } else {
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ value = writeString(builder, *iter);
+ }
+ gtk_text_buffer_insert(buffer, &end, value.c_str(), value.length());
+ gtk_text_buffer_insert(buffer, &end, "\n", -1);
+ }
+
+ gtk_text_buffer_insert(buffer, &end, "\n", -1);
+ gtk_text_buffer_insert(buffer, &end, gStrings[ST_EXTRAREPORTINFO].c_str(),
+ -1);
+}
+
+void UpdateSubmit() {
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gSubmitReportCheck))) {
+ gtk_widget_set_sensitive(gViewReportButton, TRUE);
+ gtk_widget_set_sensitive(gCommentText, TRUE);
+ if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, TRUE);
+ gtk_label_set_text(GTK_LABEL(gProgressLabel),
+ gStrings[ST_REPORTPRESUBMIT].c_str());
+ } else {
+ gtk_widget_set_sensitive(gViewReportButton, FALSE);
+ gtk_widget_set_sensitive(gCommentText, FALSE);
+ if (gIncludeURLCheck) gtk_widget_set_sensitive(gIncludeURLCheck, FALSE);
+ gtk_label_set_text(GTK_LABEL(gProgressLabel), "");
+ }
+}
+
+static void ViewReportClicked(GtkButton* button, gpointer userData) {
+ GtkDialog* dialog = GTK_DIALOG(gtk_dialog_new_with_buttons(
+ gStrings[ST_VIEWREPORTTITLE].c_str(), GTK_WINDOW(gWindow),
+ GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_OK, nullptr));
+
+ GtkWidget* scrolled = gtk_scrolled_window_new(0, 0);
+ gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(dialog)),
+ scrolled);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
+ gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled),
+ GTK_SHADOW_IN);
+ gtk_widget_set_vexpand(scrolled, TRUE);
+
+ GtkWidget* viewReportTextView = gtk_text_view_new();
+ gtk_container_add(GTK_CONTAINER(scrolled), viewReportTextView);
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(viewReportTextView), FALSE);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(viewReportTextView), GTK_WRAP_WORD);
+ gtk_widget_set_size_request(GTK_WIDGET(viewReportTextView), -1, 100);
+
+ ShowReportInfo(GTK_TEXT_VIEW(viewReportTextView));
+
+ gtk_dialog_set_default_response(dialog, GTK_RESPONSE_OK);
+ gtk_widget_set_size_request(GTK_WIDGET(dialog), 400, 200);
+ gtk_widget_show_all(GTK_WIDGET(dialog));
+ gtk_dialog_run(dialog);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+}
+
+static void CommentChanged(GtkTextBuffer* buffer, gpointer userData) {
+ GtkTextIter start, end;
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_end_iter(buffer, &end);
+ const char* comment = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
+ if (comment[0] == '\0' || gCommentFieldHint) {
+ gQueryParameters.removeMember("Comments");
+ } else {
+ gQueryParameters["Comments"] = comment;
+ }
+}
+
+static void CommentInsert(GtkTextBuffer* buffer, GtkTextIter* location,
+ gchar* text, gint len, gpointer userData) {
+ GtkTextIter start, end;
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_end_iter(buffer, &end);
+ const char* comment = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
+
+ // limit to 500 bytes in utf-8
+ if (strlen(comment) + len > MAX_COMMENT_LENGTH) {
+ g_signal_stop_emission_by_name(buffer, "insert-text");
+ }
+}
+
+static void UpdateHintText(GtkWidget* widget, gboolean gainedFocus,
+ bool* hintShowing, const char* hintText) {
+ GtkTextBuffer* buffer = nullptr;
+ if (GTK_IS_TEXT_VIEW(widget))
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
+
+ if (gainedFocus) {
+ if (*hintShowing) {
+ if (buffer == nullptr) { // sort of cheating
+ gtk_entry_set_text(GTK_ENTRY(widget), "");
+ } else { // GtkTextView
+ gtk_text_buffer_set_text(buffer, "", 0);
+ }
+ gtk_widget_modify_text(widget, GTK_STATE_NORMAL, nullptr);
+ *hintShowing = false;
+ }
+ } else {
+ // lost focus
+ const char* text = nullptr;
+ if (buffer == nullptr) {
+ text = gtk_entry_get_text(GTK_ENTRY(widget));
+ } else {
+ GtkTextIter start, end;
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_end_iter(buffer, &end);
+ text = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
+ }
+
+ if (text == nullptr || text[0] == '\0') {
+ *hintShowing = true;
+
+ if (buffer == nullptr) {
+ gtk_entry_set_text(GTK_ENTRY(widget), hintText);
+ } else {
+ gtk_text_buffer_set_text(buffer, hintText, -1);
+ }
+
+ gtk_widget_modify_text(
+ widget, GTK_STATE_NORMAL,
+ &gtk_widget_get_style(widget)->text[GTK_STATE_INSENSITIVE]);
+ }
+ }
+}
+
+static gboolean CommentFocusChange(GtkWidget* widget, GdkEventFocus* event,
+ gpointer userData) {
+ UpdateHintText(widget, event->in, &gCommentFieldHint,
+ gStrings[ST_COMMENTGRAYTEXT].c_str());
+
+ return FALSE;
+}
+
+typedef struct _GnomeProgram GnomeProgram;
+typedef struct _GnomeModuleInfo GnomeModuleInfo;
+typedef GnomeProgram* (*_gnome_program_init_fn)(const char*, const char*,
+ const GnomeModuleInfo*, int,
+ char**, const char*, ...);
+typedef const GnomeModuleInfo* (*_libgnomeui_module_info_get_fn)();
+
+void TryInitGnome() {
+ gnomeLib = dlopen("libgnome-2.so.0", RTLD_LAZY);
+ if (!gnomeLib) return;
+
+ gnomeuiLib = dlopen("libgnomeui-2.so.0", RTLD_LAZY);
+ if (!gnomeuiLib) return;
+
+ _gnome_program_init_fn gnome_program_init =
+ (_gnome_program_init_fn)(dlsym(gnomeLib, "gnome_program_init"));
+ _libgnomeui_module_info_get_fn libgnomeui_module_info_get =
+ (_libgnomeui_module_info_get_fn)(dlsym(gnomeuiLib,
+ "libgnomeui_module_info_get"));
+
+ if (gnome_program_init && libgnomeui_module_info_get) {
+ gnome_program_init("crashreporter", "1.0", libgnomeui_module_info_get(),
+ gArgc, gArgv, nullptr);
+ }
+}
+
+/* === Crashreporter UI Functions === */
+
+/*
+ * Anything not listed here is in crashreporter_gtk_common.cpp:
+ * UIInit
+ * UIShowDefaultUI
+ * UIError_impl
+ * UIGetIniPath
+ * UIGetSettingsPath
+ * UIEnsurePathExists
+ * UIFileExists
+ * UIMoveFile
+ * UIDeleteFile
+ * UIOpenRead
+ * UIOpenWrite
+ */
+
+void UIShutdown() {
+ if (gnomeuiLib) dlclose(gnomeuiLib);
+ // Don't dlclose gnomeLib as libgnomevfs and libORBit-2 use atexit().
+}
+
+bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
+ const string& sendURL, const vector<string>& restartArgs) {
+ gFiles = files;
+ gQueryParameters = queryParameters;
+ gSendURL = sendURL;
+ gRestartArgs = restartArgs;
+ if (gQueryParameters.isMember("URL")) {
+ gURLParameter = gQueryParameters["URL"].asString();
+ }
+
+ if (gAutoSubmit) {
+ SendReport();
+ CloseApp(nullptr);
+ return true;
+ }
+
+ gWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(gWindow),
+ gStrings[ST_CRASHREPORTERTITLE].c_str());
+ gtk_window_set_resizable(GTK_WINDOW(gWindow), FALSE);
+ gtk_window_set_position(GTK_WINDOW(gWindow), GTK_WIN_POS_CENTER);
+ gtk_container_set_border_width(GTK_CONTAINER(gWindow), 12);
+ g_signal_connect(gWindow, "delete-event", G_CALLBACK(WindowDeleted), 0);
+ g_signal_connect(gWindow, "key_press_event", G_CALLBACK(check_escape),
+ nullptr);
+
+ GtkWidget* vbox = gtk_vbox_new(FALSE, 6);
+ gtk_container_add(GTK_CONTAINER(gWindow), vbox);
+
+ GtkWidget* titleLabel = gtk_label_new("");
+ gtk_box_pack_start(GTK_BOX(vbox), titleLabel, FALSE, FALSE, 0);
+ gtk_misc_set_alignment(GTK_MISC(titleLabel), 0, 0.5);
+ char* markup =
+ g_strdup_printf("<b>%s</b>", gStrings[ST_CRASHREPORTERHEADER].c_str());
+ gtk_label_set_markup(GTK_LABEL(titleLabel), markup);
+ g_free(markup);
+
+ GtkWidget* descriptionLabel =
+ gtk_label_new(gStrings[ST_CRASHREPORTERDESCRIPTION].c_str());
+ gtk_box_pack_start(GTK_BOX(vbox), descriptionLabel, TRUE, TRUE, 0);
+ // force the label to line wrap
+ gtk_label_set_max_width_chars(GTK_LABEL(descriptionLabel),
+ LABEL_MAX_CHAR_WIDTH);
+ gtk_label_set_line_wrap(GTK_LABEL(descriptionLabel), TRUE);
+ gtk_label_set_selectable(GTK_LABEL(descriptionLabel), TRUE);
+ gtk_misc_set_alignment(GTK_MISC(descriptionLabel), 0, 0.5);
+
+ // this is honestly how they suggest you indent a section
+ GtkWidget* indentBox = gtk_hbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), indentBox, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(indentBox), gtk_label_new(""), FALSE, FALSE, 6);
+
+ GtkWidget* innerVBox1 = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(indentBox), innerVBox1, TRUE, TRUE, 0);
+
+ gSubmitReportCheck =
+ gtk_check_button_new_with_label(gStrings[ST_CHECKSUBMIT].c_str());
+ gtk_box_pack_start(GTK_BOX(innerVBox1), gSubmitReportCheck, FALSE, FALSE, 0);
+ g_signal_connect(gSubmitReportCheck, "clicked",
+ G_CALLBACK(SubmitReportChecked), 0);
+
+ // indent again, below the "submit report" checkbox
+ GtkWidget* indentBox2 = gtk_hbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(innerVBox1), indentBox2, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(indentBox2), gtk_label_new(""), FALSE, FALSE, 6);
+
+ GtkWidget* innerVBox = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(indentBox2), innerVBox, TRUE, TRUE, 0);
+ gtk_box_set_spacing(GTK_BOX(innerVBox), 6);
+
+ GtkWidget* viewReportButtonBox = gtk_hbutton_box_new();
+ gtk_box_pack_start(GTK_BOX(innerVBox), viewReportButtonBox, FALSE, FALSE, 0);
+ gtk_box_set_spacing(GTK_BOX(viewReportButtonBox), 6);
+ gtk_button_box_set_layout(GTK_BUTTON_BOX(viewReportButtonBox),
+ GTK_BUTTONBOX_START);
+
+ gViewReportButton =
+ gtk_button_new_with_label(gStrings[ST_VIEWREPORT].c_str());
+ gtk_box_pack_start(GTK_BOX(viewReportButtonBox), gViewReportButton, FALSE,
+ FALSE, 0);
+ g_signal_connect(gViewReportButton, "clicked", G_CALLBACK(ViewReportClicked),
+ 0);
+
+ GtkWidget* scrolled = gtk_scrolled_window_new(0, 0);
+ gtk_container_add(GTK_CONTAINER(innerVBox), scrolled);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
+ GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
+ gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled),
+ GTK_SHADOW_IN);
+ gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolled),
+ 100);
+
+ gCommentTextLabel = gtk_label_new(gStrings[ST_COMMENTGRAYTEXT].c_str());
+ gCommentText = gtk_text_view_new();
+ gtk_label_set_mnemonic_widget(GTK_LABEL(gCommentTextLabel), gCommentText);
+ gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(gCommentText), FALSE);
+ g_signal_connect(gCommentText, "focus-in-event",
+ G_CALLBACK(CommentFocusChange), 0);
+ g_signal_connect(gCommentText, "focus-out-event",
+ G_CALLBACK(CommentFocusChange), 0);
+
+ GtkTextBuffer* commentBuffer =
+ gtk_text_view_get_buffer(GTK_TEXT_VIEW(gCommentText));
+ g_signal_connect(commentBuffer, "changed", G_CALLBACK(CommentChanged), 0);
+ g_signal_connect(commentBuffer, "insert-text", G_CALLBACK(CommentInsert), 0);
+
+ gtk_container_add(GTK_CONTAINER(scrolled), gCommentText);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gCommentText), GTK_WRAP_WORD_CHAR);
+ gtk_widget_set_size_request(GTK_WIDGET(gCommentText), -1, 100);
+
+ if (gQueryParameters.isMember("URL")) {
+ gIncludeURLCheck =
+ gtk_check_button_new_with_label(gStrings[ST_CHECKURL].c_str());
+ gtk_box_pack_start(GTK_BOX(innerVBox), gIncludeURLCheck, FALSE, FALSE, 0);
+ g_signal_connect(gIncludeURLCheck, "clicked", G_CALLBACK(IncludeURLClicked),
+ 0);
+ // on by default
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gIncludeURLCheck), TRUE);
+ }
+
+ GtkWidget* progressBox = gtk_hbox_new(FALSE, 6);
+ gtk_box_pack_start(GTK_BOX(vbox), progressBox, TRUE, TRUE, 0);
+
+ // Get the throbber image from alongside the executable
+ char* dir = g_path_get_dirname(gArgv[0]);
+ char* path = g_build_filename(dir, "Throbber-small.gif", nullptr);
+ g_free(dir);
+ gThrobber = gtk_image_new_from_file(path);
+ gtk_box_pack_start(GTK_BOX(progressBox), gThrobber, FALSE, FALSE, 0);
+
+ gProgressLabel = gtk_label_new(gStrings[ST_REPORTPRESUBMIT].c_str());
+ gtk_box_pack_start(GTK_BOX(progressBox), gProgressLabel, TRUE, TRUE, 0);
+ // force the label to line wrap
+ gtk_label_set_max_width_chars(GTK_LABEL(gProgressLabel),
+ LABEL_MAX_CHAR_WIDTH);
+ gtk_label_set_line_wrap(GTK_LABEL(gProgressLabel), TRUE);
+
+ GtkWidget* buttonBox = gtk_hbutton_box_new();
+ gtk_box_pack_end(GTK_BOX(vbox), buttonBox, FALSE, FALSE, 0);
+ gtk_box_set_spacing(GTK_BOX(buttonBox), 6);
+ gtk_button_box_set_layout(GTK_BUTTON_BOX(buttonBox), GTK_BUTTONBOX_END);
+
+ gCloseButton = gtk_button_new_with_label(gStrings[ST_QUIT].c_str());
+ gtk_box_pack_start(GTK_BOX(buttonBox), gCloseButton, FALSE, FALSE, 0);
+ gtk_widget_set_can_default(gCloseButton, TRUE);
+ g_signal_connect(gCloseButton, "clicked", G_CALLBACK(CloseClicked), 0);
+
+ gRestartButton = 0;
+ if (!restartArgs.empty()) {
+ gRestartButton = gtk_button_new_with_label(gStrings[ST_RESTART].c_str());
+ gtk_box_pack_start(GTK_BOX(buttonBox), gRestartButton, FALSE, FALSE, 0);
+ gtk_widget_set_can_default(gRestartButton, TRUE);
+ g_signal_connect(gRestartButton, "clicked", G_CALLBACK(RestartClicked), 0);
+ }
+
+ gtk_widget_grab_focus(gSubmitReportCheck);
+
+ gtk_widget_grab_default(gRestartButton ? gRestartButton : gCloseButton);
+
+ LoadSettings();
+
+ UpdateSubmit();
+
+ UpdateHintText(gCommentText, FALSE, &gCommentFieldHint,
+ gStrings[ST_COMMENTGRAYTEXT].c_str());
+
+ gtk_widget_show_all(gWindow);
+ // stick this here to avoid the show_all above...
+ gtk_widget_hide(gThrobber);
+
+ gtk_main();
+
+ return gDidTrySend;
+}
diff --git a/toolkit/crashreporter/client/crashreporter_osx.h b/toolkit/crashreporter/client/crashreporter_osx.h
new file mode 100644
index 0000000000..19282a3fe6
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_osx.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CRASHREPORTER_OSX_H__
+#define CRASHREPORTER_OSX_H__
+
+#include <Cocoa/Cocoa.h>
+#include "HTTPMultipartUpload.h"
+#include "crashreporter.h"
+#include "json/json.h"
+
+// Defined below
+@class TextViewWithPlaceHolder;
+
+@interface CrashReporterUI : NSObject {
+ IBOutlet NSWindow* mWindow;
+
+ /* Crash reporter view */
+ IBOutlet NSTextField* mHeaderLabel;
+ IBOutlet NSTextField* mDescriptionLabel;
+ IBOutlet NSButton* mViewReportButton;
+ IBOutlet NSScrollView* mCommentScrollView;
+ IBOutlet TextViewWithPlaceHolder* mCommentText;
+ IBOutlet NSButton* mSubmitReportButton;
+ IBOutlet NSButton* mIncludeURLButton;
+ IBOutlet NSButton* mEmailMeButton;
+ IBOutlet NSTextField* mEmailText;
+ IBOutlet NSButton* mCloseButton;
+ IBOutlet NSButton* mRestartButton;
+ IBOutlet NSProgressIndicator* mProgressIndicator;
+ IBOutlet NSTextField* mProgressText;
+
+ /* Error view */
+ IBOutlet NSView* mErrorView;
+ IBOutlet NSTextField* mErrorHeaderLabel;
+ IBOutlet NSTextField* mErrorLabel;
+ IBOutlet NSButton* mErrorCloseButton;
+
+ /* For "show info" alert */
+ IBOutlet NSWindow* mViewReportWindow;
+ IBOutlet NSTextView* mViewReportTextView;
+ IBOutlet NSButton* mViewReportOkButton;
+
+ HTTPMultipartUpload* mPost;
+}
+
+- (void)showCrashUI:(const StringTable&)files
+ queryParameters:(const Json::Value&)queryParameters
+ sendURL:(const std::string&)sendURL;
+- (void)showErrorUI:(const std::string&)message;
+- (void)showReportInfo;
+- (void)maybeSubmitReport;
+- (void)closeMeDown:(id)unused;
+
+- (IBAction)submitReportClicked:(id)sender;
+- (IBAction)viewReportClicked:(id)sender;
+- (IBAction)viewReportOkClicked:(id)sender;
+- (IBAction)closeClicked:(id)sender;
+- (IBAction)restartClicked:(id)sender;
+- (IBAction)includeURLClicked:(id)sender;
+
+- (void)textDidChange:(NSNotification*)aNotification;
+- (BOOL)textView:(NSTextView*)aTextView
+ shouldChangeTextInRange:(NSRange)affectedCharRange
+ replacementString:(NSString*)replacementString;
+
+- (void)doInitialResizing;
+- (float)setStringFitVertically:(NSControl*)control
+ string:(NSString*)str
+ resizeWindow:(BOOL)resizeWindow;
+- (void)setView:(NSView*)v animate:(BOOL)animate;
+- (void)enableControls:(BOOL)enabled;
+- (void)updateSubmit;
+- (void)updateURL;
+- (void)updateEmail;
+- (void)sendReport;
+- (bool)setupPost;
+- (void)uploadThread:(HTTPMultipartUpload*)post;
+- (void)uploadComplete:(NSData*)data;
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication;
+- (void)applicationWillTerminate:(NSNotification*)aNotification;
+
+@end
+
+/*
+ * Subclass NSTextView to provide a text view with placeholder text.
+ * Also provide a setEnabled implementation.
+ */
+@interface TextViewWithPlaceHolder : NSTextView {
+ NSMutableAttributedString* mPlaceHolderString;
+}
+
+- (BOOL)becomeFirstResponder;
+- (void)drawRect:(NSRect)rect;
+- (BOOL)resignFirstResponder;
+- (void)setPlaceholder:(NSString*)placeholder;
+- (void)insertTab:(id)sender;
+- (void)insertBacktab:(id)sender;
+- (void)setEnabled:(BOOL)enabled;
+- (void)dealloc;
+
+@end
+
+#endif
diff --git a/toolkit/crashreporter/client/crashreporter_osx.mm b/toolkit/crashreporter/client/crashreporter_osx.mm
new file mode 100644
index 0000000000..38b3349a04
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_osx.mm
@@ -0,0 +1,762 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import <CoreFoundation/CoreFoundation.h>
+#include "crashreporter.h"
+#include "crashreporter_osx.h"
+#include <crt_externs.h>
+#include <spawn.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <sstream>
+
+using std::ostringstream;
+using std::string;
+using std::vector;
+
+using namespace CrashReporter;
+
+static NSAutoreleasePool* gMainPool;
+static CrashReporterUI* gUI = 0;
+static StringTable gFiles;
+static Json::Value gQueryParameters;
+static string gURLParameter;
+static string gSendURL;
+static vector<string> gRestartArgs;
+static bool gDidTrySend = false;
+static bool gRTLlayout = false;
+
+static cpu_type_t pref_cpu_types[2] = {
+#if defined(__i386__)
+ CPU_TYPE_X86,
+#elif defined(__x86_64__)
+ CPU_TYPE_X86_64,
+#elif defined(__ppc__)
+ CPU_TYPE_POWERPC,
+#elif defined(__aarch64__)
+ CPU_TYPE_ARM64,
+#endif
+ CPU_TYPE_ANY};
+
+#define NSSTR(s) [NSString stringWithUTF8String:(s).c_str()]
+
+static NSString* Str(const char* aName) {
+ string str = gStrings[aName];
+ if (str.empty()) str = "?";
+ return NSSTR(str);
+}
+
+static bool RestartApplication() {
+ vector<char*> argv(gRestartArgs.size() + 1);
+
+ posix_spawnattr_t spawnattr;
+ if (posix_spawnattr_init(&spawnattr) != 0) {
+ return false;
+ }
+
+ // Set spawn attributes.
+ size_t attr_count = sizeof(pref_cpu_types) / sizeof(pref_cpu_types[0]);
+ size_t attr_ocount = 0;
+ if (posix_spawnattr_setbinpref_np(&spawnattr, attr_count, pref_cpu_types, &attr_ocount) != 0 ||
+ attr_ocount != attr_count) {
+ posix_spawnattr_destroy(&spawnattr);
+ return false;
+ }
+
+ unsigned int i;
+ for (i = 0; i < gRestartArgs.size(); i++) {
+ argv[i] = (char*)gRestartArgs[i].c_str();
+ }
+ argv[i] = 0;
+
+ char** env = NULL;
+ char*** nsEnv = _NSGetEnviron();
+ if (nsEnv) env = *nsEnv;
+ int result = posix_spawnp(NULL, argv[0], NULL, &spawnattr, &argv[0], env);
+
+ posix_spawnattr_destroy(&spawnattr);
+
+ return result == 0;
+}
+
+@implementation CrashReporterUI
+
+- (void)awakeFromNib {
+ gUI = self;
+ [mWindow center];
+
+ [mWindow setTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]];
+ [NSApp activateIgnoringOtherApps:YES];
+}
+
+- (void)showCrashUI:(const StringTable&)files
+ queryParameters:(const Json::Value&)queryParameters
+ sendURL:(const string&)sendURL {
+ gFiles = files;
+ gQueryParameters = queryParameters;
+ gSendURL = sendURL;
+
+ if (gAutoSubmit) {
+ gDidTrySend = true;
+ [self sendReport];
+ return;
+ }
+
+ [mWindow setTitle:Str(ST_CRASHREPORTERTITLE)];
+ [mHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
+
+ NSRect viewReportFrame = [mViewReportButton frame];
+ [mViewReportButton setTitle:Str(ST_VIEWREPORT)];
+ [mViewReportButton sizeToFit];
+ if (gRTLlayout) {
+ // sizeToFit will keep the left side fixed, so realign
+ float oldWidth = viewReportFrame.size.width;
+ viewReportFrame = [mViewReportButton frame];
+ viewReportFrame.origin.x += oldWidth - viewReportFrame.size.width;
+ [mViewReportButton setFrame:viewReportFrame];
+ }
+
+ [mSubmitReportButton setTitle:Str(ST_CHECKSUBMIT)];
+ [mIncludeURLButton setTitle:Str(ST_CHECKURL)];
+ [mViewReportOkButton setTitle:Str(ST_OK)];
+
+ [mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)];
+ if (gRTLlayout) [mCommentText toggleBaseWritingDirection:self];
+
+ if (gQueryParameters.isMember("URL")) {
+ // save the URL value in case the checkbox gets unchecked
+ gURLParameter = gQueryParameters["URL"].asString();
+ } else {
+ // no URL specified, hide checkbox
+ [mIncludeURLButton removeFromSuperview];
+ // shrink window to fit
+ NSRect frame = [mWindow frame];
+ NSRect includeURLFrame = [mIncludeURLButton frame];
+ NSRect emailFrame = [mEmailMeButton frame];
+ int buttonMask = [mViewReportButton autoresizingMask];
+ int checkMask = [mSubmitReportButton autoresizingMask];
+ int commentScrollMask = [mCommentScrollView autoresizingMask];
+
+ [mViewReportButton setAutoresizingMask:NSViewMinYMargin];
+ [mSubmitReportButton setAutoresizingMask:NSViewMinYMargin];
+ [mCommentScrollView setAutoresizingMask:NSViewMinYMargin];
+
+ // remove all the space in between
+ frame.size.height -= includeURLFrame.origin.y - emailFrame.origin.y;
+ [mWindow setFrame:frame display:true animate:NO];
+
+ [mViewReportButton setAutoresizingMask:buttonMask];
+ [mSubmitReportButton setAutoresizingMask:checkMask];
+ [mCommentScrollView setAutoresizingMask:commentScrollMask];
+ }
+
+ // resize some buttons horizontally and possibly some controls vertically
+ [self doInitialResizing];
+
+ // load default state of submit checkbox
+ // we don't just do this via IB because we want the default to be
+ // off a certain percentage of the time
+ BOOL submitChecked = YES;
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ if (nil != [userDefaults objectForKey:@"submitReport"]) {
+ submitChecked = [userDefaults boolForKey:@"submitReport"];
+ } else {
+ [userDefaults setBool:submitChecked forKey:@"submitReport"];
+ }
+ [mSubmitReportButton setState:(submitChecked ? NSOnState : NSOffState)];
+
+ // load default state of include URL checkbox
+ BOOL includeChecked = YES;
+ if (nil != [userDefaults objectForKey:@"IncludeURL"]) {
+ includeChecked = [userDefaults boolForKey:@"IncludeURL"];
+ } else {
+ [userDefaults setBool:includeChecked forKey:@"IncludeURL"];
+ }
+ [mIncludeURLButton setState:(includeChecked ? NSOnState : NSOffState)];
+
+ [self updateSubmit];
+ [self updateURL];
+ [self updateEmail];
+
+ [mWindow makeKeyAndOrderFront:nil];
+}
+
+- (void)showErrorUI:(const string&)message {
+ [self setView:mErrorView animate:NO];
+
+ [mErrorHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
+ [self setStringFitVertically:mErrorLabel string:NSSTR(message) resizeWindow:YES];
+ [mErrorCloseButton setTitle:Str(ST_OK)];
+
+ [mErrorCloseButton setKeyEquivalent:@"\r"];
+ [mWindow makeFirstResponder:mErrorCloseButton];
+ [mWindow makeKeyAndOrderFront:nil];
+}
+
+- (void)showReportInfo {
+ NSDictionary* boldAttr = @{
+ NSFontAttributeName : [NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]],
+ NSForegroundColorAttributeName : NSColor.textColor,
+ };
+ NSDictionary* normalAttr = @{
+ NSFontAttributeName : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]],
+ NSForegroundColorAttributeName : NSColor.textColor,
+ };
+
+ [mViewReportTextView setString:@""];
+ for (Json::ValueConstIterator iter = gQueryParameters.begin(); iter != gQueryParameters.end();
+ ++iter) {
+ NSAttributedString* key = [[NSAttributedString alloc] initWithString:NSSTR(iter.name() + ": ")
+ attributes:boldAttr];
+ string str;
+ if (iter->isString()) {
+ str = iter->asString();
+ } else {
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ str = writeString(builder, *iter);
+ }
+ NSAttributedString* value = [[NSAttributedString alloc] initWithString:NSSTR(str + "\n")
+ attributes:normalAttr];
+ [[mViewReportTextView textStorage] appendAttributedString:key];
+ [[mViewReportTextView textStorage] appendAttributedString:value];
+ [key release];
+ [value release];
+ }
+
+ NSAttributedString* extra =
+ [[NSAttributedString alloc] initWithString:NSSTR("\n" + gStrings[ST_EXTRAREPORTINFO])
+ attributes:normalAttr];
+ [[mViewReportTextView textStorage] appendAttributedString:extra];
+ [extra release];
+}
+
+- (void)maybeSubmitReport {
+ if ([mSubmitReportButton state] == NSOnState) {
+ [self setStringFitVertically:mProgressText string:Str(ST_REPORTDURINGSUBMIT) resizeWindow:YES];
+ // disable all the controls
+ [self enableControls:NO];
+ [mSubmitReportButton setEnabled:NO];
+ [mRestartButton setEnabled:NO];
+ [mCloseButton setEnabled:NO];
+ [mProgressIndicator startAnimation:self];
+ gDidTrySend = true;
+ [self sendReport];
+ } else {
+ [NSApp terminate:self];
+ }
+}
+
+- (void)closeMeDown:(id)unused {
+ [NSApp terminate:self];
+}
+
+- (IBAction)submitReportClicked:(id)sender {
+ [self updateSubmit];
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ [userDefaults setBool:([mSubmitReportButton state] == NSOnState) forKey:@"submitReport"];
+ [userDefaults synchronize];
+}
+
+- (IBAction)viewReportClicked:(id)sender {
+ [self showReportInfo];
+ [NSApp beginSheet:mViewReportWindow
+ modalForWindow:mWindow
+ modalDelegate:nil
+ didEndSelector:nil
+ contextInfo:nil];
+}
+
+- (IBAction)viewReportOkClicked:(id)sender {
+ [mViewReportWindow orderOut:nil];
+ [NSApp endSheet:mViewReportWindow];
+}
+
+- (IBAction)closeClicked:(id)sender {
+ [self maybeSubmitReport];
+}
+
+- (IBAction)restartClicked:(id)sender {
+ RestartApplication();
+ [self maybeSubmitReport];
+}
+
+- (IBAction)includeURLClicked:(id)sender {
+ [self updateURL];
+ NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
+ [userDefaults setBool:([mIncludeURLButton state] == NSOnState) forKey:@"IncludeURL"];
+ [userDefaults synchronize];
+}
+
+- (void)textDidChange:(NSNotification*)aNotification {
+ // update comment parameter
+ if ([[[mCommentText textStorage] mutableString] length] > 0)
+ gQueryParameters["Comments"] = [[[mCommentText textStorage] mutableString] UTF8String];
+ else
+ gQueryParameters.removeMember("Comments");
+}
+
+// Limit the comment field to 500 bytes in UTF-8
+- (BOOL)textView:(NSTextView*)aTextView
+ shouldChangeTextInRange:(NSRange)affectedCharRange
+ replacementString:(NSString*)replacementString {
+ // current string length + replacement text length - replaced range length
+ if (([[aTextView string] lengthOfBytesUsingEncoding:NSUTF8StringEncoding] +
+ [replacementString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] -
+ [[[aTextView string] substringWithRange:affectedCharRange]
+ lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) > MAX_COMMENT_LENGTH) {
+ return NO;
+ }
+ return YES;
+}
+
+- (void)doInitialResizing {
+ NSRect windowFrame = [mWindow frame];
+ NSRect restartFrame = [mRestartButton frame];
+ NSRect closeFrame = [mCloseButton frame];
+ // resize close button to fit text
+ float oldCloseWidth = closeFrame.size.width;
+ [mCloseButton setTitle:Str(ST_QUIT)];
+ [mCloseButton sizeToFit];
+ closeFrame = [mCloseButton frame];
+ // move close button left if it grew
+ if (!gRTLlayout) {
+ closeFrame.origin.x -= closeFrame.size.width - oldCloseWidth;
+ }
+
+ if (gRestartArgs.size() == 0) {
+ [mRestartButton removeFromSuperview];
+ if (!gRTLlayout) {
+ closeFrame.origin.x =
+ restartFrame.origin.x + (restartFrame.size.width - closeFrame.size.width);
+ } else {
+ closeFrame.origin.x = restartFrame.origin.x;
+ }
+ [mCloseButton setFrame:closeFrame];
+ [mCloseButton setKeyEquivalent:@"\r"];
+ } else {
+ [mRestartButton setTitle:Str(ST_RESTART)];
+ // resize "restart" button
+ float oldRestartWidth = restartFrame.size.width;
+ [mRestartButton sizeToFit];
+ restartFrame = [mRestartButton frame];
+ if (!gRTLlayout) {
+ // move left by the amount that the button grew
+ restartFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
+ closeFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
+ } else {
+ // shift the close button right in RTL
+ closeFrame.origin.x += restartFrame.size.width - oldRestartWidth;
+ }
+ [mRestartButton setFrame:restartFrame];
+ [mCloseButton setFrame:closeFrame];
+ // possibly resize window if both buttons no longer fit
+ // leave 20 px from either side of the window, and 12 px
+ // between the buttons
+ float neededWidth = closeFrame.size.width + restartFrame.size.width + 2 * 20 + 12;
+
+ if (neededWidth > windowFrame.size.width) {
+ windowFrame.size.width = neededWidth;
+ [mWindow setFrame:windowFrame display:true animate:NO];
+ }
+ [mRestartButton setKeyEquivalent:@"\r"];
+ }
+
+ NSButton* checkboxes[] = {mSubmitReportButton, mIncludeURLButton};
+
+ for (auto checkbox : checkboxes) {
+ NSRect frame = [checkbox frame];
+ [checkbox sizeToFit];
+ if (gRTLlayout) {
+ // sizeToFit will keep the left side fixed, so realign
+ float oldWidth = frame.size.width;
+ frame = [checkbox frame];
+ frame.origin.x += oldWidth - frame.size.width;
+ [checkbox setFrame:frame];
+ }
+ // keep existing spacing on left side, + 20 px spare on right
+ float neededWidth = frame.origin.x + checkbox.intrinsicContentSize.width + 20;
+ if (neededWidth > windowFrame.size.width) {
+ windowFrame.size.width = neededWidth;
+ [mWindow setFrame:windowFrame display:true animate:NO];
+ }
+ }
+
+ // do this down here because we may have made the window wider
+ // up above
+ [self setStringFitVertically:mDescriptionLabel
+ string:Str(ST_CRASHREPORTERDESCRIPTION)
+ resizeWindow:YES];
+
+ // now pin all the controls (except quit/submit) in place,
+ // if we lengthen the window after this, it's just to lengthen
+ // the progress text, so nothing above that text should move.
+ NSView* views[] = {mSubmitReportButton, mViewReportButton, mCommentScrollView,
+ mIncludeURLButton, mProgressIndicator, mProgressText};
+ for (auto view : views) {
+ [view setAutoresizingMask:NSViewMinYMargin];
+ }
+}
+
+- (float)setStringFitVertically:(NSControl*)control
+ string:(NSString*)str
+ resizeWindow:(BOOL)resizeWindow {
+ // hack to make the text field grow vertically
+ NSRect frame = [control frame];
+ float oldHeight = frame.size.height;
+
+ frame.size.height = 10000;
+ NSSize oldCellSize = [[control cell] cellSizeForBounds:frame];
+ [control setStringValue:str];
+ NSSize newCellSize = [[control cell] cellSizeForBounds:frame];
+
+ float delta = newCellSize.height - oldCellSize.height;
+ frame.origin.y -= delta;
+ frame.size.height = oldHeight + delta;
+ [control setFrame:frame];
+
+ if (resizeWindow) {
+ NSRect frame = [mWindow frame];
+ frame.origin.y -= delta;
+ frame.size.height += delta;
+ [mWindow setFrame:frame display:true animate:NO];
+ }
+
+ return delta;
+}
+
+- (void)setView:(NSView*)v animate:(BOOL)animate {
+ NSRect frame = [mWindow frame];
+
+ NSRect oldViewFrame = [[mWindow contentView] frame];
+ NSRect newViewFrame = [v frame];
+
+ frame.origin.y += oldViewFrame.size.height - newViewFrame.size.height;
+ frame.size.height -= oldViewFrame.size.height - newViewFrame.size.height;
+
+ frame.origin.x += oldViewFrame.size.width - newViewFrame.size.width;
+ frame.size.width -= oldViewFrame.size.width - newViewFrame.size.width;
+
+ [mWindow setContentView:v];
+ [mWindow setFrame:frame display:true animate:animate];
+}
+
+- (void)enableControls:(BOOL)enabled {
+ [mViewReportButton setEnabled:enabled];
+ [mIncludeURLButton setEnabled:enabled];
+ [mCommentText setEnabled:enabled];
+ [mCommentScrollView setHasVerticalScroller:enabled];
+}
+
+- (void)updateSubmit {
+ if ([mSubmitReportButton state] == NSOnState) {
+ [self setStringFitVertically:mProgressText string:Str(ST_REPORTPRESUBMIT) resizeWindow:YES];
+ [mProgressText setHidden:NO];
+ // enable all the controls
+ [self enableControls:YES];
+ } else {
+ // not submitting, disable all the controls under
+ // the submit checkbox, and hide the status text
+ [mProgressText setHidden:YES];
+ [self enableControls:NO];
+ }
+}
+
+- (void)updateURL {
+ if ([mIncludeURLButton state] == NSOnState && !gURLParameter.empty()) {
+ gQueryParameters["URL"] = gURLParameter;
+ } else {
+ gQueryParameters.removeMember("URL");
+ }
+}
+
+- (void)updateEmail {
+ // In order to remove the email fields, we have to edit the .nib files which
+ // we can't do with current xcode so we make them hidden; updating the
+ // crashreporter interface for mac is covered in bug #1696164
+ [mEmailMeButton setHidden:YES];
+ [mEmailText setHidden:YES];
+}
+
+- (void)sendReport {
+ if (![self setupPost]) {
+ LogMessage("Crash report submission failed: could not set up POST data");
+
+ if (gAutoSubmit) {
+ [NSApp terminate:self];
+ }
+
+ [self setStringFitVertically:mProgressText string:Str(ST_SUBMITFAILED) resizeWindow:YES];
+ // quit after 5 seconds
+ [self performSelector:@selector(closeMeDown:) withObject:nil afterDelay:5.0];
+ }
+
+ [NSThread detachNewThreadSelector:@selector(uploadThread:) toTarget:self withObject:mPost];
+}
+
+- (bool)setupPost {
+ NSURL* url =
+ [NSURL URLWithString:[NSSTR(gSendURL)
+ stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+ if (!url) return false;
+
+ mPost = [[HTTPMultipartUpload alloc] initWithURL:url];
+ if (!mPost) return false;
+
+ for (StringTable::const_iterator i = gFiles.begin(); i != gFiles.end(); i++) {
+ [mPost addFileAtPath:NSSTR(i->second) name:NSSTR(i->first)];
+ }
+
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ string output = writeString(builder, gQueryParameters).append("\r\n");
+ NSMutableString* parameters = [[NSMutableString alloc] initWithUTF8String:output.c_str()];
+
+ [mPost setParameters:parameters];
+ [parameters release];
+
+ return true;
+}
+
+- (void)uploadComplete:(NSData*)data {
+ NSHTTPURLResponse* response = [mPost response];
+ [mPost release];
+
+ bool success;
+ string reply;
+ if (!data || !response || [response statusCode] != 200) {
+ success = false;
+ reply = "";
+
+ // if data is nil, we probably logged an error in uploadThread
+ if (data != nil && response != nil) {
+ ostringstream message;
+ message << "Crash report submission failed: server returned status " << [response statusCode];
+ LogMessage(message.str());
+ }
+ } else {
+ success = true;
+ LogMessage("Crash report submitted successfully");
+
+ NSString* encodingName = [response textEncodingName];
+ NSStringEncoding encoding;
+ if (encodingName) {
+ encoding = CFStringConvertEncodingToNSStringEncoding(
+ CFStringConvertIANACharSetNameToEncoding((CFStringRef)encodingName));
+ } else {
+ encoding = NSISOLatin1StringEncoding;
+ }
+ NSString* r = [[NSString alloc] initWithData:data encoding:encoding];
+ reply = [r UTF8String];
+ [r release];
+ }
+
+ SendCompleted(success, reply);
+
+ if (gAutoSubmit) {
+ [NSApp terminate:self];
+ }
+
+ [mProgressIndicator stopAnimation:self];
+ if (success) {
+ [self setStringFitVertically:mProgressText string:Str(ST_REPORTSUBMITSUCCESS) resizeWindow:YES];
+ } else {
+ [self setStringFitVertically:mProgressText string:Str(ST_SUBMITFAILED) resizeWindow:YES];
+ }
+ // quit after 5 seconds
+ [self performSelector:@selector(closeMeDown:) withObject:nil afterDelay:5.0];
+}
+
+- (void)uploadThread:(HTTPMultipartUpload*)post {
+ NSAutoreleasePool* autoreleasepool = [[NSAutoreleasePool alloc] init];
+ NSError* error = nil;
+ NSData* data = [post send:&error];
+ if (error) {
+ data = nil;
+ NSString* errorDesc = [error localizedDescription];
+ string message = [errorDesc UTF8String];
+ LogMessage("Crash report submission failed: " + message);
+ }
+
+ [self performSelectorOnMainThread:@selector(uploadComplete:) withObject:data waitUntilDone:YES];
+
+ [autoreleasepool release];
+}
+
+// to get auto-quit when we close the window
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication {
+ return YES;
+}
+
+- (void)applicationWillTerminate:(NSNotification*)aNotification {
+ // since we use [NSApp terminate:] we never return to main,
+ // so do our cleanup here
+ if (!gDidTrySend) DeleteDump();
+}
+
+@end
+
+@implementation TextViewWithPlaceHolder
+
+- (BOOL)becomeFirstResponder {
+ [self setNeedsDisplay:YES];
+ return [super becomeFirstResponder];
+}
+
+- (void)drawRect:(NSRect)rect {
+ [super drawRect:rect];
+ if (mPlaceHolderString && [[self string] isEqualToString:@""] &&
+ self != [[self window] firstResponder])
+ [mPlaceHolderString drawInRect:[self frame]];
+}
+
+- (BOOL)resignFirstResponder {
+ [self setNeedsDisplay:YES];
+ return [super resignFirstResponder];
+}
+
+- (void)setPlaceholder:(NSString*)placeholder {
+ NSColor* txtColor = [NSColor disabledControlTextColor];
+ NSDictionary* txtDict =
+ [NSDictionary dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName, nil];
+ mPlaceHolderString = [[NSMutableAttributedString alloc] initWithString:placeholder
+ attributes:txtDict];
+ if (gRTLlayout)
+ [mPlaceHolderString setAlignment:NSTextAlignmentRight
+ range:NSMakeRange(0, [placeholder length])];
+}
+
+- (void)insertTab:(id)sender {
+ // don't actually want to insert tabs, just tab to next control
+ [[self window] selectNextKeyView:sender];
+}
+
+- (void)insertBacktab:(id)sender {
+ [[self window] selectPreviousKeyView:sender];
+}
+
+- (void)setEnabled:(BOOL)enabled {
+ [self setSelectable:enabled];
+ [self setEditable:enabled];
+ if (![[self string] isEqualToString:@""]) {
+ NSAttributedString* colorString;
+ NSColor* txtColor;
+ if (enabled)
+ txtColor = [NSColor textColor];
+ else
+ txtColor = [NSColor disabledControlTextColor];
+ NSDictionary* txtDict =
+ [NSDictionary dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName, nil];
+ colorString = [[NSAttributedString alloc] initWithString:[self string] attributes:txtDict];
+ [[self textStorage] setAttributedString:colorString];
+ [self setInsertionPointColor:txtColor];
+ [colorString release];
+ }
+}
+
+- (void)dealloc {
+ [mPlaceHolderString release];
+ [super dealloc];
+}
+
+@end
+
+/* === Crashreporter UI Functions === */
+
+bool UIInit() {
+ gMainPool = [[NSAutoreleasePool alloc] init];
+ [NSApplication sharedApplication];
+
+ if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes") gRTLlayout = true;
+
+ if (gAutoSubmit) {
+ gUI = [[CrashReporterUI alloc] init];
+ } else {
+ [[NSBundle mainBundle] loadNibNamed:(gRTLlayout ? @"MainMenuRTL" : @"MainMenu")
+ owner:NSApp
+ topLevelObjects:nil];
+ }
+
+ return true;
+}
+
+void UIShutdown() { [gMainPool release]; }
+
+void UIShowDefaultUI() {
+ [gUI showErrorUI:gStrings[ST_CRASHREPORTERDEFAULT]];
+ [NSApp run];
+}
+
+bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
+ const string& sendURL, const vector<string>& restartArgs) {
+ gRestartArgs = restartArgs;
+
+ [gUI showCrashUI:files queryParameters:queryParameters sendURL:sendURL];
+ [NSApp run];
+
+ return gDidTrySend;
+}
+
+void UIError_impl(const string& message) {
+ if (!gUI) {
+ // UI failed to initialize, printing is the best we can do
+ printf("Error: %s\n", message.c_str());
+ return;
+ }
+
+ [gUI showErrorUI:message];
+ [NSApp run];
+}
+
+bool UIGetIniPath(string& path) {
+ NSString* tmpPath = [NSString stringWithUTF8String:gArgv[0]];
+ NSString* iniName = [tmpPath lastPathComponent];
+ iniName = [iniName stringByAppendingPathExtension:@"ini"];
+ tmpPath = [tmpPath stringByDeletingLastPathComponent];
+ tmpPath = [tmpPath stringByDeletingLastPathComponent];
+ tmpPath = [tmpPath stringByAppendingPathComponent:@"Resources"];
+ tmpPath = [tmpPath stringByAppendingPathComponent:iniName];
+ path = [tmpPath UTF8String];
+ return true;
+}
+
+bool UIGetSettingsPath(const string& vendor, const string& product, string& settingsPath) {
+ NSArray* paths =
+ NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+ NSString* destPath = [paths firstObject];
+
+ // Note that MacOS ignores the vendor when creating the profile hierarchy -
+ // all application preferences directories live alongside one another in
+ // ~/Library/Application Support/
+ destPath = [destPath stringByAppendingPathComponent:NSSTR(product)];
+ // Thunderbird stores its profile in ~/Library/Thunderbird,
+ // but we're going to put stuff in ~/Library/Application Support/Thunderbird
+ // anyway, so we have to ensure that path exists.
+ string tempPath = [destPath UTF8String];
+ if (!UIEnsurePathExists(tempPath)) return false;
+
+ destPath = [destPath stringByAppendingPathComponent:@"Crash Reports"];
+
+ settingsPath = [destPath UTF8String];
+
+ return true;
+}
+
+bool UIMoveFile(const string& file, const string& newfile) {
+ if (!rename(file.c_str(), newfile.c_str())) return true;
+ if (errno != EXDEV) return false;
+
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ NSString* source = [fileManager stringWithFileSystemRepresentation:file.c_str()
+ length:file.length()];
+ NSString* dest = [fileManager stringWithFileSystemRepresentation:newfile.c_str()
+ length:newfile.length()];
+ if (!source || !dest) return false;
+
+ [fileManager moveItemAtPath:source toPath:dest error:NULL];
+ return UIFileExists(newfile);
+}
diff --git a/toolkit/crashreporter/client/crashreporter_unix_common.cpp b/toolkit/crashreporter/client/crashreporter_unix_common.cpp
new file mode 100644
index 0000000000..0cecc6262a
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_unix_common.cpp
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "crashreporter.h"
+
+#include <algorithm>
+#include <sys/wait.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+using namespace CrashReporter;
+using std::ios_base;
+using std::sort;
+using std::string;
+using std::vector;
+
+struct FileData {
+ time_t timestamp;
+ string path;
+};
+
+static bool CompareFDTime(const FileData& fd1, const FileData& fd2) {
+ return fd1.timestamp > fd2.timestamp;
+}
+
+void UIPruneSavedDumps(const string& directory) {
+ DIR* dirfd = opendir(directory.c_str());
+ if (!dirfd) return;
+
+ vector<FileData> dumpfiles;
+
+ while (dirent* dir = readdir(dirfd)) {
+ FileData fd;
+ fd.path = directory + '/' + dir->d_name;
+ if (fd.path.size() < 5) continue;
+
+ if (fd.path.compare(fd.path.size() - 4, 4, ".dmp") != 0) continue;
+
+ struct stat st;
+ if (stat(fd.path.c_str(), &st)) {
+ closedir(dirfd);
+ return;
+ }
+
+ fd.timestamp = st.st_mtime;
+
+ dumpfiles.push_back(fd);
+ }
+
+ closedir(dirfd);
+
+ sort(dumpfiles.begin(), dumpfiles.end(), CompareFDTime);
+
+ while (dumpfiles.size() > kSaveCount) {
+ // get the path of the oldest file
+ string path = dumpfiles[dumpfiles.size() - 1].path;
+ UIDeleteFile(path.c_str());
+
+ // s/.dmp/.extra/
+ path.replace(path.size() - 4, 4, ".extra");
+ UIDeleteFile(path.c_str());
+
+ dumpfiles.pop_back();
+ }
+}
+
+bool UIRunProgram(const string& exename, const vector<string>& args,
+ bool wait) {
+ pid_t pid = fork();
+
+ if (pid == -1) {
+ return false;
+ } else if (pid == 0) {
+ // Child
+ size_t argvLen = args.size() + 2;
+ vector<char*> argv(argvLen);
+
+ argv[0] = const_cast<char*>(exename.c_str());
+
+ for (size_t i = 0; i < args.size(); i++) {
+ argv[i + 1] = const_cast<char*>(args[i].c_str());
+ }
+
+ argv[argvLen - 1] = nullptr;
+
+ // Run the program
+ int rv = execv(exename.c_str(), argv.data());
+
+ if (rv == -1) {
+ exit(EXIT_FAILURE);
+ }
+ } else {
+ // Parent
+ if (wait) {
+ waitpid(pid, nullptr, 0);
+ }
+ }
+
+ return true;
+}
+
+bool UIEnsurePathExists(const string& path) {
+ int ret = mkdir(path.c_str(), S_IRWXU);
+ int e = errno;
+ if (ret == -1 && e != EEXIST) return false;
+
+ return true;
+}
+
+bool UIFileExists(const string& path) {
+ struct stat sb;
+ int ret = stat(path.c_str(), &sb);
+ if (ret == -1 || !(sb.st_mode & S_IFREG)) return false;
+
+ return true;
+}
+
+bool UIDeleteFile(const string& file) { return (unlink(file.c_str()) != -1); }
+
+std::ifstream* UIOpenRead(const string& filename, ios_base::openmode mode) {
+ return new std::ifstream(filename.c_str(), mode);
+}
+
+std::ofstream* UIOpenWrite(const string& filename, ios_base::openmode mode) {
+ return new std::ofstream(filename.c_str(), mode);
+}
+
+string UIGetEnv(const string& name) {
+ const char* var = getenv(name.c_str());
+ if (var && *var) {
+ return var;
+ }
+
+ return "";
+}
diff --git a/toolkit/crashreporter/client/crashreporter_win.cpp b/toolkit/crashreporter/client/crashreporter_win.cpp
new file mode 100644
index 0000000000..e680aa37c9
--- /dev/null
+++ b/toolkit/crashreporter/client/crashreporter_win.cpp
@@ -0,0 +1,1295 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef WIN32_LEAN_AND_MEAN
+# undef WIN32_LEAN_AND_MEAN
+#endif
+
+#include "crashreporter.h"
+
+#include <windows.h>
+#include <versionhelpers.h>
+#include <commctrl.h>
+#include <richedit.h>
+#include <shellapi.h>
+#include <shlobj.h>
+#include <shlwapi.h>
+#include <math.h>
+#include <set>
+#include <algorithm>
+#include "resource.h"
+#include "windows/sender/crash_report_sender.h"
+#include "common/windows/string_utils-inl.h"
+
+#define SUBMIT_REPORT_VALUE L"SubmitCrashReport"
+#define INCLUDE_URL_VALUE L"IncludeURL"
+
+#define SENDURL_ORIGINAL L"https://crash-reports.mozilla.com/submit"
+#define SENDURL_XPSP2 L"https://crash-reports-xpsp2.mozilla.com/submit"
+
+#define WM_UPLOADCOMPLETE WM_APP
+
+// Thanks, Windows.h :(
+#undef min
+#undef max
+
+using std::ifstream;
+using std::ios;
+using std::ios_base;
+using std::map;
+using std::ofstream;
+using std::set;
+using std::string;
+using std::vector;
+using std::wstring;
+
+using namespace CrashReporter;
+
+typedef struct {
+ HWND hDlg;
+ Json::Value queryParameters;
+ map<wstring, wstring> files;
+ wstring sendURL;
+
+ wstring serverResponse;
+} SendThreadData;
+
+/*
+ * Per http://msdn2.microsoft.com/en-us/library/ms645398(VS.85).aspx
+ * "The DLGTEMPLATEEX structure is not defined in any standard header file.
+ * The structure definition is provided here to explain the format of an
+ * extended template for a dialog box.
+ */
+typedef struct {
+ WORD dlgVer;
+ WORD signature;
+ DWORD helpID;
+ DWORD exStyle;
+ // There's more to this struct, but it has weird variable-length
+ // members, and I only actually need to touch exStyle on an existing
+ // instance, so I've omitted the rest.
+} DLGTEMPLATEEX;
+
+static HANDLE gThreadHandle;
+static SendThreadData gSendData = {
+ 0,
+};
+static vector<string> gRestartArgs;
+static Json::Value gQueryParameters;
+static wstring gCrashReporterKey(L"Software\\Mozilla\\Crash Reporter");
+static string gURLParameter;
+static int gCheckboxPadding = 6;
+static bool gRTLlayout = false;
+
+// When vertically resizing the dialog, these items should move down
+static set<UINT> gAttachedBottom;
+
+// Default set of items for gAttachedBottom
+static const UINT kDefaultAttachedBottom[] = {
+ IDC_SUBMITREPORTCHECK, IDC_VIEWREPORTBUTTON, IDC_COMMENTTEXT,
+ IDC_INCLUDEURLCHECK, IDC_PROGRESSTEXT, IDC_THROBBER,
+ IDC_CLOSEBUTTON, IDC_RESTARTBUTTON,
+};
+
+static wstring UTF8ToWide(const string& utf8, bool* success = 0);
+static DWORD WINAPI SendThreadProc(LPVOID param);
+
+static wstring Str(const char* key) { return UTF8ToWide(gStrings[key]); }
+
+/* === win32 helper functions === */
+
+static void DoInitCommonControls() {
+ INITCOMMONCONTROLSEX ic;
+ ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
+ ic.dwICC = ICC_PROGRESS_CLASS;
+ InitCommonControlsEx(&ic);
+ // also get the rich edit control
+ LoadLibrary(L"Msftedit.dll");
+}
+
+static bool GetBoolValue(HKEY hRegKey, LPCTSTR valueName, DWORD* value) {
+ DWORD type, dataSize;
+ dataSize = sizeof(DWORD);
+ if (RegQueryValueEx(hRegKey, valueName, nullptr, &type, (LPBYTE)value,
+ &dataSize) == ERROR_SUCCESS &&
+ type == REG_DWORD)
+ return true;
+
+ return false;
+}
+
+static bool CheckBoolKey(const wchar_t* key, const wchar_t* valueName,
+ bool* enabled) {
+ /*
+ * NOTE! This code needs to stay in sync with the preference checking
+ * code in in nsExceptionHandler.cpp.
+ */
+ *enabled = false;
+ bool found = false;
+ HKEY hRegKey;
+ DWORD val;
+ // see if our reg key is set globally
+ if (RegOpenKey(HKEY_LOCAL_MACHINE, key, &hRegKey) == ERROR_SUCCESS) {
+ if (GetBoolValue(hRegKey, valueName, &val)) {
+ *enabled = (val == 1);
+ found = true;
+ }
+ RegCloseKey(hRegKey);
+ } else {
+ // look for it in user settings
+ if (RegOpenKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) {
+ if (GetBoolValue(hRegKey, valueName, &val)) {
+ *enabled = (val == 1);
+ found = true;
+ }
+ RegCloseKey(hRegKey);
+ }
+ }
+
+ return found;
+}
+
+static void SetBoolKey(const wchar_t* key, const wchar_t* value, bool enabled) {
+ /*
+ * NOTE! This code needs to stay in sync with the preference setting
+ * code in in nsExceptionHandler.cpp.
+ */
+ HKEY hRegKey;
+
+ if (RegCreateKey(HKEY_CURRENT_USER, key, &hRegKey) == ERROR_SUCCESS) {
+ DWORD data = (enabled ? 1 : 0);
+ RegSetValueEx(hRegKey, value, 0, REG_DWORD, (LPBYTE)&data, sizeof(data));
+ RegCloseKey(hRegKey);
+ }
+}
+
+static string FormatLastError() {
+ DWORD err = GetLastError();
+ LPWSTR s;
+ string message = "Crash report submission failed: ";
+ // odds are it's a WinInet error
+ HANDLE hInetModule = GetModuleHandle(L"WinInet.dll");
+ if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_FROM_HMODULE,
+ hInetModule, err, 0, (LPWSTR)&s, 0, nullptr) != 0) {
+ message += WideToUTF8(s, nullptr);
+ LocalFree(s);
+ // strip off any trailing newlines
+ string::size_type n = message.find_last_not_of("\r\n");
+ if (n < message.size() - 1) {
+ message.erase(n + 1);
+ }
+ } else {
+ char buf[64];
+ sprintf(buf, "Unknown error, error code: 0x%08x",
+ static_cast<unsigned int>(err));
+ message += buf;
+ }
+ return message;
+}
+
+#define TS_DRAW 2
+#define BP_CHECKBOX 3
+
+typedef HANDLE(WINAPI* OpenThemeDataPtr)(HWND hwnd, LPCWSTR pszClassList);
+typedef HRESULT(WINAPI* CloseThemeDataPtr)(HANDLE hTheme);
+typedef HRESULT(WINAPI* GetThemePartSizePtr)(HANDLE hTheme, HDC hdc,
+ int iPartId, int iStateId,
+ RECT* prc, int ts, SIZE* psz);
+typedef HRESULT(WINAPI* GetThemeContentRectPtr)(HANDLE hTheme, HDC hdc,
+ int iPartId, int iStateId,
+ const RECT* pRect,
+ RECT* pContentRect);
+
+static void GetThemeSizes(HWND hwnd) {
+ HMODULE themeDLL = LoadLibrary(L"uxtheme.dll");
+
+ if (!themeDLL) return;
+
+ OpenThemeDataPtr openTheme =
+ (OpenThemeDataPtr)GetProcAddress(themeDLL, "OpenThemeData");
+ CloseThemeDataPtr closeTheme =
+ (CloseThemeDataPtr)GetProcAddress(themeDLL, "CloseThemeData");
+ GetThemePartSizePtr getThemePartSize =
+ (GetThemePartSizePtr)GetProcAddress(themeDLL, "GetThemePartSize");
+
+ if (!openTheme || !closeTheme || !getThemePartSize) {
+ FreeLibrary(themeDLL);
+ return;
+ }
+
+ HANDLE buttonTheme = openTheme(hwnd, L"Button");
+ if (!buttonTheme) {
+ FreeLibrary(themeDLL);
+ return;
+ }
+ HDC hdc = GetDC(hwnd);
+ SIZE s;
+ getThemePartSize(buttonTheme, hdc, BP_CHECKBOX, 0, nullptr, TS_DRAW, &s);
+ gCheckboxPadding = s.cx;
+ closeTheme(buttonTheme);
+ FreeLibrary(themeDLL);
+}
+
+// Gets the position of a window relative to another window's client area
+static void GetRelativeRect(HWND hwnd, HWND hwndParent, RECT* r) {
+ GetWindowRect(hwnd, r);
+ MapWindowPoints(nullptr, hwndParent, (POINT*)r, 2);
+}
+
+static void SetDlgItemVisible(HWND hwndDlg, UINT item, bool visible) {
+ HWND hwnd = GetDlgItem(hwndDlg, item);
+
+ ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE);
+}
+
+/* === Crash Reporting Dialog === */
+
+static void StretchDialog(HWND hwndDlg, int ydiff) {
+ RECT r;
+ GetWindowRect(hwndDlg, &r);
+ r.bottom += ydiff;
+ MoveWindow(hwndDlg, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE);
+}
+
+static void ReflowDialog(HWND hwndDlg, int ydiff) {
+ // Move items attached to the bottom down/up by as much as
+ // the window resize
+ for (set<UINT>::const_iterator item = gAttachedBottom.begin();
+ item != gAttachedBottom.end(); item++) {
+ RECT r;
+ HWND hwnd = GetDlgItem(hwndDlg, *item);
+ GetRelativeRect(hwnd, hwndDlg, &r);
+ r.top += ydiff;
+ r.bottom += ydiff;
+ MoveWindow(hwnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE);
+ }
+}
+
+static DWORD WINAPI SendThreadProc(LPVOID param) {
+ bool finishedOk;
+ SendThreadData* td = (SendThreadData*)param;
+
+ if (td->sendURL.empty()) {
+ finishedOk = false;
+ LogMessage("No server URL, not sending report");
+ } else {
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ string parameters(Json::writeString(builder, td->queryParameters));
+ google_breakpad::CrashReportSender sender(L"");
+ finishedOk = (sender.SendCrashReport(td->sendURL, parameters, td->files,
+ &td->serverResponse) ==
+ google_breakpad::RESULT_SUCCEEDED);
+ if (finishedOk) {
+ LogMessage("Crash report submitted successfully");
+ } else {
+ // get an error string and print it to the log
+ // XXX: would be nice to get the HTTP status code here, filed:
+ // http://code.google.com/p/google-breakpad/issues/detail?id=220
+ LogMessage(FormatLastError());
+ }
+ }
+
+ if (gAutoSubmit) {
+ // Ordinarily this is done on the main thread in CrashReporterDialogProc,
+ // for auto submit we don't run that and it should be safe to finish up
+ // here as is done on other platforms.
+ SendCompleted(finishedOk, WideToUTF8(gSendData.serverResponse));
+ } else {
+ PostMessage(td->hDlg, WM_UPLOADCOMPLETE, finishedOk ? 1 : 0, 0);
+ }
+
+ return 0;
+}
+
+static void EndCrashReporterDialog(HWND hwndDlg, int code) {
+ // Save the current values to the registry
+ SetBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE,
+ IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK) != 0);
+ SetBoolKey(gCrashReporterKey.c_str(), SUBMIT_REPORT_VALUE,
+ IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0);
+
+ EndDialog(hwndDlg, code);
+}
+
+static void MaybeResizeProgressText(HWND hwndDlg) {
+ HWND hwndProgress = GetDlgItem(hwndDlg, IDC_PROGRESSTEXT);
+ HDC hdc = GetDC(hwndProgress);
+ HFONT hfont = (HFONT)SendMessage(hwndProgress, WM_GETFONT, 0, 0);
+ if (hfont) SelectObject(hdc, hfont);
+ SIZE size;
+ RECT rect;
+ GetRelativeRect(hwndProgress, hwndDlg, &rect);
+
+ wchar_t text[1024];
+ GetWindowText(hwndProgress, text, 1024);
+
+ if (!GetTextExtentPoint32(hdc, text, wcslen(text), &size)) return;
+
+ if (size.cx < (rect.right - rect.left)) return;
+
+ // Figure out how much we need to resize things vertically
+ // This is sort of a fudge, but it should be good enough.
+ int wantedHeight =
+ size.cy * (int)ceil((float)size.cx / (float)(rect.right - rect.left));
+ int diff = wantedHeight - (rect.bottom - rect.top);
+ if (diff <= 0) return;
+
+ MoveWindow(hwndProgress, rect.left, rect.top, rect.right - rect.left,
+ wantedHeight, TRUE);
+
+ gAttachedBottom.clear();
+ gAttachedBottom.insert(IDC_CLOSEBUTTON);
+ gAttachedBottom.insert(IDC_RESTARTBUTTON);
+
+ StretchDialog(hwndDlg, diff);
+
+ for (size_t i = 0; i < sizeof(kDefaultAttachedBottom) / sizeof(UINT); i++) {
+ gAttachedBottom.insert(kDefaultAttachedBottom[i]);
+ }
+}
+
+static void MaybeSendReport(HWND hwndDlg) {
+ if (!IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK)) {
+ EndCrashReporterDialog(hwndDlg, 0);
+ return;
+ }
+
+ // disable all the form controls
+ EnableWindow(GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK), false);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON), false);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_COMMENTTEXT), false);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), false);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_CLOSEBUTTON), false);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_RESTARTBUTTON), false);
+
+ SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT, Str(ST_REPORTDURINGSUBMIT).c_str());
+ MaybeResizeProgressText(hwndDlg);
+ // start throbber
+ // play entire AVI, and loop
+ Animate_Play(GetDlgItem(hwndDlg, IDC_THROBBER), 0, -1, -1);
+ SetDlgItemVisible(hwndDlg, IDC_THROBBER, true);
+ gThreadHandle = nullptr;
+ gSendData.hDlg = hwndDlg;
+ gSendData.queryParameters = gQueryParameters;
+
+ gThreadHandle =
+ CreateThread(nullptr, 0, SendThreadProc, &gSendData, 0, nullptr);
+}
+
+static void RestartApplication() {
+ wstring cmdLine;
+
+ for (unsigned int i = 0; i < gRestartArgs.size(); i++) {
+ cmdLine += L"\"" + UTF8ToWide(gRestartArgs[i]) + L"\" ";
+ }
+
+ STARTUPINFO si;
+ PROCESS_INFORMATION pi;
+
+ ZeroMemory(&si, sizeof(si));
+ si.cb = sizeof(si);
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_SHOWNORMAL;
+ ZeroMemory(&pi, sizeof(pi));
+
+ if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE,
+ 0, nullptr, nullptr, &si, &pi)) {
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ }
+}
+
+static void ShowReportInfo(HWND hwndDlg) {
+ wstring description;
+
+ for (Json::ValueConstIterator iter = gQueryParameters.begin();
+ iter != gQueryParameters.end(); ++iter) {
+ description += UTF8ToWide(iter.name());
+ description += L": ";
+ string value;
+ if (iter->isString()) {
+ value = iter->asString();
+ } else {
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ value = Json::writeString(builder, *iter);
+ }
+ description += UTF8ToWide(value);
+ description += L"\n";
+ }
+
+ description += L"\n";
+ description += Str(ST_EXTRAREPORTINFO);
+
+ SetDlgItemText(hwndDlg, IDC_VIEWREPORTTEXT, description.c_str());
+}
+
+static void UpdateURL(HWND hwndDlg) {
+ if (IsDlgButtonChecked(hwndDlg, IDC_INCLUDEURLCHECK)) {
+ gQueryParameters["URL"] = gURLParameter;
+ } else {
+ gQueryParameters.removeMember("URL");
+ }
+}
+
+static void UpdateComment(HWND hwndDlg) {
+ wchar_t comment[MAX_COMMENT_LENGTH + 1];
+ GetDlgItemTextW(hwndDlg, IDC_COMMENTTEXT, comment,
+ sizeof(comment) / sizeof(comment[0]));
+ if (wcslen(comment) > 0)
+ gQueryParameters["Comments"] = WideToUTF8(comment);
+ else
+ gQueryParameters.removeMember("Comments");
+}
+
+/*
+ * Dialog procedure for the "view report" dialog.
+ */
+static BOOL CALLBACK ViewReportDialogProc(HWND hwndDlg, UINT message,
+ WPARAM wParam, LPARAM lParam) {
+ switch (message) {
+ case WM_INITDIALOG: {
+ SetWindowText(hwndDlg, Str(ST_VIEWREPORTTITLE).c_str());
+ SetDlgItemText(hwndDlg, IDOK, Str(ST_OK).c_str());
+ SendDlgItemMessage(hwndDlg, IDC_VIEWREPORTTEXT, EM_SETTARGETDEVICE,
+ (WPARAM) nullptr, 0);
+ ShowReportInfo(hwndDlg);
+ SetFocus(GetDlgItem(hwndDlg, IDOK));
+ return FALSE;
+ }
+
+ case WM_COMMAND: {
+ if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDOK)
+ EndDialog(hwndDlg, 0);
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+// Return the number of bytes this string will take encoded
+// in UTF-8
+static inline int BytesInUTF8(wchar_t* str) {
+ // Just count size of buffer for UTF-8, minus one
+ // (we don't need to count the null terminator)
+ return WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, nullptr,
+ nullptr) -
+ 1;
+}
+
+// Calculate the length of the text in this edit control (in bytes,
+// in the UTF-8 encoding) after replacing the current selection
+// with |insert|.
+static int NewTextLength(HWND hwndEdit, wchar_t* insert) {
+ wchar_t current[MAX_COMMENT_LENGTH + 1];
+
+ GetWindowText(hwndEdit, current, MAX_COMMENT_LENGTH + 1);
+ DWORD selStart, selEnd;
+ SendMessage(hwndEdit, EM_GETSEL, (WPARAM)&selStart, (LPARAM)&selEnd);
+
+ int selectionLength = 0;
+ if (selEnd - selStart > 0) {
+ wchar_t selection[MAX_COMMENT_LENGTH + 1];
+ google_breakpad::WindowsStringUtils::safe_wcsncpy(
+ selection, MAX_COMMENT_LENGTH + 1, current + selStart,
+ selEnd - selStart);
+ selection[selEnd - selStart] = '\0';
+ selectionLength = BytesInUTF8(selection);
+ }
+
+ // current string length + replacement text length
+ // - replaced selection length
+ return BytesInUTF8(current) + BytesInUTF8(insert) - selectionLength;
+}
+
+// Window procedure for subclassing edit controls
+static LRESULT CALLBACK EditSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam,
+ LPARAM lParam) {
+ static WNDPROC super = nullptr;
+
+ if (super == nullptr) super = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_USERDATA);
+
+ switch (uMsg) {
+ case WM_PAINT: {
+ HDC hdc;
+ PAINTSTRUCT ps;
+ RECT r;
+ wchar_t windowText[1024];
+
+ GetWindowText(hwnd, windowText, 1024);
+ // if the control contains text or is focused, draw it normally
+ if (GetFocus() == hwnd || windowText[0] != '\0')
+ return CallWindowProc(super, hwnd, uMsg, wParam, lParam);
+
+ GetClientRect(hwnd, &r);
+ hdc = BeginPaint(hwnd, &ps);
+ FillRect(hdc, &r,
+ GetSysColorBrush(IsWindowEnabled(hwnd) ? COLOR_WINDOW
+ : COLOR_BTNFACE));
+ SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT));
+ SelectObject(hdc, (HFONT)GetStockObject(DEFAULT_GUI_FONT));
+ SetBkMode(hdc, TRANSPARENT);
+ wchar_t* txt = (wchar_t*)GetProp(hwnd, L"PROP_GRAYTEXT");
+ // Get the actual edit control rect
+ CallWindowProc(super, hwnd, EM_GETRECT, 0, (LPARAM)&r);
+ UINT format = DT_EDITCONTROL | DT_NOPREFIX | DT_WORDBREAK | DT_INTERNAL;
+ if (gRTLlayout) format |= DT_RIGHT;
+ if (txt) DrawText(hdc, txt, wcslen(txt), &r, format);
+ EndPaint(hwnd, &ps);
+ return 0;
+ }
+
+ // We handle WM_CHAR and WM_PASTE to limit the comment box to 500
+ // bytes in UTF-8.
+ case WM_CHAR: {
+ // Leave accelerator keys and non-printing chars (except LF) alone
+ if (wParam & (1 << 24) || wParam & (1 << 29) ||
+ (wParam < ' ' && wParam != '\n'))
+ break;
+
+ wchar_t ch[2] = {(wchar_t)wParam, 0};
+ if (NewTextLength(hwnd, ch) > MAX_COMMENT_LENGTH) return 0;
+
+ break;
+ }
+
+ case WM_PASTE: {
+ if (IsClipboardFormatAvailable(CF_UNICODETEXT) && OpenClipboard(hwnd)) {
+ HGLOBAL hg = GetClipboardData(CF_UNICODETEXT);
+ wchar_t* pastedText = (wchar_t*)GlobalLock(hg);
+ int newSize = 0;
+
+ if (pastedText) newSize = NewTextLength(hwnd, pastedText);
+
+ GlobalUnlock(hg);
+ CloseClipboard();
+
+ if (newSize > MAX_COMMENT_LENGTH) return 0;
+ }
+ break;
+ }
+
+ case WM_SETFOCUS:
+ case WM_KILLFOCUS: {
+ RECT r;
+ GetClientRect(hwnd, &r);
+ InvalidateRect(hwnd, &r, TRUE);
+ break;
+ }
+
+ case WM_DESTROY: {
+ // cleanup our property
+ HGLOBAL hData = RemoveProp(hwnd, L"PROP_GRAYTEXT");
+ if (hData) GlobalFree(hData);
+ }
+ }
+
+ return CallWindowProc(super, hwnd, uMsg, wParam, lParam);
+}
+
+// Resize a control to fit this text
+static int ResizeControl(HWND hwndButton, RECT& rect, wstring text,
+ bool shiftLeft, int userDefinedPadding) {
+ HDC hdc = GetDC(hwndButton);
+ HFONT hfont = (HFONT)SendMessage(hwndButton, WM_GETFONT, 0, 0);
+ if (hfont) SelectObject(hdc, hfont);
+ SIZE size, oldSize;
+ int sizeDiff = 0;
+
+ wchar_t oldText[1024];
+ GetWindowText(hwndButton, oldText, 1024);
+
+ if (GetTextExtentPoint32(hdc, text.c_str(), text.length(), &size)
+ // default text on the button
+ && GetTextExtentPoint32(hdc, oldText, wcslen(oldText), &oldSize)) {
+ /*
+ Expand control widths to accomidate wider text strings. For most
+ controls (including buttons) the text padding is defined by the
+ dialog's rc file. Some controls (such as checkboxes) have padding
+ that extends to the end of the dialog, in which case we ignore the
+ rc padding and rely on a user defined value passed in through
+ userDefinedPadding.
+ */
+ int textIncrease = size.cx - oldSize.cx;
+ if (textIncrease < 0) return 0;
+ int existingTextPadding;
+ if (userDefinedPadding == 0)
+ existingTextPadding = (rect.right - rect.left) - oldSize.cx;
+ else
+ existingTextPadding = userDefinedPadding;
+ sizeDiff = textIncrease + existingTextPadding;
+
+ if (shiftLeft) {
+ // shift left by the amount the button should grow
+ rect.left -= sizeDiff;
+ } else {
+ // grow right instead
+ rect.right += sizeDiff;
+ }
+ MoveWindow(hwndButton, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return sizeDiff;
+}
+
+// The window was resized horizontally, so widen some of our
+// controls to make use of the space
+static void StretchControlsToFit(HWND hwndDlg) {
+ int controls[] = {IDC_DESCRIPTIONTEXT, IDC_SUBMITREPORTCHECK, IDC_COMMENTTEXT,
+ IDC_INCLUDEURLCHECK, IDC_PROGRESSTEXT};
+
+ RECT dlgRect;
+ GetClientRect(hwndDlg, &dlgRect);
+
+ for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); i++) {
+ RECT r;
+ HWND hwndControl = GetDlgItem(hwndDlg, controls[i]);
+ GetRelativeRect(hwndControl, hwndDlg, &r);
+ // 6 pixel spacing on the right
+ if (r.right + 6 != dlgRect.right) {
+ r.right = dlgRect.right - 6;
+ MoveWindow(hwndControl, r.left, r.top, r.right - r.left, r.bottom - r.top,
+ TRUE);
+ }
+ }
+}
+
+static void SubmitReportChecked(HWND hwndDlg) {
+ bool enabled = (IsDlgButtonChecked(hwndDlg, IDC_SUBMITREPORTCHECK) != 0);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON), enabled);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_COMMENTTEXT), enabled);
+ EnableWindow(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), enabled);
+ SetDlgItemVisible(hwndDlg, IDC_PROGRESSTEXT, enabled);
+}
+
+static INT_PTR DialogBoxParamMaybeRTL(UINT idd, HWND hwndParent,
+ DLGPROC dlgProc, LPARAM param) {
+ INT_PTR rv = 0;
+ if (gRTLlayout) {
+ // We need to toggle the WS_EX_LAYOUTRTL style flag on the dialog
+ // template.
+ HRSRC hDialogRC = FindResource(nullptr, MAKEINTRESOURCE(idd), RT_DIALOG);
+ HGLOBAL hDlgTemplate = LoadResource(nullptr, hDialogRC);
+ DLGTEMPLATEEX* pDlgTemplate = (DLGTEMPLATEEX*)LockResource(hDlgTemplate);
+ unsigned long sizeDlg = SizeofResource(nullptr, hDialogRC);
+ HGLOBAL hMyDlgTemplate = GlobalAlloc(GPTR, sizeDlg);
+ DLGTEMPLATEEX* pMyDlgTemplate = (DLGTEMPLATEEX*)GlobalLock(hMyDlgTemplate);
+ memcpy(pMyDlgTemplate, pDlgTemplate, sizeDlg);
+
+ pMyDlgTemplate->exStyle |= WS_EX_LAYOUTRTL;
+
+ rv = DialogBoxIndirectParam(nullptr, (LPCDLGTEMPLATE)pMyDlgTemplate,
+ hwndParent, dlgProc, param);
+ GlobalUnlock(hMyDlgTemplate);
+ GlobalFree(hMyDlgTemplate);
+ } else {
+ rv = DialogBoxParam(nullptr, MAKEINTRESOURCE(idd), hwndParent, dlgProc,
+ param);
+ }
+
+ return rv;
+}
+
+static BOOL CALLBACK CrashReporterDialogProc(HWND hwndDlg, UINT message,
+ WPARAM wParam, LPARAM lParam) {
+ static int sHeight = 0;
+
+ bool success;
+ bool enabled;
+
+ switch (message) {
+ case WM_INITDIALOG: {
+ GetThemeSizes(hwndDlg);
+ RECT r;
+ GetClientRect(hwndDlg, &r);
+ sHeight = r.bottom - r.top;
+
+ SetWindowText(hwndDlg, Str(ST_CRASHREPORTERTITLE).c_str());
+ HICON hIcon =
+ LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDI_MAINICON));
+ SendMessage(hwndDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
+ SendMessage(hwndDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
+
+ // resize the "View Report" button based on the string length
+ RECT rect;
+ HWND hwnd = GetDlgItem(hwndDlg, IDC_VIEWREPORTBUTTON);
+ GetRelativeRect(hwnd, hwndDlg, &rect);
+ ResizeControl(hwnd, rect, Str(ST_VIEWREPORT), false, 0);
+ SetDlgItemText(hwndDlg, IDC_VIEWREPORTBUTTON, Str(ST_VIEWREPORT).c_str());
+
+ hwnd = GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK);
+ GetRelativeRect(hwnd, hwndDlg, &rect);
+ long maxdiff = ResizeControl(hwnd, rect, Str(ST_CHECKSUBMIT), false,
+ gCheckboxPadding);
+ SetDlgItemText(hwndDlg, IDC_SUBMITREPORTCHECK,
+ Str(ST_CHECKSUBMIT).c_str());
+
+ if (!CheckBoolKey(gCrashReporterKey.c_str(), SUBMIT_REPORT_VALUE,
+ &enabled))
+ enabled = true;
+
+ CheckDlgButton(hwndDlg, IDC_SUBMITREPORTCHECK,
+ enabled ? BST_CHECKED : BST_UNCHECKED);
+ SubmitReportChecked(hwndDlg);
+
+ HWND hwndComment = GetDlgItem(hwndDlg, IDC_COMMENTTEXT);
+ WNDPROC OldWndProc = (WNDPROC)SetWindowLongPtr(
+ hwndComment, GWLP_WNDPROC, (LONG_PTR)EditSubclassProc);
+
+ // Subclass comment edit control to get placeholder text
+ SetWindowLongPtr(hwndComment, GWLP_USERDATA, (LONG_PTR)OldWndProc);
+ wstring commentGrayText = Str(ST_COMMENTGRAYTEXT);
+ wchar_t* hMem = (wchar_t*)GlobalAlloc(
+ GPTR, (commentGrayText.length() + 1) * sizeof(wchar_t));
+ wcscpy(hMem, commentGrayText.c_str());
+ SetProp(hwndComment, L"PROP_GRAYTEXT", hMem);
+
+ hwnd = GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK);
+ GetRelativeRect(hwnd, hwndDlg, &rect);
+ long diff =
+ ResizeControl(hwnd, rect, Str(ST_CHECKURL), false, gCheckboxPadding);
+ maxdiff = std::max(diff, maxdiff);
+ SetDlgItemText(hwndDlg, IDC_INCLUDEURLCHECK, Str(ST_CHECKURL).c_str());
+
+ // want this on by default
+ if (CheckBoolKey(gCrashReporterKey.c_str(), INCLUDE_URL_VALUE,
+ &enabled) &&
+ !enabled) {
+ CheckDlgButton(hwndDlg, IDC_INCLUDEURLCHECK, BST_UNCHECKED);
+ } else {
+ CheckDlgButton(hwndDlg, IDC_INCLUDEURLCHECK, BST_CHECKED);
+ }
+
+ SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT,
+ Str(ST_REPORTPRESUBMIT).c_str());
+
+ RECT closeRect;
+ HWND hwndClose = GetDlgItem(hwndDlg, IDC_CLOSEBUTTON);
+ GetRelativeRect(hwndClose, hwndDlg, &closeRect);
+
+ RECT restartRect;
+ HWND hwndRestart = GetDlgItem(hwndDlg, IDC_RESTARTBUTTON);
+ GetRelativeRect(hwndRestart, hwndDlg, &restartRect);
+
+ // set the close button text and shift the buttons around
+ // since the size may need to change
+ int sizeDiff = ResizeControl(hwndClose, closeRect, Str(ST_QUIT), true, 0);
+ restartRect.left -= sizeDiff;
+ restartRect.right -= sizeDiff;
+ SetDlgItemText(hwndDlg, IDC_CLOSEBUTTON, Str(ST_QUIT).c_str());
+
+ if (gRestartArgs.size() > 0) {
+ // Resize restart button to fit text
+ ResizeControl(hwndRestart, restartRect, Str(ST_RESTART), true, 0);
+ SetDlgItemText(hwndDlg, IDC_RESTARTBUTTON, Str(ST_RESTART).c_str());
+ } else {
+ // No restart arguments, so just hide the restart button
+ SetDlgItemVisible(hwndDlg, IDC_RESTARTBUTTON, false);
+ }
+ // See if we need to widen the window
+ // Leave 6 pixels on either side + 6 pixels between the buttons
+ int neededSize = closeRect.right - closeRect.left + restartRect.right -
+ restartRect.left + 6 * 3;
+ GetClientRect(hwndDlg, &r);
+ // We may already have resized one of the checkboxes above
+ maxdiff = std::max(maxdiff, neededSize - (r.right - r.left));
+
+ if (maxdiff > 0) {
+ // widen window
+ GetWindowRect(hwndDlg, &r);
+ r.right += maxdiff;
+ MoveWindow(hwndDlg, r.left, r.top, r.right - r.left, r.bottom - r.top,
+ TRUE);
+ // shift both buttons right
+ if (restartRect.left + maxdiff < 6) maxdiff += 6;
+ closeRect.left += maxdiff;
+ closeRect.right += maxdiff;
+ restartRect.left += maxdiff;
+ restartRect.right += maxdiff;
+ MoveWindow(hwndClose, closeRect.left, closeRect.top,
+ closeRect.right - closeRect.left,
+ closeRect.bottom - closeRect.top, TRUE);
+ StretchControlsToFit(hwndDlg);
+ }
+ // need to move the restart button regardless
+ MoveWindow(hwndRestart, restartRect.left, restartRect.top,
+ restartRect.right - restartRect.left,
+ restartRect.bottom - restartRect.top, TRUE);
+
+ // Resize the description text last, in case the window was resized
+ // before this.
+ SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETEVENTMASK,
+ (WPARAM) nullptr, ENM_REQUESTRESIZE);
+
+ wstring description = Str(ST_CRASHREPORTERHEADER);
+ description += L"\n\n";
+ description += Str(ST_CRASHREPORTERDESCRIPTION);
+ SetDlgItemText(hwndDlg, IDC_DESCRIPTIONTEXT, description.c_str());
+
+ // Make the title bold.
+ CHARFORMAT fmt = {
+ 0,
+ };
+ fmt.cbSize = sizeof(fmt);
+ fmt.dwMask = CFM_BOLD;
+ fmt.dwEffects = CFE_BOLD;
+ SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETSEL, 0,
+ Str(ST_CRASHREPORTERHEADER).length());
+ SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETCHARFORMAT,
+ SCF_SELECTION, (LPARAM)&fmt);
+ SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETSEL, 0, 0);
+ // Force redraw.
+ SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_SETTARGETDEVICE,
+ (WPARAM) nullptr, 0);
+ // Force resize.
+ SendDlgItemMessage(hwndDlg, IDC_DESCRIPTIONTEXT, EM_REQUESTRESIZE, 0, 0);
+
+ // if no URL was given, hide the URL checkbox
+ if (!gQueryParameters.isMember("URL")) {
+ RECT urlCheckRect;
+ GetWindowRect(GetDlgItem(hwndDlg, IDC_INCLUDEURLCHECK), &urlCheckRect);
+
+ SetDlgItemVisible(hwndDlg, IDC_INCLUDEURLCHECK, false);
+
+ gAttachedBottom.erase(IDC_VIEWREPORTBUTTON);
+ gAttachedBottom.erase(IDC_SUBMITREPORTCHECK);
+ gAttachedBottom.erase(IDC_COMMENTTEXT);
+
+ StretchDialog(hwndDlg, urlCheckRect.top - urlCheckRect.bottom);
+
+ gAttachedBottom.insert(IDC_VIEWREPORTBUTTON);
+ gAttachedBottom.insert(IDC_SUBMITREPORTCHECK);
+ gAttachedBottom.insert(IDC_COMMENTTEXT);
+ }
+
+ MaybeResizeProgressText(hwndDlg);
+
+ // Open the AVI resource for the throbber
+ Animate_Open(GetDlgItem(hwndDlg, IDC_THROBBER),
+ MAKEINTRESOURCE(IDR_THROBBER));
+
+ UpdateURL(hwndDlg);
+
+ SetFocus(GetDlgItem(hwndDlg, IDC_SUBMITREPORTCHECK));
+ return FALSE;
+ }
+ case WM_SIZE: {
+ ReflowDialog(hwndDlg, HIWORD(lParam) - sHeight);
+ sHeight = HIWORD(lParam);
+ InvalidateRect(hwndDlg, nullptr, TRUE);
+ return FALSE;
+ }
+ case WM_NOTIFY: {
+ NMHDR* notification = reinterpret_cast<NMHDR*>(lParam);
+ if (notification->code == EN_REQUESTRESIZE) {
+ // Resizing the rich edit control to fit the description text.
+ REQRESIZE* reqresize = reinterpret_cast<REQRESIZE*>(lParam);
+ RECT newSize = reqresize->rc;
+ RECT oldSize;
+ GetRelativeRect(notification->hwndFrom, hwndDlg, &oldSize);
+
+ // resize the text box as requested
+ MoveWindow(notification->hwndFrom, newSize.left, newSize.top,
+ newSize.right - newSize.left, newSize.bottom - newSize.top,
+ TRUE);
+
+ // Resize the dialog to fit (the WM_SIZE handler will move the controls)
+ StretchDialog(hwndDlg, newSize.bottom - oldSize.bottom);
+ }
+ return FALSE;
+ }
+ case WM_COMMAND: {
+ if (HIWORD(wParam) == BN_CLICKED) {
+ switch (LOWORD(wParam)) {
+ case IDC_VIEWREPORTBUTTON:
+ DialogBoxParamMaybeRTL(IDD_VIEWREPORTDIALOG, hwndDlg,
+ (DLGPROC)ViewReportDialogProc, 0);
+ break;
+ case IDC_SUBMITREPORTCHECK:
+ SubmitReportChecked(hwndDlg);
+ break;
+ case IDC_INCLUDEURLCHECK:
+ UpdateURL(hwndDlg);
+ break;
+ case IDC_CLOSEBUTTON:
+ MaybeSendReport(hwndDlg);
+ break;
+ case IDC_RESTARTBUTTON:
+ RestartApplication();
+ MaybeSendReport(hwndDlg);
+ break;
+ }
+ } else if (HIWORD(wParam) == EN_CHANGE) {
+ switch (LOWORD(wParam)) {
+ case IDC_COMMENTTEXT:
+ UpdateComment(hwndDlg);
+ }
+ }
+
+ return FALSE;
+ }
+ case WM_UPLOADCOMPLETE: {
+ WaitForSingleObject(gThreadHandle, INFINITE);
+ success = (wParam == 1);
+ SendCompleted(success, WideToUTF8(gSendData.serverResponse));
+ // hide throbber
+ Animate_Stop(GetDlgItem(hwndDlg, IDC_THROBBER));
+ SetDlgItemVisible(hwndDlg, IDC_THROBBER, false);
+
+ SetDlgItemText(hwndDlg, IDC_PROGRESSTEXT,
+ success ? Str(ST_REPORTSUBMITSUCCESS).c_str()
+ : Str(ST_SUBMITFAILED).c_str());
+ MaybeResizeProgressText(hwndDlg);
+ // close dialog after 5 seconds
+ SetTimer(hwndDlg, 0, 5000, nullptr);
+ //
+ return TRUE;
+ }
+
+ case WM_TIMER: {
+ // The "1" gets used down in UIShowCrashUI to indicate that we at least
+ // tried to send the report.
+ EndCrashReporterDialog(hwndDlg, 1);
+ return FALSE;
+ }
+
+ case WM_CLOSE: {
+ EndCrashReporterDialog(hwndDlg, 0);
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+static wstring UTF8ToWide(const string& utf8, bool* success) {
+ wchar_t* buffer = nullptr;
+ int buffer_size =
+ MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
+ if (buffer_size == 0) {
+ if (success) *success = false;
+ return L"";
+ }
+
+ buffer = new wchar_t[buffer_size];
+ if (buffer == nullptr) {
+ if (success) *success = false;
+ return L"";
+ }
+
+ MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, buffer, buffer_size);
+ wstring str = buffer;
+ delete[] buffer;
+
+ if (success) *success = true;
+
+ return str;
+}
+
+static string WideToMBCP(const wstring& wide, unsigned int cp,
+ bool* success = nullptr) {
+ char* buffer = nullptr;
+ int buffer_size = WideCharToMultiByte(cp, 0, wide.c_str(), -1, nullptr, 0,
+ nullptr, nullptr);
+ if (buffer_size == 0) {
+ if (success) *success = false;
+ return "";
+ }
+
+ buffer = new char[buffer_size];
+ if (buffer == nullptr) {
+ if (success) *success = false;
+ return "";
+ }
+
+ WideCharToMultiByte(cp, 0, wide.c_str(), -1, buffer, buffer_size, nullptr,
+ nullptr);
+ string mb = buffer;
+ delete[] buffer;
+
+ if (success) *success = true;
+
+ return mb;
+}
+
+string WideToUTF8(const wstring& wide, bool* success) {
+ return WideToMBCP(wide, CP_UTF8, success);
+}
+
+/* === Crashreporter UI Functions === */
+
+bool UIInit() {
+ for (size_t i = 0; i < sizeof(kDefaultAttachedBottom) / sizeof(UINT); i++) {
+ gAttachedBottom.insert(kDefaultAttachedBottom[i]);
+ }
+
+ DoInitCommonControls();
+
+ return true;
+}
+
+void UIShutdown() {}
+
+void UIShowDefaultUI() {
+ MessageBox(nullptr, Str(ST_CRASHREPORTERDEFAULT).c_str(), L"Crash Reporter",
+ MB_OK | MB_ICONSTOP);
+}
+
+static bool CanUseMainCrashReportServer() {
+ // Any NT from 6.0 and above is fine.
+ if (IsWindowsVersionOrGreater(6, 0, 0)) {
+ return true;
+ }
+
+ // On NT 5 servers, we need Server 2003 SP2.
+ if (IsWindowsServer()) {
+ return IsWindowsVersionOrGreater(5, 2, 2);
+ }
+
+ // Otherwise we have an NT 5 client.
+ // We need exactly XP SP3 (version 5.1 SP3 but not version 5.2).
+ return (IsWindowsVersionOrGreater(5, 1, 3) &&
+ !IsWindowsVersionOrGreater(5, 2, 0));
+}
+
+bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
+ const string& sendURL, const vector<string>& restartArgs) {
+ gSendData.hDlg = nullptr;
+ gSendData.sendURL = UTF8ToWide(sendURL);
+
+ // Older Windows don't support the crash report server's crypto.
+ // This is a hack to use an alternate server.
+ if (!CanUseMainCrashReportServer() &&
+ gSendData.sendURL.find(SENDURL_ORIGINAL) == 0) {
+ gSendData.sendURL.replace(0, ARRAYSIZE(SENDURL_ORIGINAL) - 1,
+ SENDURL_XPSP2);
+ }
+
+ for (StringTable::const_iterator i = files.begin(); i != files.end(); i++) {
+ gSendData.files[UTF8ToWide(i->first)] = UTF8ToWide(i->second);
+ }
+
+ gQueryParameters = queryParameters;
+
+ if (gQueryParameters.isMember("Vendor")) {
+ gCrashReporterKey = L"Software\\";
+ string vendor = gQueryParameters["Vendor"].asString();
+ if (!vendor.empty()) {
+ gCrashReporterKey += UTF8ToWide(vendor) + L"\\";
+ }
+ string productName = gQueryParameters["ProductName"].asString();
+ gCrashReporterKey += UTF8ToWide(productName) + L"\\Crash Reporter";
+ }
+
+ if (gQueryParameters.isMember("URL")) {
+ gURLParameter = gQueryParameters["URL"].asString();
+ }
+
+ gRestartArgs = restartArgs;
+
+ if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes")
+ gRTLlayout = true;
+
+ if (gAutoSubmit) {
+ gSendData.queryParameters = gQueryParameters;
+
+ gThreadHandle =
+ CreateThread(nullptr, 0, SendThreadProc, &gSendData, 0, nullptr);
+ WaitForSingleObject(gThreadHandle, INFINITE);
+ // SendCompleted was called from SendThreadProc
+ return true;
+ }
+
+ return 1 == DialogBoxParamMaybeRTL(IDD_SENDDIALOG, nullptr,
+ (DLGPROC)CrashReporterDialogProc, 0);
+}
+
+void UIError_impl(const string& message) {
+ wstring title = Str(ST_CRASHREPORTERTITLE);
+ if (title.empty()) title = L"Crash Reporter Error";
+
+ MessageBox(nullptr, UTF8ToWide(message).c_str(), title.c_str(),
+ MB_OK | MB_ICONSTOP);
+}
+
+bool UIGetIniPath(string& path) {
+ wchar_t fileName[MAX_PATH];
+ if (GetModuleFileName(nullptr, fileName, MAX_PATH)) {
+ // get crashreporter ini
+ wchar_t* s = wcsrchr(fileName, '.');
+ if (s) {
+ wcscpy(s, L".ini");
+ path = WideToUTF8(fileName);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool UIGetSettingsPath(const string& vendor, const string& product,
+ string& settings_path) {
+ wchar_t path[MAX_PATH] = {};
+ HRESULT hRes = SHGetFolderPath(nullptr, CSIDL_APPDATA, nullptr, 0, path);
+ if (FAILED(hRes)) {
+ // This provides a fallback for getting the path to APPDATA by querying the
+ // registry when the call to SHGetFolderPath is unable to provide this path
+ // (Bug 513958).
+ HKEY key;
+ DWORD type, dwRes;
+ DWORD size = sizeof(path) - 1;
+ dwRes = ::RegOpenKeyExW(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Exp"
+ L"lorer\\Shell Folders",
+ 0, KEY_READ, &key);
+ if (dwRes != ERROR_SUCCESS) return false;
+
+ dwRes =
+ RegQueryValueExW(key, L"AppData", nullptr, &type, (LPBYTE)&path, &size);
+ ::RegCloseKey(key);
+ // The call to RegQueryValueExW must succeed, the type must be REG_SZ, the
+ // buffer size must not equal 0, and the buffer size be a multiple of 2.
+ if (dwRes != ERROR_SUCCESS || type != REG_SZ || size == 0 || size % 2 != 0)
+ return false;
+ }
+
+ if (!vendor.empty()) {
+ PathAppend(path, UTF8ToWide(vendor).c_str());
+ }
+ PathAppend(path, UTF8ToWide(product).c_str());
+ PathAppend(path, L"Crash Reports");
+ settings_path = WideToUTF8(path);
+ return true;
+}
+
+bool UIEnsurePathExists(const string& path) {
+ if (CreateDirectory(UTF8ToWide(path).c_str(), nullptr) == 0) {
+ if (GetLastError() != ERROR_ALREADY_EXISTS) return false;
+ }
+
+ return true;
+}
+
+bool UIFileExists(const string& path) {
+ DWORD attrs = GetFileAttributes(UTF8ToWide(path).c_str());
+ return (attrs != INVALID_FILE_ATTRIBUTES);
+}
+
+bool UIMoveFile(const string& oldfile, const string& newfile) {
+ if (oldfile == newfile) return true;
+
+ return MoveFile(UTF8ToWide(oldfile).c_str(), UTF8ToWide(newfile).c_str()) ==
+ TRUE;
+}
+
+bool UIDeleteFile(const string& oldfile) {
+ return DeleteFile(UTF8ToWide(oldfile).c_str()) == TRUE;
+}
+
+ifstream* UIOpenRead(const string& filename, ios_base::openmode mode) {
+#if defined(_MSC_VER)
+ ifstream* file = new ifstream();
+ file->open(UTF8ToWide(filename).c_str(), mode);
+#else // GCC
+ ifstream* file =
+ new ifstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), mode);
+#endif // _MSC_VER
+
+ return file;
+}
+
+ofstream* UIOpenWrite(const string& filename, ios_base::openmode mode) {
+#if defined(_MSC_VER)
+ ofstream* file = new ofstream();
+ file->open(UTF8ToWide(filename).c_str(), mode);
+#else // GCC
+ ofstream* file =
+ new ofstream(WideToMBCP(UTF8ToWide(filename), CP_ACP).c_str(), mode);
+#endif // _MSC_VER
+
+ return file;
+}
+
+struct FileData {
+ FILETIME timestamp;
+ wstring path;
+};
+
+static bool CompareFDTime(const FileData& fd1, const FileData& fd2) {
+ return CompareFileTime(&fd1.timestamp, &fd2.timestamp) > 0;
+}
+
+void UIPruneSavedDumps(const std::string& directory) {
+ wstring wdirectory = UTF8ToWide(directory);
+
+ WIN32_FIND_DATA fdata;
+ wstring findpath = wdirectory + L"\\*.dmp";
+ HANDLE dirlist = FindFirstFile(findpath.c_str(), &fdata);
+ if (dirlist == INVALID_HANDLE_VALUE) return;
+
+ vector<FileData> dumpfiles;
+
+ for (BOOL ok = true; ok; ok = FindNextFile(dirlist, &fdata)) {
+ FileData fd = {fdata.ftLastWriteTime, wdirectory + L"\\" + fdata.cFileName};
+ dumpfiles.push_back(fd);
+ }
+
+ sort(dumpfiles.begin(), dumpfiles.end(), CompareFDTime);
+
+ while (dumpfiles.size() > kSaveCount) {
+ // get the path of the oldest file
+ wstring path = (--dumpfiles.end())->path;
+ DeleteFile(path.c_str());
+
+ // s/.dmp/.extra/
+ path.replace(path.size() - 4, 4, L".extra");
+ DeleteFile(path.c_str());
+
+ dumpfiles.pop_back();
+ }
+ FindClose(dirlist);
+}
+
+bool UIRunProgram(const string& exename, const std::vector<std::string>& args,
+ bool wait) {
+ wstring cmdLine = L"\"" + UTF8ToWide(exename) + L"\" ";
+
+ for (auto arg : args) {
+ cmdLine += L"\"" + UTF8ToWide(arg) + L"\" ";
+ }
+
+ STARTUPINFO si = {};
+ si.cb = sizeof(si);
+ PROCESS_INFORMATION pi = {};
+
+ if (!CreateProcess(/* lpApplicationName */ nullptr, (LPWSTR)cmdLine.c_str(),
+ /* lpProcessAttributes */ nullptr,
+ /* lpThreadAttributes */ nullptr,
+ /* bInheritHandles */ false,
+ NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW,
+ /* lpEnvironment */ nullptr,
+ /* lpCurrentDirectory */ nullptr, &si, &pi)) {
+ return false;
+ }
+
+ if (wait) {
+ WaitForSingleObject(pi.hProcess, INFINITE);
+ }
+
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ return true;
+}
+
+string UIGetEnv(const string& name) {
+ const wchar_t* var = _wgetenv(UTF8ToWide(name).c_str());
+ if (var && *var) {
+ return WideToUTF8(var);
+ }
+
+ return "";
+}
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Info.plist b/toolkit/crashreporter/client/macbuild/Contents/Info.plist
new file mode 100644
index 0000000000..51d6c4de37
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Info.plist
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleDisplayName</key>
+ <string>crashreporter</string>
+ <key>CFBundleExecutable</key>
+ <string>crashreporter</string>
+ <key>CFBundleIconFile</key>
+ <string>crashreporter.icns</string>
+ <key>CFBundleIdentifier</key>
+ <string>org.mozilla.crashreporter</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>crashreporter</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSHasLocalizedDisplayName</key>
+ <true/>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSRequiresAquaSystemAppearance</key>
+ <false/>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>LSUIElement</key>
+ <true/>
+</dict>
+</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/PkgInfo b/toolkit/crashreporter/client/macbuild/Contents/PkgInfo
new file mode 100644
index 0000000000..cae6d0a58f
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/PkgInfo
@@ -0,0 +1,2 @@
+APPL????
+
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in
new file mode 100644
index 0000000000..e08ce59eb6
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Localized versions of Info.plist keys */
+
+CFBundleName = "Crash Reporter";
+CFBundleDisplayName = "@APP_NAME@ Crash Reporter";
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib
new file mode 100644
index 0000000000..254131e431
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBClasses</key>
+ <array>
+ <dict>
+ <key>ACTIONS</key>
+ <dict>
+ <key>closeClicked</key>
+ <string>id</string>
+ <key>includeURLClicked</key>
+ <string>id</string>
+ <key>restartClicked</key>
+ <string>id</string>
+ <key>submitReportClicked</key>
+ <string>id</string>
+ <key>viewReportClicked</key>
+ <string>id</string>
+ <key>viewReportOkClicked</key>
+ <string>id</string>
+ </dict>
+ <key>CLASS</key>
+ <string>CrashReporterUI</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>OUTLETS</key>
+ <dict>
+ <key>mCloseButton</key>
+ <string>NSButton</string>
+ <key>mCommentScrollView</key>
+ <string>NSScrollView</string>
+ <key>mCommentText</key>
+ <string>TextViewWithPlaceHolder</string>
+ <key>mDescriptionLabel</key>
+ <string>NSTextField</string>
+ <key>mEmailMeButton</key>
+ <string>NSButton</string>
+ <key>mEmailText</key>
+ <string>NSTextField</string>
+ <key>mErrorCloseButton</key>
+ <string>NSButton</string>
+ <key>mErrorHeaderLabel</key>
+ <string>NSTextField</string>
+ <key>mErrorLabel</key>
+ <string>NSTextField</string>
+ <key>mErrorView</key>
+ <string>NSView</string>
+ <key>mHeaderLabel</key>
+ <string>NSTextField</string>
+ <key>mIncludeURLButton</key>
+ <string>NSButton</string>
+ <key>mProgressIndicator</key>
+ <string>NSProgressIndicator</string>
+ <key>mProgressText</key>
+ <string>NSTextField</string>
+ <key>mRestartButton</key>
+ <string>NSButton</string>
+ <key>mSubmitReportButton</key>
+ <string>NSButton</string>
+ <key>mViewReportButton</key>
+ <string>NSButton</string>
+ <key>mViewReportOkButton</key>
+ <string>NSButton</string>
+ <key>mViewReportTextView</key>
+ <string>NSTextView</string>
+ <key>mViewReportWindow</key>
+ <string>NSWindow</string>
+ <key>mWindow</key>
+ <string>NSWindow</string>
+ </dict>
+ <key>SUPERCLASS</key>
+ <string>NSObject</string>
+ </dict>
+ <dict>
+ <key>ACTIONS</key>
+ <dict>
+ <key>insertTab</key>
+ <string>id</string>
+ </dict>
+ <key>CLASS</key>
+ <string>TextViewWithPlaceHolder</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>SUPERCLASS</key>
+ <string>NSTextView</string>
+ </dict>
+ <dict>
+ <key>CLASS</key>
+ <string>FirstResponder</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>SUPERCLASS</key>
+ <string>NSObject</string>
+ </dict>
+ </array>
+ <key>IBVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib
new file mode 100644
index 0000000000..517349ffce
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBFramework Version</key>
+ <string>629</string>
+ <key>IBOldestOS</key>
+ <integer>5</integer>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>2</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>9C7010</string>
+ <key>targetFramework</key>
+ <string>IBCocoaFramework</string>
+</dict>
+</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 0000000000..bfdcccb74c
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib
Binary files differ
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib
new file mode 100644
index 0000000000..254131e431
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBClasses</key>
+ <array>
+ <dict>
+ <key>ACTIONS</key>
+ <dict>
+ <key>closeClicked</key>
+ <string>id</string>
+ <key>includeURLClicked</key>
+ <string>id</string>
+ <key>restartClicked</key>
+ <string>id</string>
+ <key>submitReportClicked</key>
+ <string>id</string>
+ <key>viewReportClicked</key>
+ <string>id</string>
+ <key>viewReportOkClicked</key>
+ <string>id</string>
+ </dict>
+ <key>CLASS</key>
+ <string>CrashReporterUI</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>OUTLETS</key>
+ <dict>
+ <key>mCloseButton</key>
+ <string>NSButton</string>
+ <key>mCommentScrollView</key>
+ <string>NSScrollView</string>
+ <key>mCommentText</key>
+ <string>TextViewWithPlaceHolder</string>
+ <key>mDescriptionLabel</key>
+ <string>NSTextField</string>
+ <key>mEmailMeButton</key>
+ <string>NSButton</string>
+ <key>mEmailText</key>
+ <string>NSTextField</string>
+ <key>mErrorCloseButton</key>
+ <string>NSButton</string>
+ <key>mErrorHeaderLabel</key>
+ <string>NSTextField</string>
+ <key>mErrorLabel</key>
+ <string>NSTextField</string>
+ <key>mErrorView</key>
+ <string>NSView</string>
+ <key>mHeaderLabel</key>
+ <string>NSTextField</string>
+ <key>mIncludeURLButton</key>
+ <string>NSButton</string>
+ <key>mProgressIndicator</key>
+ <string>NSProgressIndicator</string>
+ <key>mProgressText</key>
+ <string>NSTextField</string>
+ <key>mRestartButton</key>
+ <string>NSButton</string>
+ <key>mSubmitReportButton</key>
+ <string>NSButton</string>
+ <key>mViewReportButton</key>
+ <string>NSButton</string>
+ <key>mViewReportOkButton</key>
+ <string>NSButton</string>
+ <key>mViewReportTextView</key>
+ <string>NSTextView</string>
+ <key>mViewReportWindow</key>
+ <string>NSWindow</string>
+ <key>mWindow</key>
+ <string>NSWindow</string>
+ </dict>
+ <key>SUPERCLASS</key>
+ <string>NSObject</string>
+ </dict>
+ <dict>
+ <key>ACTIONS</key>
+ <dict>
+ <key>insertTab</key>
+ <string>id</string>
+ </dict>
+ <key>CLASS</key>
+ <string>TextViewWithPlaceHolder</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>SUPERCLASS</key>
+ <string>NSTextView</string>
+ </dict>
+ <dict>
+ <key>CLASS</key>
+ <string>FirstResponder</string>
+ <key>LANGUAGE</key>
+ <string>ObjC</string>
+ <key>SUPERCLASS</key>
+ <string>NSObject</string>
+ </dict>
+ </array>
+ <key>IBVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib
new file mode 100644
index 0000000000..4a2251aaf5
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/info.nib
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBFramework Version</key>
+ <string>629</string>
+ <key>IBOldestOS</key>
+ <integer>5</integer>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>2</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>9D34</string>
+ <key>targetFramework</key>
+ <string>IBCocoaFramework</string>
+</dict>
+</plist>
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib
new file mode 100644
index 0000000000..6c93849b94
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/English.lproj/MainMenuRTL.nib/keyedobjects.nib
Binary files differ
diff --git a/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns b/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns
new file mode 100644
index 0000000000..341cd05a4d
--- /dev/null
+++ b/toolkit/crashreporter/client/macbuild/Contents/Resources/crashreporter.icns
Binary files differ
diff --git a/toolkit/crashreporter/client/moz.build b/toolkit/crashreporter/client/moz.build
new file mode 100644
index 0000000000..f678ca1cd6
--- /dev/null
+++ b/toolkit/crashreporter/client/moz.build
@@ -0,0 +1,96 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG["OS_TARGET"] != "Android":
+ Program("crashreporter")
+
+ UNIFIED_SOURCES += [
+ "../CrashAnnotations.cpp",
+ "crashreporter.cpp",
+ "ping.cpp",
+ ]
+
+ LOCAL_INCLUDES += [
+ "/toolkit/components/jsoncpp/include",
+ ]
+
+ USE_LIBS += [
+ "jsoncpp",
+ ]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ UNIFIED_SOURCES += [
+ "crashreporter_win.cpp",
+ ]
+ include("/toolkit/crashreporter/breakpad-client/windows/sender/objs.mozbuild")
+ SOURCES += objs_sender
+ SOURCES += [
+ "../google-breakpad/src/common/windows/http_upload.cc",
+ ]
+ DEFINES["UNICODE"] = True
+ DEFINES["_UNICODE"] = True
+ USE_LIBS += [
+ "nss",
+ ]
+ OS_LIBS += [
+ "advapi32",
+ "comctl32",
+ "gdi32",
+ "ole32",
+ "shell32",
+ "wininet",
+ "shlwapi",
+ "user32",
+ ]
+elif CONFIG["OS_ARCH"] == "Darwin":
+ UNIFIED_SOURCES += [
+ "../google-breakpad/src/common/mac/HTTPMultipartUpload.m",
+ "crashreporter_osx.mm",
+ "crashreporter_unix_common.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "../google-breakpad/src/common/mac",
+ ]
+ OS_LIBS += ["-framework Cocoa"]
+ USE_LIBS += [
+ "nss",
+ ]
+ LDFLAGS += ["-Wl,-rpath,@executable_path/../../../"]
+elif CONFIG["OS_ARCH"] == "SunOS":
+ SOURCES += [
+ "crashreporter_linux.cpp",
+ "crashreporter_unix.cpp",
+ ]
+ USE_LIBS += [
+ "breakpad_solaris_common_s",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ UNIFIED_SOURCES += [
+ "../google-breakpad/src/common/linux/http_upload.cc",
+ "crashreporter_gtk_common.cpp",
+ "crashreporter_linux.cpp",
+ "crashreporter_unix_common.cpp",
+ ]
+ OS_LIBS += CONFIG["MOZ_GTK3_LIBS"]
+ OS_LIBS += CONFIG["MOZ_GTHREAD_LIBS"]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_GTHREAD_CFLAGS"]
+
+if CONFIG["OS_ARCH"] == "Linux" or CONFIG["OS_ARCH"] == "SunOS":
+ FINAL_TARGET_FILES += [
+ "Throbber-small.gif",
+ ]
+
+DEFINES["BIN_SUFFIX"] = '"%s"' % CONFIG["BIN_SUFFIX"]
+
+RCINCLUDE = "crashreporter.rc"
+
+# Don't use the STL wrappers in the crashreporter clients; they don't
+# link with -lmozalloc, and it really doesn't matter here anyway.
+DisableStlWrapping()
+
+include("/toolkit/crashreporter/crashreporter.mozbuild")
diff --git a/toolkit/crashreporter/client/ping.cpp b/toolkit/crashreporter/client/ping.cpp
new file mode 100644
index 0000000000..72dc163de2
--- /dev/null
+++ b/toolkit/crashreporter/client/ping.cpp
@@ -0,0 +1,324 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "crashreporter.h"
+
+#include <cstring>
+#include <ctime>
+#include <string>
+
+#if defined(XP_LINUX)
+# include <fcntl.h>
+# include <unistd.h>
+# include <sys/stat.h>
+#elif defined(XP_MACOSX)
+# include <CoreFoundation/CoreFoundation.h>
+#elif defined(XP_WIN)
+# include <objbase.h>
+#endif
+
+#include "json/json.h"
+
+#include "CrashAnnotations.h"
+
+using std::string;
+
+namespace CrashReporter {
+
+struct UUID {
+ uint32_t m0;
+ uint16_t m1;
+ uint16_t m2;
+ uint8_t m3[8];
+};
+
+// Generates an UUID; the code here is mostly copied from nsUUIDGenerator.cpp
+static string GenerateUUID() {
+ UUID id = {};
+
+#if defined(XP_WIN) // Windows
+ HRESULT hr = CoCreateGuid((GUID*)&id);
+ if (FAILED(hr)) {
+ return "";
+ }
+#elif defined(XP_MACOSX) // MacOS X
+ CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
+ if (!uuid) {
+ return "";
+ }
+
+ CFUUIDBytes bytes = CFUUIDGetUUIDBytes(uuid);
+ memcpy(&id, &bytes, sizeof(UUID));
+
+ CFRelease(uuid);
+#elif defined(HAVE_ARC4RANDOM_BUF) // Android, BSD, ...
+ arc4random_buf(&id, sizeof(UUID));
+#else // Linux
+ int fd = open("/dev/urandom", O_RDONLY);
+
+ if (fd == -1) {
+ return "";
+ }
+
+ if (read(fd, &id, sizeof(UUID)) != sizeof(UUID)) {
+ close(fd);
+ return "";
+ }
+
+ close(fd);
+#endif
+
+ /* Put in the version */
+ id.m2 &= 0x0fff;
+ id.m2 |= 0x4000;
+
+ /* Put in the variant */
+ id.m3[0] &= 0x3f;
+ id.m3[0] |= 0x80;
+
+ const char* kUUIDFormatString =
+ "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x";
+ const size_t kUUIDFormatStringLength = 36;
+ char str[kUUIDFormatStringLength + 1] = {'\0'};
+
+ int num = snprintf(str, kUUIDFormatStringLength + 1, kUUIDFormatString, id.m0,
+ id.m1, id.m2, id.m3[0], id.m3[1], id.m3[2], id.m3[3],
+ id.m3[4], id.m3[5], id.m3[6], id.m3[7]);
+
+ if (num != kUUIDFormatStringLength) {
+ return "";
+ }
+
+ return str;
+}
+
+const char kISO8601Date[] = "%F";
+const char kISO8601DateHours[] = "%FT%H:00:00.000Z";
+
+// Return the current date as a string in the specified format, the following
+// constants are provided:
+// - kISO8601Date, the ISO 8601 date format, YYYY-MM-DD
+// - kISO8601DateHours, the ISO 8601 full date format, YYYY-MM-DDTHH:00:00.000Z
+static string CurrentDate(string format) {
+ time_t now;
+ time(&now);
+ char buf[64]; // This should be plenty
+ strftime(buf, sizeof buf, format.c_str(), gmtime(&now));
+ return buf;
+}
+
+const char kTelemetryClientId[] = "TelemetryClientId";
+const char kTelemetryUrl[] = "TelemetryServerURL";
+const char kTelemetrySessionId[] = "TelemetrySessionId";
+const int kTelemetryVersion = 4;
+
+// Create the payload.metadata node of the crash ping using fields extracted
+// from the .extra file
+static Json::Value CreateMetadataNode(const Json::Value& aExtra) {
+ Json::Value node;
+
+ for (Json::ValueConstIterator iter = aExtra.begin(); iter != aExtra.end();
+ ++iter) {
+ Annotation annotation;
+
+ if (AnnotationFromString(annotation, iter.memberName())) {
+ if (IsAnnotationAllowlistedForPing(annotation)) {
+ node[iter.memberName()] = *iter;
+ }
+ }
+ }
+
+ return node;
+}
+
+// Create the payload node of the crash ping
+static Json::Value CreatePayloadNode(const Json::Value& aExtra,
+ const string& aHash,
+ const string& aSessionId) {
+ Json::Value payload;
+
+ payload["sessionId"] = aSessionId;
+ payload["version"] = 1;
+ payload["crashDate"] = CurrentDate(kISO8601Date);
+ payload["crashTime"] = CurrentDate(kISO8601DateHours);
+ payload["hasCrashEnvironment"] = true;
+ payload["crashId"] = CrashReporter::GetDumpLocalID();
+ payload["minidumpSha256Hash"] = aHash;
+ payload["processType"] = "main"; // This is always a main crash
+ if (aExtra.isMember("StackTraces")) {
+ payload["stackTraces"] = aExtra["StackTraces"];
+ }
+
+ // Assemble the payload metadata
+ payload["metadata"] = CreateMetadataNode(aExtra);
+
+ return payload;
+}
+
+// Create the application node of the crash ping
+static Json::Value CreateApplicationNode(
+ const string& aVendor, const string& aName, const string& aVersion,
+ const string& aDisplayVersion, const string& aPlatformVersion,
+ const string& aChannel, const string& aBuildId, const string& aArchitecture,
+ const string& aXpcomAbi) {
+ Json::Value application;
+
+ application["vendor"] = aVendor;
+ application["name"] = aName;
+ application["buildId"] = aBuildId;
+ application["displayVersion"] = aDisplayVersion;
+ application["platformVersion"] = aPlatformVersion;
+ application["version"] = aVersion;
+ application["channel"] = aChannel;
+ if (!aArchitecture.empty()) {
+ application["architecture"] = aArchitecture;
+ }
+ if (!aXpcomAbi.empty()) {
+ application["xpcomAbi"] = aXpcomAbi;
+ }
+
+ return application;
+}
+
+// Create the root node of the crash ping
+static Json::Value CreateRootNode(
+ const Json::Value& aExtra, const string& aUuid, const string& aHash,
+ const string& aClientId, const string& aSessionId, const string& aName,
+ const string& aVersion, const string& aChannel, const string& aBuildId) {
+ Json::Value root;
+ root["type"] = "crash"; // This is a crash ping
+ root["id"] = aUuid;
+ root["version"] = kTelemetryVersion;
+ root["creationDate"] = CurrentDate(kISO8601DateHours);
+ root["clientId"] = aClientId;
+
+ // Parse the telemetry environment
+ Json::Value environment;
+ Json::Reader reader;
+ string architecture;
+ string xpcomAbi;
+ string displayVersion;
+ string platformVersion;
+
+ if (reader.parse(aExtra["TelemetryEnvironment"].asString(), environment,
+ /* collectComments */ false)) {
+ if (environment.isMember("build") && environment["build"].isObject()) {
+ Json::Value build = environment["build"];
+ if (build.isMember("architecture") && build["architecture"].isString()) {
+ architecture = build["architecture"].asString();
+ }
+ if (build.isMember("xpcomAbi") && build["xpcomAbi"].isString()) {
+ xpcomAbi = build["xpcomAbi"].asString();
+ }
+ if (build.isMember("displayVersion") &&
+ build["displayVersion"].isString()) {
+ displayVersion = build["displayVersion"].asString();
+ }
+ if (build.isMember("platformVersion") &&
+ build["platformVersion"].isString()) {
+ platformVersion = build["platformVersion"].asString();
+ }
+ }
+
+ root["environment"] = environment;
+ }
+
+ root["payload"] = CreatePayloadNode(aExtra, aHash, aSessionId);
+ root["application"] = CreateApplicationNode(
+ aExtra["Vendor"].asString(), aName, aVersion, displayVersion,
+ platformVersion, aChannel, aBuildId, architecture, xpcomAbi);
+
+ return root;
+}
+
+// Generates the URL used to submit the crash ping, see TelemetrySend.sys.mjs
+string GenerateSubmissionUrl(const string& aUrl, const string& aId,
+ const string& aName, const string& aVersion,
+ const string& aChannel, const string& aBuildId) {
+ return aUrl + "/submit/telemetry/" + aId + "/crash/" + aName + "/" +
+ aVersion + "/" + aChannel + "/" + aBuildId +
+ "?v=" + std::to_string(kTelemetryVersion);
+}
+
+// Write out the ping into the specified file.
+//
+// Returns true if the ping was written out successfully, false otherwise.
+static bool WritePing(const string& aPath, const string& aPing) {
+ std::ofstream* f = UIOpenWrite(aPath, std::ios::trunc);
+ bool success = false;
+
+ if (f->is_open()) {
+ *f << aPing;
+ f->close();
+ success = f->good();
+ }
+
+ delete f;
+ return success;
+}
+
+// Assembles the crash ping using the JSON data extracted from the .extra file
+// and sends it using the crash sender. All the telemetry specific data but the
+// environment will be stripped from the annotations so that it won't be sent
+// together with the crash report.
+//
+// Note that the crash ping sender is invoked in a fire-and-forget way so this
+// won't block waiting for the ping to be delivered.
+//
+// Returns true if the ping was assembled and handed over to the pingsender
+// correctly, also populates the aPingUuid parameter with the ping UUID. Returns
+// false otherwise and leaves the aPingUuid parameter unmodified.
+bool SendCrashPing(Json::Value& aExtra, const string& aHash, string& aPingUuid,
+ const string& pingDir) {
+ // Remove the telemetry-related data from the crash annotations
+ Json::Value value;
+ aExtra.removeMember(kTelemetryClientId, &value);
+ string clientId = value.asString();
+ aExtra.removeMember(kTelemetryUrl, &value);
+ string serverUrl = value.asString();
+ aExtra.removeMember(kTelemetrySessionId, &value);
+ string sessionId = value.asString();
+
+ if (clientId.empty() || serverUrl.empty() || sessionId.empty()) {
+ return false;
+ }
+
+ string buildId = aExtra["BuildID"].asString();
+ string channel = aExtra["ReleaseChannel"].asString();
+ string name = aExtra["ProductName"].asString();
+ string version = aExtra["Version"].asString();
+ string uuid = GenerateUUID();
+ string url =
+ GenerateSubmissionUrl(serverUrl, uuid, name, version, channel, buildId);
+
+ if (serverUrl.empty() || uuid.empty()) {
+ return false;
+ }
+
+ Json::Value root = CreateRootNode(aExtra, uuid, aHash, clientId, sessionId,
+ name, version, channel, buildId);
+
+ // Write out the result to the pending pings directory
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ string ping = Json::writeString(builder, root);
+ string pingPath = pingDir + UI_DIR_SEPARATOR + uuid + ".json";
+
+ if (!WritePing(pingPath, ping)) {
+ return false;
+ }
+
+ // Hand over the ping to the sender
+ std::vector<string> args = {url, pingPath};
+ if (UIRunProgram(CrashReporter::GetProgramPath(UI_PING_SENDER_FILENAME),
+ args)) {
+ aPingUuid = uuid;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+} // namespace CrashReporter
diff --git a/toolkit/crashreporter/client/resource.h b/toolkit/crashreporter/client/resource.h
new file mode 100644
index 0000000000..2e7917daa4
--- /dev/null
+++ b/toolkit/crashreporter/client/resource.h
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by crashreporter.rc
+//
+#define IDD_SENDDIALOG 102
+#define IDR_THROBBER 103
+#define IDD_VIEWREPORTDIALOG 104
+#define IDI_MAINICON 105
+#define IDC_PROGRESS 1003
+#define IDC_DESCRIPTIONTEXT 1004
+#define IDC_CLOSEBUTTON 1005
+#define IDC_VIEWREPORTBUTTON 1006
+#define IDC_SUBMITREPORTCHECK 1007
+#define IDC_INCLUDEURLCHECK 1010
+#define IDC_COMMENTTEXT 1011
+#define IDC_RESTARTBUTTON 1012
+#define IDC_DESCRIPTIONLABEL 1013
+#define IDC_PROGRESSTEXT 1014
+#define IDC_THROBBER 1015
+#define IDC_VIEWREPORTTEXT 1016
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+# ifndef APSTUDIO_READONLY_SYMBOLS
+# define _APS_NEXT_RESOURCE_VALUE 106
+# define _APS_NEXT_COMMAND_VALUE 40001
+# define _APS_NEXT_CONTROL_VALUE 1017
+# define _APS_NEXT_SYMED_VALUE 101
+# endif
+#endif