/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "mozilla/dom/KeyboardEvent.h"

#include "mozilla/BasicEvents.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/Document.h"
#include "mozilla/LookAndFeel.h"
#include "nsContentUtils.h"
#include "nsIPrincipal.h"
#include "nsRFPService.h"
#include "prtime.h"

namespace mozilla::dom {

KeyboardEvent::KeyboardEvent(EventTarget* aOwner, nsPresContext* aPresContext,
                             WidgetKeyboardEvent* aEvent)
    : UIEvent(aOwner, aPresContext,
              aEvent ? aEvent
                     : new WidgetKeyboardEvent(false, eVoidEvent, nullptr)),
      mInitializedByJS(false),
      mInitializedByCtor(false),
      mInitializedWhichValue(0) {
  if (aEvent) {
    mEventIsInternal = false;
  } else {
    mEventIsInternal = true;
    mEvent->AsKeyboardEvent()->mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
  }
}

bool KeyboardEvent::IsMenuAccessKeyPressed() const {
  Modifiers mask = LookAndFeel::GetMenuAccessKeyModifiers();
  Modifiers modifiers = GetModifiersForMenuAccessKey();
  return mask != MODIFIER_SHIFT && (modifiers & mask) &&
         (modifiers & ~(mask | MODIFIER_SHIFT)) == 0;
}

static constexpr Modifiers kPossibleModifiersForAccessKey =
    MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META |
    MODIFIER_OS;

Modifiers KeyboardEvent::GetModifiersForMenuAccessKey() const {
  const WidgetInputEvent* inputEvent = WidgetEventPtr()->AsInputEvent();
  MOZ_ASSERT(inputEvent);
  return inputEvent->mModifiers & kPossibleModifiersForAccessKey;
}

bool KeyboardEvent::AltKey(CallerType aCallerType) {
  bool altState = mEvent->AsKeyboardEvent()->IsAlt();

  if (!ShouldResistFingerprinting(aCallerType)) {
    return altState;
  }

  // We need to give a spoofed state for Alt key since it could be used as a
  // modifier key in certain keyboard layout. For example, the '@' key for
  // German keyboard for MAC is Alt+L.
  return GetSpoofedModifierStates(Modifier::MODIFIER_ALT, altState);
}

bool KeyboardEvent::CtrlKey(CallerType aCallerType) {
  // We don't spoof this key when privacy.resistFingerprinting
  // is enabled, because it is often used for command key
  // combinations in web apps.
  return mEvent->AsKeyboardEvent()->IsControl();
}

bool KeyboardEvent::ShiftKey(CallerType aCallerType) {
  bool shiftState = mEvent->AsKeyboardEvent()->IsShift();

  if (!ShouldResistFingerprinting(aCallerType)) {
    return shiftState;
  }

  return GetSpoofedModifierStates(Modifier::MODIFIER_SHIFT, shiftState);
}

bool KeyboardEvent::MetaKey() {
  // We don't spoof this key when privacy.resistFingerprinting
  // is enabled, because it is often used for command key
  // combinations in web apps.
  return mEvent->AsKeyboardEvent()->IsMeta();
}

bool KeyboardEvent::Repeat() { return mEvent->AsKeyboardEvent()->mIsRepeat; }

bool KeyboardEvent::IsComposing() {
  return mEvent->AsKeyboardEvent()->mIsComposing;
}

void KeyboardEvent::GetKey(nsAString& aKeyName) const {
  mEvent->AsKeyboardEvent()->GetDOMKeyName(aKeyName);
}

void KeyboardEvent::GetCode(nsAString& aCodeName, CallerType aCallerType) {
  if (!ShouldResistFingerprinting(aCallerType)) {
    mEvent->AsKeyboardEvent()->GetDOMCodeName(aCodeName);
    return;
  }

  // When fingerprinting resistance is enabled, we will give a spoofed code
  // according to the content-language of the document.
  nsCOMPtr<Document> doc = GetDocument();

  nsRFPService::GetSpoofedCode(doc, mEvent->AsKeyboardEvent(), aCodeName);
}

void KeyboardEvent::GetInitDict(KeyboardEventInit& aParam) {
  GetKey(aParam.mKey);
  GetCode(aParam.mCode);
  aParam.mLocation = Location();
  aParam.mRepeat = Repeat();
  aParam.mIsComposing = IsComposing();

  // legacy attributes
  aParam.mKeyCode = KeyCode();
  aParam.mCharCode = CharCode();
  aParam.mWhich = Which();

  // modifiers from EventModifierInit
  aParam.mCtrlKey = CtrlKey();
  aParam.mShiftKey = ShiftKey();
  aParam.mAltKey = AltKey();
  aParam.mMetaKey = MetaKey();

  WidgetKeyboardEvent* internalEvent = mEvent->AsKeyboardEvent();
  aParam.mModifierAltGraph = internalEvent->IsAltGraph();
  aParam.mModifierCapsLock = internalEvent->IsCapsLocked();
  aParam.mModifierFn = internalEvent->IsFn();
  aParam.mModifierFnLock = internalEvent->IsFnLocked();
  aParam.mModifierNumLock = internalEvent->IsNumLocked();
  aParam.mModifierOS = internalEvent->IsOS();
  aParam.mModifierScrollLock = internalEvent->IsScrollLocked();
  aParam.mModifierSymbol = internalEvent->IsSymbol();
  aParam.mModifierSymbolLock = internalEvent->IsSymbolLocked();

  // EventInit
  aParam.mBubbles = internalEvent->mFlags.mBubbles;
  aParam.mCancelable = internalEvent->mFlags.mCancelable;
}

bool KeyboardEvent::ShouldUseSameValueForCharCodeAndKeyCode(
    const WidgetKeyboardEvent& aWidgetKeyboardEvent,
    CallerType aCallerType) const {
  // - If this event is initialized by JS, we don't need to return same value
  //   for keyCode and charCode since they can be initialized separately.
  // - If this is not a keypress event, we shouldn't return same value for
  //   keyCode and charCode.
  // - If we need to return legacy keyCode and charCode values for the web
  //   app due to in the blacklist.
  // - If this event is referred by default handler, i.e., the caller is
  //   system or this event is now in the system group, we don't need to use
  //   hack for web-compat.
  if (mInitializedByJS || aWidgetKeyboardEvent.mMessage != eKeyPress ||
      aWidgetKeyboardEvent.mUseLegacyKeyCodeAndCharCodeValues ||
      aCallerType == CallerType::System ||
      aWidgetKeyboardEvent.mFlags.mInSystemGroup) {
    return false;
  }

  MOZ_ASSERT(aCallerType == CallerType::NonSystem);

  return StaticPrefs::
      dom_keyboardevent_keypress_set_keycode_and_charcode_to_same_value();
}

uint32_t KeyboardEvent::CharCode(CallerType aCallerType) {
  WidgetKeyboardEvent* widgetKeyboardEvent = mEvent->AsKeyboardEvent();
  if (mInitializedByJS) {
    // If this is initialized by Ctor, we should return the initialized value.
    if (mInitializedByCtor) {
      return widgetKeyboardEvent->mCharCode;
    }
    // Otherwise, i.e., initialized by InitKey*Event(), we should return the
    // initialized value only when eKeyPress or eAccessKeyNotFound event.
    // Although this is odd, but our traditional behavior.
    return widgetKeyboardEvent->mMessage == eKeyPress ||
                   widgetKeyboardEvent->mMessage == eAccessKeyNotFound
               ? widgetKeyboardEvent->mCharCode
               : 0;
  }

  // If the key is a function key, we should return the result of KeyCode()
  // even from CharCode().  Otherwise, i.e., the key may be a printable
  // key or actually a printable key, we should return the given charCode
  // value.

  if (widgetKeyboardEvent->mKeyNameIndex != KEY_NAME_INDEX_USE_STRING &&
      ShouldUseSameValueForCharCodeAndKeyCode(*widgetKeyboardEvent,
                                              aCallerType)) {
    return ComputeTraditionalKeyCode(*widgetKeyboardEvent, aCallerType);
  }

  return widgetKeyboardEvent->mCharCode;
}

uint32_t KeyboardEvent::KeyCode(CallerType aCallerType) {
  WidgetKeyboardEvent* widgetKeyboardEvent = mEvent->AsKeyboardEvent();
  if (mInitializedByJS) {
    // If this is initialized by Ctor, we should return the initialized value.
    if (mInitializedByCtor) {
      return widgetKeyboardEvent->mKeyCode;
    }
    // Otherwise, i.e., initialized by InitKey*Event(), we should return the
    // initialized value only when the event message is a valid keyboard event
    // message.  Although this is odd, but our traditional behavior.
    // NOTE: The fix of bug 1222285 changed the behavior temporarily if
    //       spoofing is enabled.  However, the behavior does not make sense
    //       since if the event is generated by JS, the behavior shouldn't
    //       be changed by whether spoofing is enabled or not.  Therefore,
    //       we take back the original behavior.
    return widgetKeyboardEvent->HasKeyEventMessage()
               ? widgetKeyboardEvent->mKeyCode
               : 0;
  }

  // If the key is not a function key, i.e., the key may be a printable key
  // or a function key mapped as a printable key, we should use charCode value
  // for keyCode value if this is a "keypress" event.

  if (widgetKeyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
      ShouldUseSameValueForCharCodeAndKeyCode(*widgetKeyboardEvent,
                                              aCallerType)) {
    return widgetKeyboardEvent->mCharCode;
  }

  return ComputeTraditionalKeyCode(*widgetKeyboardEvent, aCallerType);
}

uint32_t KeyboardEvent::ComputeTraditionalKeyCode(
    WidgetKeyboardEvent& aKeyboardEvent, CallerType aCallerType) {
  if (!ShouldResistFingerprinting(aCallerType)) {
    return aKeyboardEvent.mKeyCode;
  }

  // In Netscape style (i.e., traditional behavior of Gecko), the keyCode
  // should be zero if the char code is given.
  if ((mEvent->mMessage == eKeyPress ||
       mEvent->mMessage == eAccessKeyNotFound) &&
      aKeyboardEvent.mCharCode) {
    return 0;
  }

  // When fingerprinting resistance is enabled, we will give a spoofed keyCode
  // according to the content-language of the document.
  nsCOMPtr<Document> doc = GetDocument();
  uint32_t spoofedKeyCode;

  if (nsRFPService::GetSpoofedKeyCode(doc, &aKeyboardEvent, spoofedKeyCode)) {
    return spoofedKeyCode;
  }

  return 0;
}

uint32_t KeyboardEvent::Which(CallerType aCallerType) {
  // If this event is initialized with ctor, which can have independent value.
  if (mInitializedByCtor) {
    return mInitializedWhichValue;
  }

  switch (mEvent->mMessage) {
    case eKeyDown:
    case eKeyUp:
      return KeyCode(aCallerType);
    case eKeyPress:
      // Special case for 4xp bug 62878.  Try to make value of which
      // more closely mirror the values that 4.x gave for RETURN and BACKSPACE
      {
        uint32_t keyCode = mEvent->AsKeyboardEvent()->mKeyCode;
        if (keyCode == NS_VK_RETURN || keyCode == NS_VK_BACK) {
          return keyCode;
        }
        return CharCode();
      }
    default:
      break;
  }

  return 0;
}

uint32_t KeyboardEvent::Location() {
  return mEvent->AsKeyboardEvent()->mLocation;
}

// static
already_AddRefed<KeyboardEvent> KeyboardEvent::ConstructorJS(
    const GlobalObject& aGlobal, const nsAString& aType,
    const KeyboardEventInit& aParam) {
  nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports());
  RefPtr<KeyboardEvent> newEvent = new KeyboardEvent(target, nullptr, nullptr);
  newEvent->InitWithKeyboardEventInit(target, aType, aParam);

