Browse Source

Merge branch 'downloadtemporaryfile' into main

Some minor todo's left but don't need a branch anymore.
Martijn Laan 5 years ago
parent
commit
4ef7dbff1b

+ 145 - 0
Examples/CodeDownloadFiles.iss

@@ -0,0 +1,145 @@
+; -- CodeDownloadFiles.iss --
+;
+; This script shows how the PrepareToInstall event function can be used to
+; download temporary files.
+
+[Setup]
+AppName=My Program
+AppVersion=1.5
+WizardStyle=modern
+DefaultDirName={autopf}\My Program
+DefaultGroupName=My Program
+UninstallDisplayIcon={app}\MyProg.exe
+OutputDir=userdocs:Inno Setup Examples Output
+
+[Files]
+; Place any regular files here
+Source: "MyProg.exe"; DestDir: "{app}";
+Source: "MyProg.chm"; DestDir: "{app}";
+Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme;
+; These files will be downloaded
+Source: "{tmp}\innosetup-latest.exe"; DestDir: "{app}"; Flags: external
+Source: "{tmp}\ISCrypt.dll"; DestDir: "{app}"; Flags: external
+
+[Icons]
+Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
+
+[Code]
+var
+  DownloadStatusLabel, DownloadFilenameLabel: TNewStaticText;
+  DownloadProgressBar: TNewProgressBar;
+  DownloadAbortButton: TNewButton;
+  DownloadControls: array of TControl;
+  NeedToAbortDownload: Boolean;
+
+procedure SetupDownloadControl(const Dest, Src: TControl; const Parent: TWinControl);
+var
+  N: Integer;
+begin
+  N := GetArrayLength(DownloadControls);
+  SetArrayLength(DownloadControls, N+1);
+  DownloadControls[N] := Dest;
+
+  if Src <> nil then begin
+    Dest.Left := Src.Left;
+    Dest.Top := Src.Top;
+    Dest.Width := Src.Width;
+    Dest.Height := Src.Height;
+    if Src is TNewStaticText then
+      TNewStaticText(Dest).Anchors := TNewStaticText(Src).Anchors
+    else if Src is TNewProgressBar then
+      TNewProgressBar(Dest).Anchors := TNewProgressBar(Src).Anchors;
+  end;
+  Dest.Visible := False;
+  Dest.Parent := Parent;
+end;
+
+procedure DownloadAbortButtonClick(Sender: TObject);
+begin
+  NeedToAbortDownload := MsgBox('Are you sure you want to stop the download?', mbConfirmation, MB_YESNO) = IDYES;
+end;
+
+procedure CreateDownloadControls;
+var
+  Page: TWizardPage;
+begin
+  Page := PageFromID(wpPreparing);
+
+  DownloadStatusLabel := TNewStaticText.Create(Page);
+  SetupDownloadControl(DownloadStatusLabel, WizardForm.StatusLabel, Page.Surface);
+  DownloadFilenameLabel := TNewStaticText.Create(Page);
+  SetupDownloadControl(DownloadFilenameLabel, WizardForm.FilenameLabel, Page.Surface);
+  DownloadProgressBar:= TNewProgressBar.Create(Page);
+  SetupDownloadControl(DownloadProgressBar, WizardForm.ProgressGauge, Page.Surface);
+  DownloadAbortButton := TNewButton.Create(Page);
+  SetupDownloadControl(DownloadAbortButton, nil, Page.Surface);
+
+  DownloadAbortButton.Caption := '&Stop download';
+  DownloadAbortButton.Top := DownloadProgressBar.Top + DownloadProgressBar.Height + ScaleY(8);
+  DownloadAbortButton.Height := WizardForm.CancelButton.Height;
+  DownloadAbortButton.Width := WizardForm.CalculateButtonWidth([DownloadAbortButton.Caption]);
+  DownloadAbortButton.Anchors := [akLeft, akTop];
+  DownloadAbortButton.OnClick := @DownloadAbortButtonClick;
+end;
+
+procedure InitializeWizard;
+begin
+  CreateDownloadControls;
+end;
+
+function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
+begin
+  if NeedToAbortDownload then begin
+    Log('Need to abort download.');
+    Result := False;
+  end else begin
+    if ProgressMax <> 0 then
+      Log(Format('  %d of %d bytes done.', [Progress, ProgressMax]))
+    else
+      Log(Format('  %d bytes done.', [Progress]));
+    
+    DownloadFilenameLabel.Caption := Url;
+    DownloadFilenameLabel.Update;
+
+    if ProgressMax <> 0 then begin
+      DownloadProgressBar.Style := npbstNormal;
+      DownloadProgressBar.Max := ProgressMax;
+      DownloadProgressBar.Position := Progress;
+    end else
+      DownloadProgressBar.Style := npbstMarquee;
+    DownloadProgressBar.Update;
+
+    Result := True;
+  end;
+end;
+
+procedure ShowDownloadControls(const AVisible: Boolean);
+var
+  I: Integer;
+begin
+  for I := 0 to GetArrayLength(DownloadControls)-1 do
+    DownloadControls[I].Visible := AVisible;
+end;
+
+procedure DownloadFiles;
+begin
+  try
+    DownloadStatusLabel.Caption := 'Downloading additional files...';
+    ShowDownloadControls(True);
+    NeedToAbortDownload := False;
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/is.exe', 'innosetup-latest.exe', '', @OnDownloadProgress);
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/iscrypt.dll', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc', @OnDownloadProgress);
+  finally
+    ShowDownloadControls(False);
+  end;
+end;
+
+function PrepareToInstall(var NeedsRestart: Boolean): String;
+begin
+  try
+    DownloadFiles;
+    Result := '';
+  except
+    Result := 'Failed to download files: ' + GetExceptionMessage;
+  end;
+end;

