<commit_before>unit Keyman.System.Process.KPInstallKeyboardLanguage;

interface

uses
  System.Win.Registry,
  Winapi.Windows,

  kpbase,
  KeymanContext,
  Windows8LanguageList,
  Winapi.msctf,
  keyman_msctf;

type
  TKPInstallKeyboardLanguageFlags = set of (ilkInstallTransientLanguage);

type
  TKPInstallKeyboardLanguage = class(TKPBase)
  public
    ///  <summary>Given a BCP47 Tag, finds a corresponding LangID, including installing
    ///  a temporary keyboard if necessary to establish a transient LangID.
    ///  Current User.</summary>
    ///  <returns>True if a language ID can be found</return>
    ///  <param name="BCP47Tag">A canonical BCP47 tag to install. May be updated
    ///  for compatibility reasons; see #1285.</param>
    ///  <param name="LangID">LangID found that corresponds to BCP47Tag</param>
    ///  <param name="TemporaryLayoutString">If a transient LangID is installed, then
    ///  Windows will have added a default keyboard for the language which should be
    ///  removed after we install the Keyman TIP.</param>
    ///  <param name="Flags">If ilkInstallTransientLanguage is included in flags,
    ///  will not install a transient language and will return False if no standard
    ///  Windows LangID can be found that corresponds to the BCP47 tag.</param>
    function FindInstallationLangID(
      var BCP47Tag: string;
      var LangID: Integer;
      var TemporaryLayoutString: string;
      Flags: TKPInstallKeyboardLanguageFlags): Boolean;

    ///  <summary>Registers a TIP for a given keyboard + language, and adds metadata to the
    ///  HKLM registry about the Keyman keyboard association. Requires elevation.</summary>
    ///  <param name="KeyboardID">Keyman keyboard to register</param>
    ///  <param name="BCP47Tag">BCP47 tag associated with the TIP</param>
    ///  <param name="KeyboardName">Name of keyboard to display in Windows UI</param>
    ///  <param name="LangID">Language ID found with FindInstallationLangID</param>
    ///  <param name="IconFileName">Icon to display in Windows UI</param>
    ///  <param name="LanguageName">Name of language to display in Keyman UI</param>
    procedure RegisterTip(const KeyboardID, BCP47Tag, KeyboardName: string; LangID: Integer; IconFileName, LanguageName: string);

    ///  <summary>Installs a TIP for a given keyboard + language, for the current user.</summary>
    ///  <param name="KeyboardID">Keyman keyboard to associate with TIP</param>
    ///  <param name="BCP47Tag">BCP47 tag associated with the TIP</param>
    ///  <param name="LangID">Language ID found with FindInstallationLangID</param>
    ///  <param name="guidProfile">Profile GUID that is assigned on exit</param>
    procedure InstallTip(const KeyboardID, BCP47Tag: string; LangID: Integer; var guidProfile: TGUID);

    ///  <summary>Removes a temporary keyboard layout that was installed by
    ///  FindInstallationLangID. Current User.</summary>
    ///  <param name="LayoutString">TemporaryLayoutString value returned from
    ///  FindInstallationLangID</param>
    procedure UninstallTemporaryLayout(const LayoutString: string);

    ///  <summary>Registers a TIP for each of the four transient language IDs,
    ///  and adds metadata to the HKLM registry about the Keyman keyboard
    ///  association. Requires elevation.</summary>
    ///  <param name="KeyboardID">Keyman keyboard to register</param>
    ///  <param name="KeyboardName">Name of keyboard to display in Windows UI</param>
    ///  <param name="IconFileName">Icon to display in Windows UI</param>
    procedure RegisterTransientTips(const KeyboardID, KeyboardName: string; IconFileName: string);

    constructor Create(AContext: TKeymanContext);
    destructor Destroy; override;
  private
    FWin8Languages: TWindows8LanguageList;
    function ConvertBCP47TagToLangID(Locale: string; var LangID: Integer): Boolean;
    function InstallBCP47Language(const FLocaleName: string): Boolean;
    function GetInputProcessorProfileMgr: ITfInputProcessorProfileMgr;
    function AreMaximumTransientLanguagesInstalled: Boolean;
  end;

