Inno Setup 5: How to manually invoke administrative install mode in [Code] section?

205 views Asked by At

Circumstances:
Hello everyone, I using Inno Setup 5.5.6(u) to create my setups. It has to be version 5.x to support some older OS. By default the UAC requests elevation and if clicked "No" it offers to run unelevated - I use the seccond solution from Martin Prikryl to get this to work. The reason for why I not put that in his comments is that the topic seems a bit different to me and it looks more of a general problem with Inno Setup to me.

Problem:
When accounts without membership of administrator group run the code and use 'optional' UAC with credentials from any administrator account the following problem occurs: Inno Setup keeps the non administrative install mode (e.g. uninstall entry, desktop icon) for the administrator account you entered the credentials for only. The program itself will be installed under ProgramFiles fine due to IsAdminLoggedOn. Any other user (including your limited user account from where you perform the install) can not call the desktop icon or the uninstall.

What I have tried so far:
So, can anyone help me switching to administrative install mode from within [Code] manually? Please do not suggest the followng as I have tried this in vain already:

  • Not work: PrivilegesRequiredOverridesAllowed - I have to use version 5.x for compatibility reasons and back then this command can not be interpreted at all.
  • Not work: PrivilegesRequired=admin or PrivilegesRequired=none - if I would use them I would not be able to catch elevation denied by user and offer unelevated install.
  • Not work: Command-line parameter /ALLUSERS theoretically should do the trick but does not work for me too. Maybe version 5 does not support it too...
  • Not work: Function IsAdminInstallMode is not recognized by setup either (when compiling error message tells me 'Unknown identifier [...]').
  • Kind of dirty solution: Creating two additional subordinate installers, one with admin privileges required one without is explained here: https://copyprogramming.com/howto/inno-setup-access-unprivileged-account-folders-from-installer-that-requires-privileges. But out of other reasons I embedded a setup inside a setup already. I would rather not nest them any further.
  • Theoretical it should be possible considering the way Inno Setup works and the fact once UAC was accepted IsAdminLoggedOn returns True (look here: https://stackoverflow.com/a/65990073/22271111
  • Not work: Preprocessor solutions are no option too currently. I tried to set PrivilegesRequired={#PrivilegesRequired} and added query for the ISPP to #define PrivilegesRequired "{code:VerifyAdmin}" depending on whether IsAdminLoggedOn in code is true or not. But then it comes to me that this [Setup] setting is statically build in setup and changes at runtime won't get recognized (at least not those from code section).
  • Inno Setup guide itself and google results do not address that matter (including Stackoverflow), thats why I created a question here.

Expected solution:
If everything went well, Inno Setup should switch to administrative install mode if UAC was accepted at runtime (pseudo command: set {#SetupSetting("PrivilegesRequired")} := 'admin'). I would be happy if I can tell the compiler to do so with some [Code] section commands.

1

There are 1 answers

0
Dragodraki On

I know, Inno Setup 5.x is outdated by now and does not provide great security, but I want to repack some application and free games only - no going to create an antivirus suite.

Indeed, there seems to be no way to switch between administrative and non-administrative mode at runtime. But here is a workaround for the most relevant constants. It includes Martin Prikryl's solution but there is so much more that it does not seem to belong to the original discussion.

I tested my code on Windows XP,7,11 - it works fine on all of them:

; Written and compatible with Inno Setup 5.5.6 unicode

#define MyAppName "[GAMENAME]"
#define app "{pf}\" + MyAppPublisher + "\" + MyAppName + "\"
#define IfPrivilegesRequiredLowestTryAdmin_YES_NO "YES"
#define AppDirLimitToProgramFilesFolder_YES_NO "NO"

[Setup]
;Important: Do not change the AppID's scheme, since it is used by Code
AppId={{ADD-YOUR-GUID-HERE}-{code:ElevationProgramName}-{code:GetAuthorIfUnelevated}
PrivilegesRequired=lowest
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppName} {code:ElevationProgramName}
UsePreviousLanguage=no

[Icons]
;--- first/second entry if increased rights (from Vista / up to XP), third for if limited rights
;Start menu shortcut (to start)
Name: "{%programdata}\Microsoft\Windows\Start Menu\Programs\{#MyAppName}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: IsAdminLoggedOn; MinVersion: 6.0; 
Name: "{code:startmenupath}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: IsAdminLoggedOn; OnlyBelowVersion: 6.0;
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: "not IsAdminLoggedOn";
;Start menu shortcut (for uninstalling)
Name: "{%programdata}\Microsoft\Windows\Start Menu\Programs\{#MyAppName}\{#MyAppName} {code:UninstallAppTitle}"; Filename: "{uninstallexe}"; IconFilename: "{app}\Icons\Uninstall.ico"; Check: IsAdminLoggedOn; MinVersion: 6.0; 
Name: "{code:startmenupath}\{#MyAppName} {code:UninstallAppTitle}"; Filename: "{uninstallexe}"; IconFilename: "{app}\Icons\Uninstall.ico"; Check: IsAdminLoggedOn; OnlyBelowVersion: 6.0;
Name: "{group}\{#MyAppName} {code:UninstallAppTitle}"; Filename: "{uninstallexe}"; IconFilename: "{app}\Icons\Uninstall.ico"; Check: "not IsAdminLoggedOn";
;Desktop shortcut (to start)
Name: "{sd}\Users\Public\Desktop\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Check: IsAdminLoggedOn; MinVersion: 6.0;
Name: "{%allusersprofile}\Desktop\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Check: IsAdminLoggedOn; OnlyBelowVersion: 6.0;
Name: "{userdesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Check: "not IsAdminLoggedOn";

[CustomMessages]
ProgramDirErrorX64=The path entered must be a subdirectory of:%n"%1"    OR    "%2"
ProgramDirErrorX32=The path entered must be a subdirectory of:%n"%1"
UserDirError=Not allowed: "%1"%nRestart setup with restricted rights or change the path.
NoAdminAndOSTooOldforUAC=Elevated rights not available!%n%nSince Windows XP cannot start the UAC from the context, your account%nmust be a member of the Administrators group to be allowed to perform installations.%n%nThe installation wizard will now close.
NoAdminAndOSTooOldforUACOfferLimitedInstall=Elevated rights not available!%n%nSince Windows XP cannot start the UAC from the context, your account%nmust be a member of the Administrators group to be allowed to perform installations.%nThe program can also be installed without administrator rights for the current user only.%n%nWould you like to continue with this installation under the condition?
UninstallAppTitle=uninstall
___DummyThatLastMessageAtTheEndDoesNotHaveAnEmptyLine=Test


[Code]

// -------------------------------------------------------------
// VARIABLES THAT ARE IMPORTANT FOR [SETUP]
function IsFilenameValid(Path: string): Boolean;
begin
  if (Pos('\', Path) + Pos('/', Path) + Pos(':', Path) + Pos('*', Path) + Pos(ExpandConstant('?'), Path) + Pos('"', Path) + Pos('<', Path) + Pos('>', Path) + Pos('|', Path)) = 0 then
  begin
    Result := True;
  end
  else
  begin
    Result := False;
  end;
end;

function BeginsWith(SubText, Text: string): Boolean;
var
  BeginStr: string;
begin
  BeginStr := Copy(Text, 0, Length(SubText));
  Result := (CompareText(SubText, BeginStr) = 0);
end;

function ElevationProgramName(Value: string): string;
begin
  try
  if (IsAdminLoggedOn) then
  begin
    Result := '(All Users)';
  end
  else
  begin
    Result := '(Current User)';
  end;
  except
  end;
end;

procedure RegisterPreviousData(PreviousDataKey: Integer);
var
UninstallMode: string;
begin
  // this will store the value under the specified key; except uninstaller you
  // can read the values stored this way in installer
  SetPreviousData(PreviousDataKey, 'UninstallMode', ElevationProgramName(UninstallMode));
end;

function WbemQuery(WbemServices: Variant; Query: string): Variant;
var
  WbemObjectSet: Variant;
begin
  Result := Null;
  WbemObjectSet := WbemServices.ExecQuery(Query);
  if not VarIsNull(WbemObjectSet) and (WbemObjectSet.Count > 0) then
  begin
    Result := WbemObjectSet.ItemIndex(0);
  end;
end;

function RetrieveCurrentUsername: string;
var
  Query: Variant;
  WbemLocator: Variant;
  WbemServices: Variant;
  ComputerSystem: Variant;
  WbemObjectSet: Variant;
begin
  WbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
  WbemServices := WbemLocator.ConnectServer('.', 'root\CIMV2');
  Query := 'SELECT UserName FROM Win32_ComputerSystem';
  ComputerSystem := WbemQuery(WbemServices, Query);
  if not VarIsNull(ComputerSystem) then
  begin
    Result := ExtractFileName(ComputerSystem.UserName);
    //Userdomain := ExtractFilePath(ComputerSystem.UserName);
    //StringChangeEx(Userdomain, '\', '', True);                                                                                   
  end;
end;

function GetAuthorIfUnelevated(Value: string): string;
begin
  if IsAdminLoggedOn then
  begin
    Result := '';
  end
  else
  begin
    Result := RetrieveCurrentUsername;
  end;
end;

const
  CSIDL_COMMON_PROGRAMS = $0017;
function startmenupath(Value: string): string;
begin
  if not IsWindowsVistaOrNewer then
  begin
     Result := GetShellFolderByCSIDL(CSIDL_COMMON_PROGRAMS, True) + '\{#MyAppName}';        
  end;       
end;
function UninstallAppTitle(Value: string): string;
begin
  if IsFilenameValid(ExpandConstant('{cm:UninstallAppTitle}')) then
  begin
    Result := ExpandConstant('{cm:UninstallAppTitle}');
  end
  else
  begin
    Result := 'uninstall';
  end;   
end;
// -------------------------------------------------------------


// -------------------------------------------------------------
// ERLAUBE DIE INSTALLATION NUR BEI KORREKTEM PFAD 
function AnythingAfterPrefix(S: string; Prefix: string): Boolean;
begin
  Result := 
     (Copy(S, 1, Length(Prefix)) = Prefix) and
     (Length(S) > Length(Prefix));
end;

procedure OnDirEditChange(Sender: TObject);
var 
  MySecretDir1: string;
  MySecretDir2: string;
  Status2: string;
  Status1: string;
  SelectDirBrowseLabel: variant;
  PathErrorX64: string;
  PathErrorX32: string;
begin

  if ('{#AppDirLimitToProgramFilesFolder_YES_NO}' = 'YES') then
  begin
    MySecretDir1 := ExpandConstant('{pf}');
    if IsWin64 then MySecretDir2 := ExpandConstant('{pf64}');
    SelectDirBrowseLabel := SetupMessage(msgSelectDirBrowseLabel);
    //                                            ↓ Name                          ↓ %1                        ↓ %2
    if IsWin64 then PathErrorX64 := FmtMessage(CustomMessage('ProgramDirErrorX64'), [ExpandConstant('{pf}'+'\'), ExpandConstant('{pf64}'+'\')]);
    PathErrorX32 := FmtMessage(CustomMessage('ProgramDirErrorX32'), [ExpandConstant('{pf}'+'\')]);
    // CONDITION TO CHECK IF THE SELECTED INSTALLATION PATH IS A SUBORDINATOR OF %ProgramFiles% OR %ProgramW6432%:
    if WizardForm.NextButton.Enabled = True then Status1 := 'True';
    if WizardForm.NextButton.Enabled = False then Status1 := 'False';
    if not AnythingAfterPrefix(TrimRight(WizardDirValue), MySecretDir1) AND (WizardDirValue > MySecretDir1) then WizardForm.NextButton.Enabled := True;
    WizardForm.NextButton.Enabled := Pos(MySecretDir1, WizardDirValue) OR Pos(MySecretDir2, WizardDirValue) = 1;
    if not AnythingAfterPrefix(TrimRight(WizardDirValue), MySecretDir1 + '\') AND AnythingAfterPrefix(TrimRight(WizardDirValue + '\'), MySecretDir2) then WizardForm.NextButton.Enabled := False;
    if not (WizardDirValue <= MySecretDir2) AND AnythingAfterPrefix(WizardDirValue, MySecretDir2 + '\') then Wizardform.NextButton.Enabled := True;
    if WizardForm.NextButton.Enabled = True then Status2 := 'True';
    if WizardForm.NextButton.Enabled = False then Status2 := 'False';
    if (Status1 = 'True') AND (Status2 = 'False') then 
    begin
      if IsWin64 then WizardForm.SelectDirBrowseLabel.Caption := PathErrorX64;
      if not IsWin64 then WizardForm.SelectDirBrowseLabel.Caption := PathErrorX32;
      WizardForm.SelectDirBrowseLabel.Font.Style := [fsBold]; // Use bold font
      WizardForm.DirEdit.Color := clRed; // Use red background color for the input field of the program path
    end;
    if (Status1 = 'False') AND (Status2 = 'True') then 
    begin
      WizardForm.SelectDirBrowseLabel.Caption := SelectDirBrowseLabel;
      WizardForm.SelectDirBrowseLabel.Font.Style := []; // Use normal font
      WizardForm.DirEdit.Color := $fffbff; // -Use -FontColor- for the input field of the program path (here: white)
    end;
  end;

  if (IsAdminLoggedOn) AND not ('{#AppDirLimitToProgramFilesFolder_YES_NO}' = 'YES') then
  begin
    MySecretDir1 := ExtractFilePath(ExpandConstant('{%userprofile}'));
    SelectDirBrowseLabel := SetupMessage(msgSelectDirBrowseLabel);
    //                                            ↓ Name                          ↓ %1                        ↓ %2
    PathErrorX32 := FmtMessage(CustomMessage('UserDirError'), [ExtractFilePath(ExpandConstant('{%userprofile}'))]);
    // REQUIRE TO CHECK IF THE SELECTED INSTALLATION PATH IS SUBORDINATE TO %systemdrive%\Users\:
    if WizardForm.NextButton.Enabled = True then Status1 := 'True';
    if WizardForm.NextButton.Enabled = False then Status1 := 'False';
    if BeginsWith(ExtractFilePath(ExpandConstant('{%userprofile}')), WizardDirValue) OR (Copy(ExtractFilePath(ExpandConstant('{%userprofile}')), 0, Length(ExtractFilePath(ExpandConstant('{%userprofile}'))) -1) = WizardDirValue) then
    begin
      WizardForm.NextButton.Enabled := False;
    end
    else
    begin
      WizardForm.NextButton.Enabled := True;
    end;
    if WizardForm.NextButton.Enabled = True then Status2 := 'True';
    if WizardForm.NextButton.Enabled = False then Status2 := 'False';
    if (Status1 = 'True') AND (Status2 = 'False') then 
    begin
      WizardForm.SelectDirBrowseLabel.Caption := PathErrorX32;
      WizardForm.SelectDirBrowseLabel.Font.Style := [fsBold]; // Use bold font
      WizardForm.DirEdit.Color := clRed; // Use red background color for the input field of the program path
    end;
    if (Status1 = 'False') AND (Status2 = 'True') then 
    begin
      WizardForm.SelectDirBrowseLabel.Caption := SelectDirBrowseLabel;
      WizardForm.SelectDirBrowseLabel.Font.Style := []; // Use normal font
      WizardForm.DirEdit.Color := $fffbff; // -Use -FontColor- for the input field of the program path (here: white) 
    end;
  end;

end;
// -------------------------------------------------------------


// -------------------------------------------------------------
// IF IfPrivilegesRequiredLowestTryAdmin_YES_NO = "YES", TRY FIRST WITH ADMIN RIGHTS AND IF UAC = NO, START WITH USER RIGHTS
function IsWinVista: Boolean;
begin
  Result := (GetWindowsVersion >= $06000000);
end;

function HaveWriteAccessToApp: Boolean;
var
  FileName: string;
begin
  FileName := AddBackslash(WizardDirValue) + 'writetest.tmp';
  Result := SaveStringToFile(FileName, 'test', False);
  if Result then
  begin
    Log(Format(
      'Have write access to the last installation path [%s]', [WizardDirValue]));
    DeleteFile(FileName);
  end
    else
  begin
    Log(Format('Does not have write access to the last installation path [%s]', [
      WizardDirValue]));
  end;
end;

procedure ExitProcess(uExitCode: UINT);
  external '[email protected] stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
  lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
  external '[email protected] stdcall';

function Elevate: Boolean;
var
  I: Integer;
  RetVal: Integer;
  Params: string;
  S: string;
begin
  { Collect current instance parameters }
  for I := 1 to ParamCount do
  begin
    S := ParamStr(I);
    { Unique log file name for the elevated instance }
    if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
    begin
      S := S + '-elevated';
    end;
    { Do not pass our /SL5 switch }
    if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
    begin
      Params := Params + AddQuotes(S) + ' ';
    end;
  end;

  { ... and add selected language }
  Params := Params + '/ALLUSERS=YES ' + '/LANG=' + ActiveLanguage;

  Log(Format('Elevating setup with parameters [%s]', [Params]));
  RetVal := ShellExecute(0, 'runas', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
  Log(Format('Running elevated setup returned [%d]', [RetVal]));
  Result := (RetVal > 32);
  { if elevated executing of this setup succeeded, then... }
  if Result then
  begin
    Log('Elevation succeeded');
    { exit this non-elevated setup instance }
    //Beende Setup mit "Abort()" anstatt mit "ExitProcess(0)", damit temporäre Dateien trotzdme noch gelöscht werden (saubere Methode)
    //ExitProcess(0);
    Abort()
  end
    else
  begin
    Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
  end;
end;

const
PathToRegistryUninstall = '\Software\Microsoft\Windows\CurrentVersion\Uninstall\' + '{#emit StringChange(SetupSetting("AppId"),"{{","{")}' + '_is1';
// -------------------------------------------------------------


procedure InitializeWizard;
var
  NoClicked: boolean;
  S: string;
  Upgrade: Boolean;
  RightsChecked: boolean;
  message2: string;
  WelcomeLabelCombinedText: string;
  message: string;
  WelcomeLabel3: TNewStaticText;

  Userprofile: String;
begin
 begin
  WizardForm.DirEdit.OnChange := @OnDirEditChange;

  if ('{#IfPrivilegesRequiredLowestTryAdmin_YES_NO}' = 'YES') then
  begin
  Upgrade :=
    RegQueryStringValue(HKLM, PathToRegistryUninstall, '{app}', S) or
    RegQueryStringValue(HKCU, PathToRegistryUninstall, '{app}', S);

  { elevate }

  if not IsWinVista then
  begin
    Log(Format('This version of Windows [%x] does not support elevation', [
      GetWindowsVersion]));
  end
    else
  if IsAdminLoggedOn then
  begin
    Log('Running elevated');
        //MsgBox(ExpandConstant('At this point -AdministrativeInstallMode- should have been set, if something like this had been possible: {#SetupSetting("PrivilegesRequired")}'), mbInformation, mb_OK);
  end
    else
  begin
    Log('Running non-elevated');
    if Upgrade then
    begin
      if not HaveWriteAccessToApp then
        begin
        Elevate;
      end;
    end
      else
    begin
      if not Elevate then
      //UAC answered with "No".'
      begin
          if MsgBox(ExpandConstant(CustomMessage('UACDeniedOptionalUserMode')), mbConfirmation, MB_YESNO or MB_DEFBUTTON1) = IDNO then
          begin
          //Question about non-admin installation also answered with "No"
          NoClicked := true
          Abort()
          end
          else
        WizardForm.DirEdit.Text := ExpandConstant('{localappdata}\{#MyAppName}');
        Log(Format('Falling back to local application user folder [%s]', [
          WizardForm.DirEdit.Text]));
      end;         
    end;
  end;
  end;
  // Treat such old OS's like Windows 2000/XP, which do not yet have a UAC for responding:
  RightsChecked := not(IsAdminLoggedOn);    
  if (RightsChecked = true) AND (not IsWindowsVistaOrNewer) then
    begin
    if ('{#IfPrivilegesRequiredLowestTryAdmin_YES_NO}' = 'NO') then
      begin
      //Question about non-admin installation also answered with "No"
      NoClicked := true
      MsgBox(ExpandConstant('{cm:NoAdminAndOSTooOldforUAC}'), mbCriticalError, MB_OK);
      Abort() 
      end;
    if ('{#IfPrivilegesRequiredLowestTryAdmin_YES_NO}' = 'YES') then
      begin
      message2 := CustomMessage('NoAdminAndOSTooOldforUACOfferLimitedInstall');
      if MsgBox(ExpandConstant(message2), mbError, MB_YESNO or MB_DEFBUTTON1) = IDYES then
        begin
        WizardForm.DirEdit.Text := ExpandConstant('{localappdata}\{#MyAppName}');
        Log(Format('Falling back to local application user folder [%s]', [
          WizardForm.DirEdit.Text]));
        end
        else
        //XP query rejected after installation due to restricted rights:
        Abort() 
      end;
    end;
end;


// -------------------------------------------------------------
// MANUALLY CHANGE UNINSTALL-REGISTRY PATH FROM HKCU TO HKLM
var
  AppId: string;
function MoveHKCUUninstallKeyToHKLM: Boolean;
var
  UninstallKey: string;
  I: Integer;
  ValueNames: TArrayOfString;
  ValueName: string;
  ValueStr: string;
  ValueDWord: Cardinal;
begin
  if ExpandConstant('{{#emit StringChange(SetupSetting("AppId"),"{{","{")}') <> '' then
  begin
    AppId := ExpandConstant('{{#emit StringChange(SetupSetting("AppId"),"{{","{")}');
  end
    else
  begin
    AppId := ExpandConstant('{{#emit StringChange(SetupSetting("AppName"),"{{","{")}');
  end;

  Result := False;
  if AppId = '' then
  begin
    Log('Cannot identify AppId');
  end
    else
  begin
    UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1';
    Log(Format(
      'AppId identified as "%s", using uninstall key "%s"', [AppId, UninstallKey]));
    if not RegKeyExists(HKCU, UninstallKey) then
    begin
      Log('HKCU uninstall key not found');
    end
      else
    if RegKeyExists(HKLM, UninstallKey) then
    begin
      Log('HKLM uninstall key exists already');
    end
      else
    begin
      Log('HKCU uninstall key found and HKLM key not exists yet');
      if not RegGetValueNames(HKCU, UninstallKey, ValueNames) then
      begin
        Log('Cannot list uninstall key values');
      end
        else
      begin
        I := 0;
        Result := True;
        while (I < GetArrayLength(ValueNames)) and Result do
        begin
          ValueName := ValueNames[I];
          if RegQueryStringValue(HKCU, UninstallKey, ValueName, ValueStr) then
          begin
            if not RegWriteStringValue(HKLM, UninstallKey, ValueName, ValueStr) then
            begin
              Log(Format('Error moving "%s" string value', [ValueName]));
              Result := False;
            end
              else
            begin
              Log(Format('Moved "%s" string value', [ValueName]));
            end;
          end
            else
          if RegQueryDWordValue(HKCU, UninstallKey, ValueName, ValueDWord) then
          begin
            if not RegWriteDWordValue(HKLM, UninstallKey, ValueName, ValueDWord) then
            begin
              Log(Format('Error moving "%s" dword value', [ValueName]));
              Result := False;
            end
              else
            begin
              Log(Format('Moved "%s" dword value', [ValueName]));
            end;
          end
            else
          begin
            { All uninstall values written by Inno Setup are either string or dword }
            Log(Format('Value "%s" is neither string nor dword', [ValueName]));
            Result := False;
          end;
          Inc(I);
        end;

        if Result then
        begin
          if not RegDeleteKeyIncludingSubkeys(HKCU, UninstallKey) then
          begin
            Log('Error removing HKCU uninstall key');
            Result := False;
          end
            else
          begin
            Log('Removed HKCU uninstall key');
          end;
        end;

        if not Result then
        begin
          if not RegDeleteKeyIncludingSubkeys(HKCU, UninstallKey) then
          begin
            Log('Failed to move uninstall key to HKLM, ' +
                'and also failed to rollback the changes');
          end
            else
          begin
            Log('Failed to move uninstall key to HKLM, rolled back the changes');
          end;
        end;
      end;
    end;
  end;
end;
// -------------------------------------------------------------



procedure CurStepChanged(CurStep: TSetupStep);
var
  InstallMode: string;
begin
  InstallMode := GetPreviousData('UninstallMode', '(Current User)');
  if (CurStep = ssPostInstall) AND (InstallMode = '(All Users)') then
  begin
    MoveHKCUUninstallKeyToHKLM;
  end;
end;


// -------------------------------------------------------------
// REMOVE THE UNINSTALL FLAG IF ELEVATED RIGHTS

procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var 
  SettingsFile: string;
  SettingsFolder: string;
  UninstallMode: string;
  InstallUsername: string;
  CurrentUsername: string;
  CurrentSID: string;
begin
  if CurUninstallStep = usUninstall then
    begin
      UninstallMode := GetPreviousData('UninstallMode', '(Current User)');
      AppId := ExpandConstant('{#SetupSetting("AppId")}');
      if (UninstallMode = '(All Users)') then
      begin
        StringChangeEx(AppId, '(Current User)', '(All Users)', True);
        RegDeleteKeyIncludingSubkeys(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1');
      end;
      if (UninstallMode = '(Current User)') then
      begin
        StringChangeEx(AppId, '(All Users)', '(Current User)', True);
        //When calling the uninstaller with elevated rights, HKCU is not removed. To do this here, I would need the SID of the currently logged on user - but there is no suitable solution for this (minor thing, so do not deal with it further)
        //RegDeleteKeyIncludingSubkeys(HKU, '???\Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1');
      end;
    end;  
  end;
// -------------------------------------------------------------

Maybe someone will find this useful, especially since its designed to be backward-compatible down to Windows 2000.