+ 47 - 1
ISHelp/isxfunc.xml

@@ -1687,7 +1687,8 @@ end;</pre></example>
         <remarks><p>Use <tt>Flags: dontcopy</tt> in the [Files] section to tell Setup to skip the file during the normal file copying stage.</p>
         <remarks><p>Use <tt>Flags: dontcopy</tt> in the [Files] section to tell Setup to skip the file during the normal file copying stage.</p>
 <p>Use <tt>Flags: noencryption</tt> in the [Files] section if encryption is enabled and you call the ExtractTemporaryFile function prior to the user entering the correct password.</p>
 <p>Use <tt>Flags: noencryption</tt> in the [Files] section if encryption is enabled and you call the ExtractTemporaryFile function prior to the user entering the correct password.</p>
 <p>When solid compression is enabled, be sure to list your temporary files at (or near) the top of the [Files] section. In order to extract an arbitrary file in a solid-compressed installation, Setup must first decompress all prior files (to a temporary buffer in memory). This can result in a substantial delay if a number of other files are listed above the specified file in the [Files] section.</p></remarks>
 <p>When solid compression is enabled, be sure to list your temporary files at (or near) the top of the [Files] section. In order to extract an arbitrary file in a solid-compressed installation, Setup must first decompress all prior files (to a temporary buffer in memory). This can result in a substantial delay if a number of other files are listed above the specified file in the [Files] section.</p></remarks>
-        <seealso><p><link topic="isxfunc_ExtractTemporaryFiles">ExtractTemporaryFiles</link></p></seealso>
+        <seealso><p><link topic="isxfunc_ExtractTemporaryFiles">ExtractTemporaryFiles</link><br />
+<link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link></p></seealso>
         <example><pre>[Files]
         <example><pre>[Files]
 Source: "Readme.txt"; Flags: dontcopy noencryption
 Source: "Readme.txt"; Flags: dontcopy noencryption
 
 
@@ -1741,6 +1742,51 @@ begin
   Result := True;
   Result := True;
 end;</pre></example>
 end;</pre></example>
       </function>
       </function>
+      <function>
+        <name>DownloadTemporaryFile</name>
+        <prototype>function DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;</prototype>
+        <description><p>Downloads the file from the specified URL to a temporary directory using the specified name. To find the location of the temporary directory, use <tt>ExpandConstant('{tmp}')</tt>.</p>
+<p>An exception will be raised if there was an error. Otherwise, returns the number of bytes downloaded.</p>
+<p>Supports HTTPS (but not expired or self-signed certificates) and HTTP. Redirects are automatically followed and proxy settings are automatically used. Safe to use from services.</p>
+<p>If RequiredSHA256OfFile is not empty it will compare this to the SHA-256 of the downloaded file and raise an exception if the hashes don't match.</p>
+<p>Set OnDownloadProgress to a function to be informed of progress, or <tt>nil</tt> otherwise.</p></description>
+        <remarks><p>TOnDownloadProgress is defined as:</p>
+<p><tt>TOnDownloadProgress = function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean;</tt></p>
+<p>ProgressMax will be 0 if the file size is still unknown. Return True to allow the download to continue, False otherwise.</p>
+<p>For basic authentication use a special URL format like this: http://username:[email protected]/</p></remarks>
+        <seealso><p><link topic="isxfunc_DownloadTemporaryFileSize">DownloadTemporaryFileSize</link><br />
+<link topic="isxfunc_ExtractTemporaryFile">ExtractTemporaryFile</link></p></seealso>
+        <example><pre>
+[Code]
+function OnDownloadProgress(const Url, Filename: string; const Progress, ProgressMax: Int64): Boolean;
+begin
+  if ProgressMax &lt;&gt; 0 then
+    Log(Format('  %d of %d bytes done.', [Progress, ProgressMax]))
+  else
+    Log(Format('  %d bytes done.', [Progress]));
+  Result := True;
+end;
+
+function InitializeSetup: Boolean;
+begin
+  try
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/is.exe', 'innosetup-latest.exe', '', @OnDownloadProgress);
+    DownloadTemporaryFile('https://jrsoftware.org/download.php/iscrypt.dll', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc', @OnDownloadProgress);
+    Result := True;
+  except
+    Log(GetExceptionMessage);
+    Result := False;
+  end;
+end;</pre>
+<p>See <i>CodeDownloadFiles.iss</i> for another example.</p></example>
+      </function>
+      <function>
+        <name>DownloadTemporaryFileSize</name>
+        <prototype>function DownloadTemporaryFileSize(const Url): Int64;</prototype>
+        <description><p>Returns the size of the file from the specified URL, without downloading the file. If the server does not provide the file size, -1 will be returned.</p>
+<p>An exception will be raised if there was an error.</p>
+<p>See <link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link> for other considerations.</p></description>
+      </function>
     </subcategory>
     </subcategory>
     <subcategory>
     <subcategory>
       <function>
       <function>

+ 224 - 11
Projects/Install.pas

@@ -16,8 +16,14 @@ interface
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
   ChangesAssociations: Boolean);
   ChangesAssociations: Boolean);
 
 