  return newEvent.forget();
}

void KeyboardEvent::InitWithKeyboardEventInit(EventTarget* aOwner,
                                              const nsAString& aType,
                                              const KeyboardEventInit& aParam) {
  bool trusted = Init(aOwner);
  InitKeyEventJS(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView,
                 false, false, false, false, aParam.mKeyCode, aParam.mCharCode);
  InitModifiers(aParam);
  SetTrusted(trusted);
  mDetail = aParam.mDetail;
  mInitializedByJS = true;
  mInitializedByCtor = true;
  mInitializedWhichValue = aParam.mWhich;

  WidgetKeyboardEvent* internalEvent = mEvent->AsKeyboardEvent();
  internalEvent->mLocation = aParam.mLocation;
  internalEvent->mIsRepeat = aParam.mRepeat;
  internalEvent->mIsComposing = aParam.mIsComposing;
  internalEvent->mKeyNameIndex =
      WidgetKeyboardEvent::GetKeyNameIndex(aParam.mKey);
  if (internalEvent->mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
    internalEvent->mKeyValue = aParam.mKey;
  }
  internalEvent->mCodeNameIndex =
      WidgetKeyboardEvent::GetCodeNameIndex(aParam.mCode);
  if (internalEvent->mCodeNameIndex == CODE_NAME_INDEX_USE_STRING) {
    internalEvent->mCodeValue = aParam.mCode;
  }
}