implementation

uses
  System.Classes,
  System.SysUtils,
  System.Variants,
  System.Win.ComObj,
  Winapi.ActiveX,

  input_installlayoutortip,
  isadmin,
  Keyman.System.MitigateWin10_1803LanguageInstall,
  keymanerrorcodes,
  KeymanPaths,
  RegistryKeys,
  BCP47Tag,
  glossary,
  TempFileManager,
  utilexecute,
  utilfiletypes,
  utilkeyman,
  utilstr,
  utiltsf;

{ TKPInstallKeyboardLanguage }

constructor TKPInstallKeyboardLanguage.Create(AContext: TKeymanContext);
begin
  FWin8Languages := TWindows8LanguageList.Create;
  inherited Create(AContext);
end;

destructor TKPInstallKeyboardLanguage.Destroy;
begin
  FreeAndNil(FWin8Languages);
  inherited Destroy;
end;

function TKPInstallKeyboardLanguage.AreMaximumTransientLanguagesInstalled: Boolean;
const
  MAX_TRANSIENT_LANGUAGES = 4;
var
  r: TRegistry;
  keys: TStringList;
  isInstalled: array[0..MAX_TRANSIENT_LANGUAGES-1] of Boolean;
  i: Integer;
  v: Integer;
  key: string;
begin
  for i := 0 to high(isInstalled) do isInstalled[i] := False;

  // We'll use Control Panel/International/User Profile to find the full set of
  // transient languages
  r := TRegistry.Create;
  keys := TStringList.Create;
  try
    if r.OpenKeyReadOnly(SRegKey_ControlPanelInternationalUserProfile) then
    begin
      r.GetKeyNames(keys);
      for key in keys do
      begin
        if r.OpenKeyReadOnly('\' + SRegKey_ControlPanelInternationalUserProfile + '\' + key) then
        begin
          if r.ValueExists(SRegValue_CPIUP_TransientLangId) then
          begin
            v := r.ReadInteger(SRegValue_CPIUP_TransientLangId);
            // Transient languages start at $2000 and go up by $400
            v := (v - $2000) div $400;
            if (v >= 0) and (v <= High(isInstalled)) then
              isInstalled[v] := True;
          end;
        end;
      end;
    end;
  finally
    keys.Free;
    r.Free;
  end;

  for i := 0 to High(isInstalled) do
    if not isInstalled[i] then
      Exit(False);

  Result := True;
end;

// TODO: change LangID to Winapi.Windows.LANGID
function TKPInstallKeyboardLanguage.FindInstallationLangID(var BCP47Tag: string; var LangID: Integer; var TemporaryLayoutString: string; Flags: TKPInstallKeyboardLanguageFlags): Boolean;
var
  ml: TMitigateWin10_1803.TMitigatedLanguage;
  Win8Lang: TWindows8Language;
begin
  if BCP47Tag = '' then
    ErrorFmt(KMN_E_ProfileInstall_InvalidBCP47Tag, VarArrayOf([BCP47Tag]));

  TemporaryLayoutString := '';

  if ConvertBCP47TagToLangID(BCP47Tag, LangID) then
  begin
    if TMitigateWin10_1803.IsMitigationRequired(LangID, ml) then
    begin
      LangID := ml.NewLanguage.Code;
      BCP47Tag := ml.NewLanguage.BCP47;
      WarnFmt(KMN_W_ProfileInstall_Win10_1803_MitigationApplied, VarArrayOf([ml.OriginalLanguage.Name, ml.NewLanguage.Name]));
    end;

    if LangID <> 0 then
      Exit(True);
  end;
<commit_msg>chore: address review comments<commit_after>unit Keyman.System.Process.KPInstallKeyboardLanguage;

interface

uses
  System.Win.Registry,
  Winapi.Windows,

  kpbase,
  KeymanContext,
  Windows8LanguageList,
  Winapi.msctf,
  keyman_msctf;

type
  TKPInstallKeyboardLanguageFlags = set of (ilkInstallTransientLanguage);

type
  TKPInstallKeyboardLanguage = class(TKPBase)
  public
    ///  <summary>Given a BCP47 Tag, finds a corresponding LangID, including installing
    ///  a temporary keyboard if necessary to establish a transient LangID.
    ///  Current User.</summary>
    ///  <returns>True if a language ID can be found</return>
    ///  <param name="BCP47Tag">A canonical BCP47 tag to install. May be updated
    ///  for compatibility reasons; see #1285.</param>
    ///  <param name="LangID">LangID found that corresponds to BCP47Tag</param>
    ///  <param name="TemporaryLayoutString">If a transient LangID is installed, then
    ///  Windows will have added a default keyboard for the language which should be
    ///  removed after we install the Keyman TIP.</param>
    ///  <param name="Flags">If ilkInstallTransientLanguage is included in flags,
    ///  will not install a transient language and will return False if no standard
    ///  Windows LangID can be found that corresponds to the BCP47 tag.</param>
    function FindInstallationLangID(
      var BCP47Tag: string;
      var LangID: Integer;
      var TemporaryLayoutString: string;
      Flags: TKPInstallKeyboardLanguageFlags): Boolean;

    ///  <summary>Registers a TIP for a given keyboard + language, and adds metadata to the
    ///  HKLM registry about the Keyman keyboard association. Requires elevation.</summary>
    ///  <param name="KeyboardID">Keyman keyboard to register</param>
    ///  <param name="BCP47Tag">BCP47 tag associated with the TIP</param>
    ///  <param name="KeyboardName">Name of keyboard to display in Windows UI</param>
    ///  <param name="LangID">Language ID found with FindInstallationLangID</param>
    ///  <param name="IconFileName">Icon to display in Windows UI</param>
    ///  <param name="LanguageName">Name of language to display in Keyman UI</param>
    procedure RegisterTip(const KeyboardID, BCP47Tag, KeyboardName: string; LangID: Integer; IconFileName, LanguageName: string);

    ///  <summary>Installs a TIP for a given keyboard + language, for the current user.</summary>
    ///  <param name="KeyboardID">Keyman keyboard to associate with TIP</param>
    ///  <param name="BCP47Tag">BCP47 tag associated with the TIP</param>
    ///  <param name="LangID">Language ID found with FindInstallationLangID</param>
    ///  <param name="guidProfile">Profile GUID that is assigned on exit</param>
    procedure InstallTip(const KeyboardID, BCP47Tag: string; LangID: Integer; var guidProfile: TGUID);

    ///  <summary>Removes a temporary keyboard layout that was installed by
    ///  FindInstallationLangID. Current User.</summary>
    ///  <param name="LayoutString">TemporaryLayoutString value returned from
    ///  FindInstallationLangID</param>
    procedure UninstallTemporaryLayout(const LayoutString: string);

    ///  <summary>Registers a TIP for each of the four transient language IDs,
    ///  and adds metadata to the HKLM registry about the Keyman keyboard
    ///  association. Requires elevation.</summary>
    ///  <param name="KeyboardID">Keyman keyboard to register</param>
    ///  <param name="KeyboardName">Name of keyboard to display in Windows UI</param>
    ///  <param name="IconFileName">Icon to display in Windows UI</param>
    procedure RegisterTransientTips(const KeyboardID, KeyboardName: string; IconFileName: string);

    constructor Create(AContext: TKeymanContext);
    destructor Destroy; override;
  private
    FWin8Languages: TWindows8LanguageList;
    function ConvertBCP47TagToLangID(Locale: string; var LangID: Integer): Boolean;
    function InstallBCP47Language(const FLocaleName: string): Boolean;
    function GetInputProcessorProfileMgr: ITfInputProcessorProfileMgr;
    function AreMaximumTransientLanguagesInstalled: Boolean;
  end;

implementation

uses
  System.Classes,
  System.SysUtils,
  System.Variants,
  System.Win.ComObj,
  Winapi.ActiveX,

  input_installlayoutortip,
  isadmin,
  Keyman.System.MitigateWin10_1803LanguageInstall,
  keymanerrorcodes,
  KeymanPaths,
  RegistryKeys,
  BCP47Tag,
  glossary,
  TempFileManager,
  utilexecute,
  utilfiletypes,
  utilkeyman,
  utilstr,
  utiltsf;

{ TKPInstallKeyboardLanguage }

constructor TKPInstallKeyboardLanguage.Create(AContext: TKeymanContext);
begin
  FWin8Languages := TWindows8LanguageList.Create;
  inherited Create(AContext);
end;

destructor TKPInstallKeyboardLanguage.Destroy;
begin
  FreeAndNil(FWin8Languages);
  inherited Destroy;
end;

function TKPInstallKeyboardLanguage.AreMaximumTransientLanguagesInstalled: Boolean;
const
  MAX_TRANSIENT_LANGUAGES = 4;
var
  r: TRegistry;
  keys: TStringList;
  isInstalled: array[0..MAX_TRANSIENT_LANGUAGES-1] of Boolean;
  i: Integer;
  v: Integer;
  key: string;
begin
  for i := 0 to high(isInstalled) do isInstalled[i] := False;

  // We'll use Control Panel/International/User Profile to find the full set of
  // transient languages
  r := TRegistry.Create;
  keys := TStringList.Create;
  try
    if r.OpenKeyReadOnly(SRegKey_ControlPanelInternationalUserProfile) then
    begin
      r.GetKeyNames(keys);
      for key in keys do
      begin
        if r.OpenKeyReadOnly('\' + SRegKey_ControlPanelInternationalUserProfile + '\' + key) then
        begin
          if r.ValueExists(SRegValue_CPIUP_TransientLangId) then
          begin
            v := r.ReadInteger(SRegValue_CPIUP_TransientLangId);
            // Transient languages start at $2000 and go up by $400
            v := (v - $2000) div $400;
            if (v >= 0) and (v <= High(isInstalled)) then
              isInstalled[v] := True;
          end;
        end;
      end;
    end;
  finally
    keys.Free;
    r.Free;
  end;

  for i := 0 to High(isInstalled) do
    if not isInstalled[i] then
      Exit(False);

  Result := True;
end;

// TODO: change LangID to Winapi.Windows.LANGID
function TKPInstallKeyboardLanguage.FindInstallationLangID(var BCP47Tag: string; var LangID: Integer; var TemporaryLayoutString: string; Flags: TKPInstallKeyboardLanguageFlags): Boolean;
var
  ml: TMitigateWin10_1803.TMitigatedLanguage;
  Win8Lang: TWindows8Language;
begin
  if BCP47Tag = '' then
    ErrorFmt(KMN_E_ProfileInstall_InvalidBCP47Tag, VarArrayOf([BCP47Tag]));

  TemporaryLayoutString := '';

  if ConvertBCP47TagToLangID(BCP47Tag, LangID) then
  begin
    if TMitigateWin10_1803.IsMitigationRequired(LangID, ml) then
    begin
      LangID := ml.NewLanguage.Code;
      BCP47Tag := ml.NewLanguage.BCP47;
      WarnFmt(KMN_W_ProfileInstall_Win10_1803_MitigationApplied, VarArrayOf([ml.OriginalLanguage.Name, ml.NewLanguage.Name]));
    end;

    if LangID <> 0 then
    begin
      // We have found a LangID, so the search is done.
      Exit(True);
    end;

    // While `ConvertBCP47TagToLangID` only ever returns `True` if the
    // `LangID` parameter is set to non-zero on exit, TMitigateWin10_1803 may
    // cause `LangID` to be reset to zero, indicating that we need to install
    // a transient lang id for the corresponding replacement `BCP47Tag` that
    // was located, after all.
  end;

{'fraction_non_alphanumeric': 0.09220996789054865, 'fraction_numerical': 0.015077481502163897, 'mean_word_length': 3.7050903119868637, 'pattern_counts': {'":': 0, '<': 103, '<?xml version=': 0, '>': 103, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2149499', 'n_tokens_mistral': 4446, 'n_tokens_neox': 4263, 'n_words': 1226}