+
+type
+  TOnDownloadProgress = function(const Url, BaseName: string; const Progress, ProgressMax: Int64): Boolean of object;
+
 procedure ExtractTemporaryFile(const BaseName: String);
 procedure ExtractTemporaryFile(const BaseName: String);
 function ExtractTemporaryFiles(const Pattern: String): Integer;
 function ExtractTemporaryFiles(const Pattern: String): Integer;
+function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;
+function DownloadTemporaryFileSize(const Url: String): Int64;
 
 
 implementation
 implementation
 
 
@@ -26,7 +32,7 @@ uses
   InstFunc, InstFnc2, SecurityFunc, Msgs, Main, Logging, Extract, FileClass,
   InstFunc, InstFnc2, SecurityFunc, Msgs, Main, Logging, Extract, FileClass,
   Compress, SHA1, PathFunc, CmnFunc, CmnFunc2, RedirFunc, Int64Em, MsgIDs,
   Compress, SHA1, PathFunc, CmnFunc, CmnFunc2, RedirFunc, Int64Em, MsgIDs,
   Wizard, DebugStruct, DebugClient, VerInfo, ScriptRunner, RegDLL, Helper,
   Wizard, DebugStruct, DebugClient, VerInfo, ScriptRunner, RegDLL, Helper,
-  ResUpdate, DotNet, TaskbarProgressFunc, NewProgressBar, RestartManager;
+  ResUpdate, DotNet, TaskbarProgressFunc, NewProgressBar, RestartManager, Net.HTTPClient;
 
 
 type
 type
   TSetupUninstallLog = class(TUninstallLog)
   TSetupUninstallLog = class(TUninstallLog)
@@ -310,6 +316,13 @@ begin
       Result := PathExtractName(Result);
       Result := PathExtractName(Result);
 end;
 end;
 
 
+function LastErrorIndicatesPossiblyInUse(const LastError: DWORD; const CheckAlreadyExists: Boolean): Boolean;
+begin
+  Result := (LastError = ERROR_ACCESS_DENIED) or
+            (LastError = ERROR_SHARING_VIOLATION) or
+            (CheckAlreadyExists and (LastError = ERROR_ALREADY_EXISTS));
+end;
+
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
   ChangesAssociations: Boolean);
   ChangesAssociations: Boolean);
 type
 type
@@ -1507,8 +1520,7 @@ var
             if LastError = ERROR_FILE_NOT_FOUND then
             if LastError = ERROR_FILE_NOT_FOUND then
               Break;
               Break;
             { Does the error code indicate that it is possibly in use? }
             { Does the error code indicate that it is possibly in use? }
-            if (LastError = ERROR_ACCESS_DENIED) or
-               (LastError = ERROR_SHARING_VIOLATION) then begin
+            if LastErrorIndicatesPossiblyInUse(LastError, False) then begin
               DoHandleFailedDeleteOrMoveFileTry('DeleteFile', TempFile, DestFile,
               DoHandleFailedDeleteOrMoveFileTry('DeleteFile', TempFile, DestFile,
                 LastError, RetriesLeft, LastOperation, NeedsRestart, ReplaceOnRestart,
                 LastError, RetriesLeft, LastOperation, NeedsRestart, ReplaceOnRestart,
                 DoBreak, DoContinue);
                 DoBreak, DoContinue);