// static
bool KeyboardEvent::IsInitKeyEventAvailable(JSContext* aCx, JSObject*) {
  if (StaticPrefs::dom_keyboardevent_init_key_event_enabled()) {
    return true;
  }
  if (!StaticPrefs::dom_keyboardevent_init_key_event_enabled_in_addons()) {
    return false;
  }
  nsIPrincipal* principal = nsContentUtils::SubjectPrincipal(aCx);
  return principal && principal->GetIsAddonOrExpandedAddonPrincipal();
}

void KeyboardEvent::InitKeyEventJS(const nsAString& aType, bool aCanBubble,
                                   bool aCancelable, nsGlobalWindowInner* aView,
                                   bool aCtrlKey, bool aAltKey, bool aShiftKey,
                                   bool aMetaKey, uint32_t aKeyCode,
                                   uint32_t aCharCode) {
  NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched);
  mInitializedByJS = true;
  mInitializedByCtor = false;

  UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, 0);

  WidgetKeyboardEvent* keyEvent = mEvent->AsKeyboardEvent();
  keyEvent->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, aMetaKey);
  keyEvent->mKeyCode = aKeyCode;
  keyEvent->mCharCode = aCharCode;
}

void KeyboardEvent::InitKeyboardEventJS(
    const nsAString& aType, bool aCanBubble, bool aCancelable,
    nsGlobalWindowInner* aView, const nsAString& aKey, uint32_t aLocation,
    bool aCtrlKey, bool aAltKey, bool aShiftKey, bool aMetaKey) {
  NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched);
  mInitializedByJS = true;
  mInitializedByCtor = false;

  UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, 0);

  WidgetKeyboardEvent* keyEvent = mEvent->AsKeyboardEvent();
  keyEvent->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, aMetaKey);
  keyEvent->mLocation = aLocation;
  keyEvent->mKeyNameIndex = KEY_NAME_INDEX_USE_STRING;
  keyEvent->mKeyValue = aKey;
}

