/* -*- 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 <sstream>
#include <memory>
#include <time.h>
#include <stdlib.h>
#include <string.h>

using std::string;
using std::istream;
using std::ifstream;
using std::istringstream;
using std::ostringstream;
using std::ostream;
using std::ofstream;
using std::vector;
using std::auto_ptr;

namespace CrashReporter {

StringTable  gStrings;
string       gSettingsPath;
string       gEventsPath;
int          gArgc;
char**       gArgv;

enum SubmissionResult {Succeeded, Failed};

static auto_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)
{
  string errorMessage;
  if (!gStrings[ST_CRASHREPORTERERROR].empty()) {
    char buf[2048];
    UI_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;
}

static string Escape(const string& str)
{
  string ret;
  for (string::const_iterator iter = str.begin();
       iter != str.end();
       iter++) {
    if (*iter == '\\') {
      ret += "\\\\";
    } else if (*iter == '\n') {
      ret += "\\n";
    } else if (*iter == '\t') {
      ret += "\\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);
  bool success = false;
  if (f->is_open()) {
    success = ReadStrings(*f, strings, unescape);
    f->close();
  }

  delete f;
  return success;
}

bool WriteStrings(ostream& out,
                  const string& header,
                  StringTable& strings,
                  bool escape)
{
  out << "[" << header << "]" << std::endl;
  for (StringTable::iterator iter = strings.begin();
       iter != strings.end();
       iter++) {
    out << iter->first << "=";
    if (escape)
      out << Escape(iter->second);
    else
      out << iter->second;

    out << std::endl;
  }

  return true;
}

bool WriteStringsToFile(const string& path,
                        const string& header,
                        StringTable& strings,
                        bool escape)
{
  ofstream* f = UIOpenWrite(path.c_str());
  bool success = false;
  if (f->is_open()) {
    success = WriteStrings(*f, header, strings, escape);
    f->close();
  }

  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);
  else
    return file;
}

static string GetDumpLocalID()
{
  string localId = Basename(gReporterDumpFile);
  string::size_type dot = localId.rfind('.');

  if (dot == string::npos)
    return "";

  return localId.substr(0, dot);
}

// This appends the StackTraces entry generated by the minidump analyzer to the
// main crash event so that it can be picked up by Firefox once it restarts
static void AppendStackTracesToEventFile(const string& aStackTraces)
{
  if (gEventsPath.empty()) {
    // If there is no path for finding the crash event, skip this step.
    return;
  }

  string localId = GetDumpLocalID();
  string path = gEventsPath + UI_DIR_SEPARATOR + localId;
  ofstream* f = UIOpenWrite(path.c_str(), true);

  if (f->is_open()) {
    *f << "StackTraces=" << aStackTraces;
    f->close();
  }

  delete f;
}

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 = GetDumpLocalID();
  string fpath = gEventsPath + UI_DIR_SEPARATOR + localId + "-submission";
  ofstream* f = UIOpenWrite(fpath.c_str(), false, true);
  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.c_str(), true));
}

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);
    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);
  if (!file->is_open()) {
    delete file;
    return false;
  }

  char buf[1024];
  UI_SNPRINTF(buf, 1024,
              gStrings["CrashID"].c_str(),
              responseItems["CrashID"].c_str());
  *file << buf << "\n";

  if (responseItems.find("ViewURL") != responseItems.end()) {
    UI_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, "");
  }
}

bool ShouldEnableSending()
{
  srand(time(0));
  return ((rand() % 100) < MOZ_CRASHREPORTER_ENABLE_PERCENT);
}

} // namespace CrashReporter

using namespace CrashReporter;

void RewriteStrings(StringTable& queryParameters)
{
  // rewrite some UI strings with the values from the query parameters
  string product = queryParameters["ProductName"];
  string vendor = queryParameters["Vendor"];
  if (vendor.empty()) {
    // Assume Mozilla if no vendor is specified
    vendor = "Mozilla";
  }

  char buf[4096];
  UI_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
    UI_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;
  }

  UI_SNPRINTF(buf, sizeof(buf),
              gStrings[ST_CRASHREPORTERDESCRIPTION].c_str(),
              product.c_str());
  gStrings[ST_CRASHREPORTERDESCRIPTION] = buf;

  UI_SNPRINTF(buf, sizeof(buf),
              gStrings[ST_CHECKSUBMIT].c_str(),
              vendor.c_str());
  gStrings[ST_CHECKSUBMIT] = buf;

  UI_SNPRINTF(buf, sizeof(buf),
              gStrings[ST_CHECKEMAIL].c_str(),
              vendor.c_str());
  gStrings[ST_CHECKEMAIL] = buf;

  UI_SNPRINTF(buf, sizeof(buf),
              gStrings[ST_RESTART].c_str(),
              product.c_str());
  gStrings[ST_RESTART] = buf;

  UI_SNPRINTF(buf, sizeof(buf),
              gStrings[ST_QUIT].c_str(),
              product.c_str());
  gStrings[ST_QUIT] = buf;

  UI_SNPRINTF(buf, sizeof(buf),
              gStrings[ST_ERROR_ENDOFLIFE].c_str(),
              product.c_str());
  gStrings[ST_ERROR_ENDOFLIFE] = buf;
}

bool CheckEndOfLifed(string version)
{
  string reportPath =
    gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + version;
  return UIFileExists(reportPath);
}

#ifndef RELEASE_OR_BETA

static string
GetMinidumpAnalyzerPath()
{
  string path = gArgv[0];
  size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX);
  path.erase(pos);
  path.append(UI_MINIDUMP_ANALYZER_FILENAME BIN_SUFFIX);

  return path;
}

#endif

int main(int argc, char** argv)
{
  gArgc = argc;
  gArgv = argv;

  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
    UIShowDefaultUI();
  } else {
#ifndef RELEASE_OR_BETA
    // start by running minidump analyzer, this is currently enabled only in
    // nightly and aurora
    UIRunMinidumpAnalyzer(GetMinidumpAnalyzerPath(), gReporterDumpFile);
#endif

    // 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();
    }

    StringTable queryParameters;
    if (!ReadStringsFromFile(gExtraFile, queryParameters, true)) {
      UIError(gStrings[ST_ERROR_EXTRAFILEREAD]);
      return 0;
    }

    if (queryParameters.find("ProductName") == queryParameters.end()) {
      UIError(gStrings[ST_ERROR_NOPRODUCTNAME]);
      return 0;
    }

    // There is enough information in the extra file to rewrite strings
    // to be product specific
    RewriteStrings(queryParameters);

    if (queryParameters.find("ServerURL") == queryParameters.end()) {
      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.
#ifdef XP_WIN32
    static const wchar_t kDataDirKey[] = L"MOZ_CRASHREPORTER_DATA_DIRECTORY";
    const wchar_t *settingsPath = _wgetenv(kDataDirKey);
    if (settingsPath && *settingsPath) {
      gSettingsPath = WideToUTF8(settingsPath);
    }
#else
    static const char kDataDirKey[] = "MOZ_CRASHREPORTER_DATA_DIRECTORY";
    const char *settingsPath = getenv(kDataDirKey);
    if (settingsPath && *settingsPath) {
      gSettingsPath = settingsPath;
    }
#endif
    else {
      string product = queryParameters["ProductName"];
      string vendor = queryParameters["Vendor"];
      if (!UIGetSettingsPath(vendor, product, gSettingsPath)) {
        gSettingsPath.clear();
      }
    }

    if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) {
      UIError(gStrings[ST_ERROR_NOSETTINGSPATH]);
      return 0;
    }

    OpenLogFile();

#ifdef XP_WIN32
    static const wchar_t kEventsDirKey[] = L"MOZ_CRASHREPORTER_EVENTS_DIRECTORY";
    const wchar_t *eventsPath = _wgetenv(kEventsDirKey);
    if (eventsPath && *eventsPath) {
      gEventsPath = WideToUTF8(eventsPath);
    }
#else
    static const char kEventsDirKey[] = "MOZ_CRASHREPORTER_EVENTS_DIRECTORY";
    const char *eventsPath = getenv(kEventsDirKey);
    if (eventsPath && *eventsPath) {
      gEventsPath = eventsPath;
    }
#endif
    else {
      gEventsPath.clear();
    }

    // Update the crash event with stacks if they are present
    auto stackTracesItr = queryParameters.find("StackTraces");
    if (stackTracesItr != queryParameters.end()) {
      AppendStackTracesToEventFile(stackTracesItr->second);
    }

    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 = queryParameters["ServerURL"];
    // we don't need to actually send this
    queryParameters.erase("ServerURL");

    queryParameters["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 (queryParameters.find("Version") != queryParameters.end() &&
         CheckEndOfLifed(queryParameters["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, queryParameters, 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