/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:expandtab:shiftwidth=2:tabstop=8:
 */
/* vim:set ts=8 sw=2 et cindent: */
/* 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 "nsDBusRemoteClient.h"
#include "RemoteUtils.h"
#include "mozilla/XREAppData.h"
#include "mozilla/Logging.h"
#include "mozilla/Base64.h"
#include "nsPrintfCString.h"

#include <dlfcn.h>
#include <dbus/dbus-glib-lowlevel.h>

#undef LOG
#ifdef MOZ_LOGGING
static mozilla::LazyLogModule sRemoteLm("nsDBusRemoteClient");
#  define LOG(str, ...) \
    MOZ_LOG(sRemoteLm, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__))
#else
#  define LOG(...)
#endif

nsDBusRemoteClient::nsDBusRemoteClient() {
  mConnection = nullptr;
  LOG("nsDBusRemoteClient::nsDBusRemoteClient");
}

nsDBusRemoteClient::~nsDBusRemoteClient() {
  LOG("nsDBusRemoteClient::~nsDBusRemoteClient");
  Shutdown();
}

nsresult nsDBusRemoteClient::Init() {
  LOG("nsDBusRemoteClient::Init");

  if (mConnection) return NS_OK;

  mConnection =
      already_AddRefed<DBusConnection>(dbus_bus_get(DBUS_BUS_SESSION, nullptr));
  if (!mConnection) {
    LOG("  failed to get DBus session");
    return NS_ERROR_FAILURE;
  }

  dbus_connection_set_exit_on_disconnect(mConnection, false);
  dbus_connection_setup_with_g_main(mConnection, nullptr);

  return NS_OK;
}

void nsDBusRemoteClient::Shutdown(void) {
  LOG("nsDBusRemoteClient::Shutdown");
  // This connection is owned by libdbus and we don't need to close it
  mConnection = nullptr;
}

nsresult nsDBusRemoteClient::SendCommandLine(
    const char* aProgram, const char* aProfile, int32_t argc, char** argv,
    const char* aStartupToken, char** aResponse, bool* aWindowFound) {
  NS_ENSURE_TRUE(aProgram, NS_ERROR_INVALID_ARG);

  LOG("nsDBusRemoteClient::SendCommandLine");

  int commandLineLength;
  char* commandLine =
      ConstructCommandLine(argc, argv, aStartupToken, &commandLineLength);
  if (!commandLine) {
    LOG("  failed to create command line");
    return NS_ERROR_FAILURE;
  }

  nsresult rv =
      DoSendDBusCommandLine(aProgram, aProfile, commandLine, commandLineLength);
  free(commandLine);

  *aWindowFound = NS_SUCCEEDED(rv);

  LOG("DoSendDBusCommandLine %s", NS_SUCCEEDED(rv) ? "OK" : "FAILED");
  return rv;
}

bool nsDBusRemoteClient::GetRemoteDestinationName(const char* aProgram,
                                                  const char* aProfile,
                                                  nsCString& aDestinationName) {
  // We have a profile name - just create the destination.
  // D-Bus names can contain only [a-z][A-Z][0-9]_
  // characters so adjust the profile string properly.
  nsAutoCString profileName;
  nsresult rv = mozilla::Base64Encode(nsAutoCString(aProfile), profileName);
  NS_ENSURE_SUCCESS(rv, false);

  mozilla::XREAppData::SanitizeNameForDBus(profileName);

  aDestinationName =
      nsPrintfCString("org.mozilla.%s.%s", aProgram, profileName.get());
  if (aDestinationName.Length() > DBUS_MAXIMUM_NAME_LENGTH)
    aDestinationName.Truncate(DBUS_MAXIMUM_NAME_LENGTH);

  static auto sDBusValidateBusName = (bool (*)(const char*, DBusError*))dlsym(
      RTLD_DEFAULT, "dbus_validate_bus_name");
  if (!sDBusValidateBusName) {
    LOG("  failed to get dbus_validate_bus_name()");
    return false;
  }

  if (!sDBusValidateBusName(aDestinationName.get(), nullptr)) {
    // We don't have a valid busName yet - try to create a default one.
    aDestinationName =
        nsPrintfCString("org.mozilla.%s.%s", aProgram, "default");
    if (!sDBusValidateBusName(aDestinationName.get(), nullptr)) {
      // We failed completelly to get a valid bus name - just quit
      // to prevent crash at dbus_bus_request_name().
      LOG("  failed to validate profile DBus name");
      return false;
    }
  }

  return true;
}

nsresult nsDBusRemoteClient::DoSendDBusCommandLine(const char* aProgram,
                                                   const char* aProfile,
                                                   const char* aBuffer,
                                                   int aLength) {
  LOG("nsDBusRemoteClient::DoSendDBusCommandLine()");

  nsAutoCString appName(aProgram);
  mozilla::XREAppData::SanitizeNameForDBus(appName);

  nsAutoCString destinationName;
  if (!GetRemoteDestinationName(appName.get(), aProfile, destinationName)) {
    LOG("  failed to get remote destination name");
    return NS_ERROR_FAILURE;
  }

  nsAutoCString pathName;
  pathName = nsPrintfCString("/org/mozilla/%s/Remote", appName.get());

  static auto sDBusValidatePathName = (bool (*)(const char*, DBusError*))dlsym(
      RTLD_DEFAULT, "dbus_validate_path");
  if (!sDBusValidatePathName ||
      !sDBusValidatePathName(pathName.get(), nullptr)) {
    LOG("  failed to validate path name");
    return NS_ERROR_FAILURE;
  }

  nsAutoCString remoteInterfaceName;
  remoteInterfaceName = nsPrintfCString("org.mozilla.%s", appName.get());

  LOG("  DBus destination: %s\n", destinationName.get());
  LOG("  DBus path: %s\n", pathName.get());
  LOG("  DBus interface: %s\n", remoteInterfaceName.get());

  RefPtr<DBusMessage> msg =
      already_AddRefed<DBusMessage>(dbus_message_new_method_call(
          destinationName.get(),
          pathName.get(),             // object to call on
          remoteInterfaceName.get(),  // interface to call on
          "OpenURL"));                // method name
  if (!msg) {
    LOG("  failed to create DBus message");
    return NS_ERROR_FAILURE;
  }

  // append arguments
  if (!dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &aBuffer,
                                aLength, DBUS_TYPE_INVALID)) {
    LOG("  failed to create DBus message");
    return NS_ERROR_FAILURE;
  }

  // send message and get a handle for a reply
  RefPtr<DBusMessage> reply = already_AddRefed<DBusMessage>(
      dbus_connection_send_with_reply_and_block(mConnection, msg, -1, nullptr));

#ifdef MOZ_LOGGING
  if (!reply) {
    LOG("  failed to get DBus reply");
  }
#endif

  return reply ? NS_OK : NS_ERROR_FAILURE;
}