bool KeyboardEvent::ShouldResistFingerprinting(CallerType aCallerType) {
  // There are five situations we don't need to spoof this keyboard event.
  //   1. The pref privcy.resistFingerprinting' is false, we fast return here.
  //   2. This event is initialized by scripts.
  //   3. This event is from Numpad.
  //   4. This event is in the system group.
  //   5. The caller type is system.
  if (!nsContentUtils::ShouldResistFingerprinting("Efficiency Check",
                                                  RFPTarget::KeyboardEvents) ||
      mInitializedByJS || aCallerType == CallerType::System ||
      mEvent->mFlags.mInSystemGroup ||
      mEvent->AsKeyboardEvent()->mLocation ==
          KeyboardEvent_Binding::DOM_KEY_LOCATION_NUMPAD) {
    return false;
  }

  nsCOMPtr<Document> doc = GetDocument();
  // We've checked the pref above, so use true as fallback if doc is null.
  return doc ? doc->ShouldResistFingerprinting(RFPTarget::KeyboardEvents)
             : true;
}

bool KeyboardEvent::GetSpoofedModifierStates(const Modifiers aModifierKey,
                                             const bool aRawModifierState) {
  bool spoofedState;
  nsCOMPtr<Document> doc = GetDocument();

  if (nsRFPService::GetSpoofedModifierStates(doc, mEvent->AsKeyboardEvent(),
                                             aModifierKey, spoofedState)) {
    return spoofedState;
  }

  return aRawModifierState;
}

}  // namespace mozilla::dom

using namespace mozilla;
using namespace mozilla::dom;

already_AddRefed<KeyboardEvent> NS_NewDOMKeyboardEvent(
    EventTarget* aOwner, nsPresContext* aPresContext,
    WidgetKeyboardEvent* aEvent) {
  RefPtr<KeyboardEvent> it = new KeyboardEvent(aOwner, aPresContext, aEvent);
  return it.forget();
}