@@ -1533,15 +1545,13 @@ var
                 ((CurFile^.FileType = ftUninstExe) and DestFileExistedBefore)) then begin
                 ((CurFile^.FileType = ftUninstExe) and DestFileExistedBefore)) then begin
           LastOperation := SetupMessages[msgErrorRenamingTemp];
           LastOperation := SetupMessages[msgErrorRenamingTemp];
           { Since the DeleteFile above succeeded you would expect the rename to
           { Since the DeleteFile above succeeded you would expect the rename to
-            also always succeed, but if it doesn't anyway. }
+            also always succeed, but if it doesn't retry anyway. }
           RetriesLeft := 4;
           RetriesLeft := 4;
           while not MoveFileRedir(DisableFsRedir, TempFile, DestFile) do begin
           while not MoveFileRedir(DisableFsRedir, TempFile, DestFile) do begin
             { Couldn't rename the temporary file... }
             { Couldn't rename the temporary file... }
             LastError := GetLastError;
             LastError := GetLastError;
             { Does the error code indicate that it is possibly in use? }
             { Does the error code indicate that it is possibly in use? }
-            if (LastError = ERROR_ACCESS_DENIED) or
-               (LastError = ERROR_SHARING_VIOLATION) or
-               (LastError = ERROR_ALREADY_EXISTS) then begin
+            if LastErrorIndicatesPossiblyInUse(LastError, True) then begin
               DoHandleFailedDeleteOrMoveFileTry('MoveFile', TempFile, DestFile,
               DoHandleFailedDeleteOrMoveFileTry('MoveFile', TempFile, DestFile,
                 LastError, RetriesLeft, LastOperation, NeedsRestart, ReplaceOnRestart,
                 LastError, RetriesLeft, LastOperation, NeedsRestart, ReplaceOnRestart,
                 DoBreak, DoContinue);
                 DoBreak, DoContinue);
@@ -2963,8 +2973,7 @@ var
         Break;
         Break;
       LastError := GetLastError;
       LastError := GetLastError;
       { Does the error code indicate that the file is possibly in use? }
       { Does the error code indicate that the file is possibly in use? }
-      if (LastError = ERROR_ACCESS_DENIED) or
-         (LastError = ERROR_SHARING_VIOLATION) then begin
+      if LastErrorIndicatesPossiblyInUse(LastError, False) then begin
         if RetriesLeft > 0 then begin
         if RetriesLeft > 0 then begin
           LogFmt('The existing file appears to be in use (%d). ' +
           LogFmt('The existing file appears to be in use (%d). ' +
             'Retrying.', [LastError]);
             'Retrying.', [LastError]);
@@ -3396,7 +3405,7 @@ begin
       Exit;
       Exit;
     end;
     end;
   end;
   end;
-  InternalError(Format('ExtractTemporaryFile: The file "%s" was not found', [BaseName]));
+  InternalErrorFmt('ExtractTemporaryFile: The file "%s" was not found', [BaseName]);
 end;
 end;
 
 
 function ExtractTemporaryFiles(const Pattern: String): Integer;
 function ExtractTemporaryFiles(const Pattern: String): Integer;
@@ -3430,7 +3439,211 @@ begin
   end;
   end;
 
 
   if Result = 0 then
   if Result = 0 then
-    InternalError(Format('ExtractTemporaryFiles: No files matching "%s" found', [Pattern]));
+    InternalErrorFmt('ExtractTemporaryFiles: No files matching "%s" found', [Pattern]);
+end;
+
+type
+  THTTPDataReceiver = class
+  private
+    FBaseName, FUrl: String;
+    FOnDownloadProgress: TOnDownloadProgress;
+    FAborted: Boolean;
+    FProgress, FProgressMax: Int64;
+    FLastReportedProgress, FLastReportedProgressMax: Int64;
+  public
+    property BaseName: String write FBaseName;
+    property Url: String write FUrl;
+    property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
+    property Aborted: Boolean read FAborted;
+    property Progress: Int64 read FProgress;
+    property ProgressMax: Int64 read FProgressMax;
+    procedure OnReceiveData(const Sender: TObject; AContentLength: Int64; AReadCount: Int64; var Abort: Boolean);
+  end;
+
+procedure THTTPDataReceiver.OnReceiveData(const Sender: TObject; AContentLength: Int64; AReadCount: Int64; var Abort: Boolean);
+begin
+  FProgress := AReadCount;
+  FProgressMax := AContentLength;
+
+  if Assigned(FOnDownloadProgress) then begin
+    { Make sure script isn't called crazy often because that would slow the download significantly. Only report:
+      -At start or finish
+      -Or if somehow Progress decreased or Max changed
+      -Or if at least 512 KB progress was made since last report
+    }
+    if (FProgress = 0) or (FProgress = FProgressMax) or
+       (FProgress < FLastReportedProgress) or (FProgressMax <> FLastReportedProgressMax) or
+       ((FProgress - FLastReportedProgress) > 524288) then begin
+      try
+        if not FOnDownloadProgress(FUrl, FBaseName, FProgress, FProgressMax) then
+          Abort := True;
+      finally
+        FLastReportedProgress := FProgress;
+        FLastReportedProgressMax := FProgressMax;
+      end;
+    end;
+  end;
+
+  if not Abort and DownloadTemporaryFileAllowProcessMessages then
+    Application.ProcessMessages;
+
+  if Abort then
+    FAborted := True
+end;
+
+procedure SetSecureProtocols(const AHTTPClient: THTTPClient);
+begin
+  { TLS 1.2 isn't enabled by default on older versions of Windows }
+  AHTTPClient.SecureProtocols := [THTTPSecureProtocol.TLS1, THTTPSecureProtocol.TLS11, THTTPSecureProtocol.TLS12];
+end;
+
+function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;
+var
+  DisableFsRedir: Boolean;
+  PrevState: TPreviousFsRedirectionState;
+  DestFile, TempFile: String;
+  TempF: TFileStream;
+  TempFileLeftOver: Boolean;
+  HTTPDataReceiver: THTTPDataReceiver;
+  HTTPClient: THTTPClient;
+  HTTPResponse: IHTTPResponse;
+  SHA256OfFile: String;
+  RetriesLeft: Integer;
+  LastError: DWORD;
+begin
+  if Url = '' then
+    InternalError('DownloadTemporaryFile: Invalid Url value');
+  if BaseName = '' then
+    InternalError('DownloadTemporaryFile: Invalid BaseName value');
+
+  DestFile := AddBackslash(TempInstallDir) + BaseName;
+
+  LogFmt('Downloading temporary file from %s: %s', [Url, DestFile]);
+
+  DisableFsRedir := InstallDefaultDisableFsRedir;
+
+  { Prepare directory }
+  if FileExists(DestFile) then begin
+    if (RequiredSHA256OfFile <> '') and (RequiredSHA256OfFile = GetSHA256OfFile(DisableFsRedir, DestFile)) then begin
+      Log('  File already downloaded.');
+      Result := 0;
+      Exit;
+    end;    
+    SetFileAttributesRedir(DisableFsRedir, DestFile, GetFileAttributesRedir(DisableFsRedir, DestFile) and not FILE_ATTRIBUTE_READONLY);
+    DelayDeleteFile(DisableFsRedir, DestFile, 13, 50, 250);
+  end else
+    ForceDirectories(DisableFsRedir, PathExtractPath(DestFile));
+
+  HTTPDataReceiver := nil;
+  HTTPClient := nil;
+  TempF := nil;
+  TempFileLeftOver := False;
+  try
+    { Setup downloader }
+    HTTPDataReceiver := THTTPDataReceiver.Create;
+    HTTPDataReceiver.BaseName := BaseName;
+    HTTPDataReceiver.Url := Url;
+    HTTPDataReceiver.OnDownloadProgress := OnDownloadProgress;
+
+    HTTPClient := THTTPClient.Create; { http://docwiki.embarcadero.com/RADStudio/Rio/en/Using_an_HTTP_Client }
+    SetSecureProtocols(HTTPClient);
+    HTTPClient.OnReceiveData := HTTPDataReceiver.OnReceiveData;
+
+    { Create temporary file }
+    TempFile := GenerateUniqueName(DisableFsRedir, PathExtractPath(DestFile), '.tmp');
+    if not DisableFsRedirectionIf(DisableFsRedir, PrevState) then
+      raise Exception.Create('DisableFsRedirectionIf failed');
+    try
+      TempF := TFileStream.Create(TempFile, fmCreate);
+      TempFileLeftOver := True;
+    finally
+      RestoreFsRedirection(PrevState);
+    end;
+
+    { To test redirects: https://jrsoftware.org/download.php/is.exe
+      To test expired certificates: https://expired.badssl.com/
+      To test self-signed certificates: https://self-signed.badssl.com/
+      To test basic authentication: https://guest:[email protected]/HTTP/Basic/
+      To test 100 MB file: https://speed.hetzner.de/100MB.bin
+      To test 1 GB file: https://speed.hetzner.de/1GB.bin }
+
+    { Download to temporary file}
+    HTTPResponse := HTTPClient.Get(Url, TempF);
+    if HTTPDataReceiver.Aborted then
+      raise Exception.Create('Download aborted')
+    else if (HTTPResponse.StatusCode < 200) or (HTTPResponse.StatusCode > 299) then
+      raise Exception.CreateFmt('Download failed: %d %s', [HTTPResponse.StatusCode, HTTPResponse.StatusText])
+    else begin
+      { Download completed, get temporary file size and close it }
+      Result := TempF.Size;
+      FreeAndNil(TempF);
+
+      { Check hash if specified, otherwise check everything else we can check }
+      if RequiredSHA256OfFile <> '' then begin
+        try
+          SHA256OfFile := GetSHA256OfFile(DisableFsRedir, TempFile);
+        except on E: Exception do
+          raise Exception.CreateFmt('File hash failed: %s', [E.Message]);
+        end;
+        if RequiredSHA256OfFile <> SHA256OfFile then
+          raise Exception.CreateFmt('Invalid file hash: expected %s, found %s', [RequiredSHA256OfFile, SHA256OfFile]);
+      end else begin
+        if HTTPDataReceiver.Progress <> HTTPDataReceiver.ProgressMax then
+          raise Exception.CreateFmt('Invalid progress: %d of %d', [HTTPDataReceiver.Progress, HTTPDataReceiver.ProgressMax])
+        else if HTTPDataReceiver.ProgressMax <> Result then
+          raise Exception.CreateFmt('Invalid file size: expected %d, found %d', [HTTPDataReceiver.ProgressMax, Result]);
+      end;
+
+      { Rename the temporary file to the new name now, with retries if needed }
+      RetriesLeft := 4;
+      while not MoveFileRedir(DisableFsRedir, TempFile, DestFile) do begin
+        { Couldn't rename the temporary file... }
+        LastError := GetLastError;
+        { Does the error code indicate that it is possibly in use? }
+        if LastErrorIndicatesPossiblyInUse(LastError, True) then begin
+          LogFmt('  The existing file appears to be in use (%d). ' +
+            'Retrying.', [LastError]);
+          Dec(RetriesLeft);
+          Sleep(1000);
+          if RetriesLeft > 0 then
+            Continue;
+        end;
+        { Some other error occurred, or we ran out of tries }
+        SetLastError(LastError);
+        Win32ErrorMsg('MoveFile'); { Throws an exception }
+      end;
+      TempFileLeftOver := False;
+    end;
+  finally
+    TempF.Free;
+    HTTPClient.Free;
+    HTTPDataReceiver.Free;
+    if TempFileLeftOver then
+      DeleteFileRedir(DisableFsRedir, TempFile);
+  end;
+end;
+
+function DownloadTemporaryFileSize(const Url: String): Int64;
+var
+  HTTPClient: THTTPClient;
+  HTTPResponse: IHTTPResponse;
+begin
+  if Url = '' then
+    InternalError('DownloadTemporaryFileSize: Invalid Url value');
+
+  LogFmt('Getting size of %s.', [Url]);
+
+  HTTPClient := THTTPClient.Create;
+  try
+    SetSecureProtocols(HTTPClient);
+    HTTPResponse := HTTPClient.Head(Url);
+    if (HTTPResponse.StatusCode < 200) or (HTTPResponse.StatusCode > 299) then
+      raise Exception.CreateFmt('Getting size failed: %d %s', [HTTPResponse.StatusCode, HTTPResponse.StatusText])
+    else
+      Result := HTTPResponse.ContentLength; { Could be -1 }
+  finally
+    HTTPClient.Free;
+  end;
 end;
 end;
 
 
 end.
 end.

+ 1 - 1
Projects/Main.pas

@@ -179,7 +179,7 @@ var
   DisableCodeConsts: Integer;
   DisableCodeConsts: Integer;
   SetupExitCode: Integer;
   SetupExitCode: Integer;
   CreatedIcon: Boolean;
   CreatedIcon: Boolean;
-  RestartInitiatedByThisProcess: Boolean;
+  RestartInitiatedByThisProcess, DownloadTemporaryFileAllowProcessMessages: Boolean;
 {$IFDEF IS_D12}
 {$IFDEF IS_D12}
   TaskbarButtonHidden: Boolean;
   TaskbarButtonHidden: Boolean;
 {$ENDIF}
 {$ENDIF}

+ 10 - 2
Projects/ScriptFunc.pas

@@ -120,10 +120,18 @@ const
   );
   );
 
 
   { Install }
   { Install }
-  InstallTable: array [0..1] of AnsiString =
+{$IFNDEF PS_NOINT64}
+  InstallTable: array [0..3] of AnsiString =
+{$ELSE}
+  InstallTable: array [0..2] of AnsiString =
+{$ENDIF}
   (
   (
     'procedure ExtractTemporaryFile(const FileName: String);',
     'procedure ExtractTemporaryFile(const FileName: String);',
-    'function ExtractTemporaryFiles(const Pattern: String): Integer;'
+    'function ExtractTemporaryFiles(const Pattern: String): Integer;',
+{$IFNDEF PS_NOINT64}
+    'function DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;',
+    'function DownloadTemporaryFileSize(const Url: String): Int64;'
+{$ENDIF}
   );
   );
 
 
   { InstFunc }
   { InstFunc }

+ 4 - 0
Projects/ScriptFunc_C.pas

@@ -142,6 +142,10 @@ begin
     '  SuiteMask: Word;' +
     '  SuiteMask: Word;' +
     'end');
     'end');
 
 
+{$IFNDEF PS_NOINT64}
+  RegisterType('TOnDownloadProgress', 'function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean;');
+{$ENDIF}
+
   RegisterFunctionTable(ScriptDlgTable);
   RegisterFunctionTable(ScriptDlgTable);
   RegisterFunctionTable(NewDiskTable);
   RegisterFunctionTable(NewDiskTable);
   RegisterFunctionTable(BrowseFuncTable);
   RegisterFunctionTable(BrowseFuncTable);

+ 14 - 0
Projects/ScriptFunc_R.pas

@@ -739,6 +739,8 @@ end;
 function InstallProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean;
 function InstallProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean;
 var
 var
   PStart: Cardinal;
   PStart: Cardinal;
+  P: PPSVariantProcPtr;
+  OnDownloadProgress: TOnDownloadProgress;
 begin
 begin
   if IsUninstaller then
   if IsUninstaller then
     NoUninstallFuncError(Proc.Name);
     NoUninstallFuncError(Proc.Name);
@@ -750,6 +752,18 @@ begin
     ExtractTemporaryFile(Stack.GetString(PStart));
     ExtractTemporaryFile(Stack.GetString(PStart));
   end else if Proc.Name = 'EXTRACTTEMPORARYFILES' then begin
   end else if Proc.Name = 'EXTRACTTEMPORARYFILES' then begin
     Stack.SetInt(PStart, ExtractTemporaryFiles(Stack.GetString(PStart-1)));
     Stack.SetInt(PStart, ExtractTemporaryFiles(Stack.GetString(PStart-1)));
+{$IFNDEF PS_NOINT64}
+  end else if Proc.Name = 'DOWNLOADTEMPORARYFILE' then begin
+    P := Stack.Items[PStart-4];
+    { ProcNo 0 means nil was passed by the script }
+    if P.ProcNo <> 0 then
+      OnDownloadProgress := TOnDownloadProgress(Caller.GetProcAsMethod(P.ProcNo))
+    else
+      OnDownloadProgress := nil;
+    Stack.SetInt64(PStart, DownloadTemporaryFile(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetString(PStart-3), OnDownloadProgress));
+  end else if Proc.Name = 'DOWNLOADTEMPORARYFILESIZE' then begin
+    Stack.SetInt64(PStart, DownloadTemporaryFileSize(Stack.GetString(PStart-1)));
+{$ENDIF}
   end else
   end else
     Result := False;
     Result := False;
 end;
 end;

+ 8 - 8
Projects/Wizard.pas

@@ -208,6 +208,7 @@ type
     function ShouldSkipPage(const PageID: Integer): Boolean;
     function ShouldSkipPage(const PageID: Integer): Boolean;
     procedure UpdateComponentSizes;
     procedure UpdateComponentSizes;
     procedure UpdateComponentSizesEnum(Index: Integer; HasChildren: Boolean; Ext: LongInt);
     procedure UpdateComponentSizesEnum(Index: Integer; HasChildren: Boolean; Ext: LongInt);
+    procedure UpdateCurPageButtonState;
     procedure UpdatePage(const PageID: Integer);
     procedure UpdatePage(const PageID: Integer);
     procedure UpdateSelectTasksPage;
     procedure UpdateSelectTasksPage;
     procedure WMSysCommand(var Message: TWMSysCommand); message WM_SYSCOMMAND;
     procedure WMSysCommand(var Message: TWMSysCommand); message WM_SYSCOMMAND;
@@ -234,7 +235,6 @@ type
     procedure IncTopDecHeight(const AControl: TControl; const Amount: Integer);
     procedure IncTopDecHeight(const AControl: TControl; const Amount: Integer);
     function PageFromID(const ID: Integer): TWizardPage;
     function PageFromID(const ID: Integer): TWizardPage;
     function PageIndexFromID(const ID: Integer): Integer;
     function PageIndexFromID(const ID: Integer): Integer;
-    procedure UpdateCurPageButtonVisibility;
     procedure SetCurPage(const NewPageID: Integer);
     procedure SetCurPage(const NewPageID: Integer);
     procedure SelectComponents(const ASelectComponents: TStringList); overload;
     procedure SelectComponents(const ASelectComponents: TStringList); overload;
     procedure SelectTasks(const ASelectTasks: TStringList); overload;
     procedure SelectTasks(const ASelectTasks: TStringList); overload;
@@ -1860,7 +1860,6 @@ end;
 
 
 function TWizardForm.PrepareToInstall(const WizardComponents, WizardTasks: TStringList): String;
 function TWizardForm.PrepareToInstall(const WizardComponents, WizardTasks: TStringList): String;
 var
 var
-  WindowDisabler: TWindowDisabler;
   CodeNeedsRestart: Boolean;
   CodeNeedsRestart: Boolean;
   Y: Integer;
   Y: Integer;
   S: String;
   S: String;
@@ -1879,19 +1878,20 @@ begin
     SetCurPage(wpPreparing);
     SetCurPage(wpPreparing);
     BackButton.Visible := False;
     BackButton.Visible := False;
     NextButton.Visible := False;
     NextButton.Visible := False;
+    CancelButton.Enabled := False;
     if InstallMode = imSilent then begin
     if InstallMode = imSilent then begin
       SetActiveWindow(Application.Handle);  { ensure taskbar button is selected }
       SetActiveWindow(Application.Handle);  { ensure taskbar button is selected }
       WizardForm.Show;
       WizardForm.Show;
     end;
     end;
     WizardForm.Update;
     WizardForm.Update;
-    WindowDisabler := TWindowDisabler.Create;
     try
     try
+      DownloadTemporaryFileAllowProcessMessages := True;
       CodeNeedsRestart := False;
       CodeNeedsRestart := False;
       Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
       Result := CodeRunner.RunStringFunctions('PrepareToInstall', [@CodeNeedsRestart], bcNonEmpty, True, '');
       PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
       PrepareToInstallNeedsRestart := (Result <> '') and CodeNeedsRestart;
     finally
     finally
-      WindowDisabler.Free;
-      UpdateCurPageButtonVisibility;
+      DownloadTemporaryFileAllowProcessMessages := False;
+      UpdateCurPageButtonState;
     end;
     end;
     Application.BringToFront;
     Application.BringToFront;
   end;
   end;
@@ -2193,7 +2193,7 @@ begin
   Result := -1;
   Result := -1;
 end;
 end;
 
 
-procedure TWizardForm.UpdateCurPageButtonVisibility;
+procedure TWizardForm.UpdateCurPageButtonState;
 var
 var
   PageIndex: Integer;
   PageIndex: Integer;
   Page: TWizardPage;
   Page: TWizardPage;
@@ -2249,7 +2249,7 @@ begin
     not(CurPageID in [wpWelcome, wpFinished]);
     not(CurPageID in [wpWelcome, wpFinished]);
 
 
   { Set button visibility and captions }
   { Set button visibility and captions }
-  UpdateCurPageButtonVisibility;
+  UpdateCurPageButtonState;
 
 
   BackButton.Caption := SetupMessages[msgButtonBack];
   BackButton.Caption := SetupMessages[msgButtonBack];
   if CurPageID = wpReady then begin
   if CurPageID = wpReady then begin
@@ -2573,7 +2573,7 @@ begin
                 if RmFoundApplications then
                 if RmFoundApplications then
                   Break;  { stop on the page }
                   Break;  { stop on the page }
               finally
               finally
-                UpdateCurPageButtonVisibility;
+                UpdateCurPageButtonState;
               end;
               end;
             end;
             end;
           finally
           finally

+ 1 - 0
setup.iss

@@ -137,6 +137,7 @@ Source: "Examples\CodeAutomation2.iss"; DestDir: "{app}\Examples"; Flags: ignore
 Source: "Examples\CodeClasses.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeClasses.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDlg.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDlg.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDll.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDll.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
+Source: "Examples\CodeDownloadFiles.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeExample1.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeExample1.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodePrepareToInstall.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodePrepareToInstall.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Components.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Components.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch

+ 15 - 3
whatsnew.htm

@@ -47,10 +47,22 @@ For conditions of distribution and use, see <a href="https://jrsoftware.org/file
   <li>Added buttons to the Welcome dialog to <a href="https://jrsoftware.org/isdonate.php">Donate</a> to support Inno Setup (Thank you!) and to <a href="https://jrsoftware.org/ismail.php">Subscribe</a> to the Inno Setup Mailing List to be notified by e-mail of new Inno Setup releases.</li>
   <li>Added buttons to the Welcome dialog to <a href="https://jrsoftware.org/isdonate.php">Donate</a> to support Inno Setup (Thank you!) and to <a href="https://jrsoftware.org/ismail.php">Subscribe</a> to the Inno Setup Mailing List to be notified by e-mail of new Inno Setup releases.</li>
   <li>The Run Parameters dialog now shows a list of most recently used parameters.</li>
   <li>The Run Parameters dialog now shows a list of most recently used parameters.</li>
 </ul>
 </ul>
-<p><span class="head2">Pascal Scripting ([Code] section) updates</span></p>
-<p>Pascal Scripting now supports SHA-256 hashes:</p>
+<p><span class="head2">Built-in download support for [Code]</span></p>
+<p>Pascal Scripting now supports downloading files and checking SHA-256 hashes:</p>
 <ul>
 <ul>
-  <li>Added new <tt>GetSHA256OfFile</tt>, <tt>GetSHA256OfString</tt>, and <tt>GetSHA256OfUnicodeString</tt> support functions.</li>
+  <li>Added new <tt>DownloadTemporaryFile</tt> support function to download files without using a third-party tool:
+  <ul>
+    <li>Allows you to <a href="https://i.imgur.com/ZLmiS3t.png">show the download progress</a> to the user. See the new <i>CodeDownloadFiles.iss</i> example script for an example.</li>
+    <li>Supports HTTPS (but not expired or self-signed certificates) and HTTP.</li>
+    <li>Redirects are automatically followed and proxy settings are automatically used.</li>
+    <li>Safe to use from services unlike existing third-party tools.</li>
+    <li>Supports SHA-256 hash checking of the downloaded file.</li>
+    <li>Supports basic authentication.</li>
+  </ul>
+  </li>
+  <li>Added new <tt>DownloadTemporaryFileSize</tt> support function to get the size of a file without downloading it.</li>
+  <li>Added new <tt>GetSHA256OfFile</tt>, <tt>GetSHA256OfString</tt>, and <tt>GetSHA256OfUnicodeString</tt> support functions to calculate SHA-256 hashes.</li>
+  <li><b>Change in default behavior:</b> Setup no longer disables itself entirely while <tt>PrepareToInstall</tt> is running. Instead only the Cancel button is disabled.</li>
 </ul>
 </ul>
 <p><span class="head2">Inno Setup Preprocessor (ISPP) updates</span></p>
 <p><span class="head2">Inno Setup Preprocessor (ISPP) updates</span></p>
 <p>ISPP now uses 64-bit integers and has new functions to more easily compare version numbers:</p>
 <p>ISPP now uses 64-bit integers and has new functions to more easily compare version numbers:</p>