{** * Provides environmental and configuration parameters for the installation. * Includes functionality for querying online API for extra metadata such as * version and location of .msi to be installed. *} unit Keyman.Setup.System.InstallInfo; interface uses System.Classes, System.Generics.Collections, System.JSON, System.SysUtils, PackageInfo, SetupStrings; type EInstallInfo = class(Exception); TMSIInfo = record Version, ProductCode: string; end; TInstallInfoLocationType = (iilLocal, iilOnline); TInstallInfoPackageLanguage = class private FName: string; FBCP47: string; public constructor Create(const ABCP47, AName: string); property BCP47: string read FBCP47; property Name: string read FName; end; TInstallInfoPackageLanguages = class(TObjectlist) end; TInstallInfoFileLocation = class private FLocationType: TInstallInfoLocationType; FUrl: string; FSize: Integer; FVersion: string; FPath: string; FProductCode: string; FVersionWithTag: string; protected procedure LoadFromJSON(o: TJSONObject); virtual; procedure SaveToJSON(o: TJSONObject); virtual; public constructor Create(ALocationType: TInstallInfoLocationType); virtual; procedure UpgradeToLocalPath(const ARootPath: string); property LocationType: TInstallInfoLocationType read FLocationType; property Size: Integer read FSize write FSize; property Path: string read FPath write FPath; property Url: string read FUrl write FUrl; property ProductCode: string read FProductCode write FProductCode; // used only by msi property VersionWithTag: string read FVersionWithTag write FVersionWithTag; // used only by msi property Version: string read FVersion write FVersion; end; TInstallInfoFileLocations = class(TObjectList) private procedure LoadFromJSON(a: TJSONArray); procedure SaveToJSON(a: TJSONArray); public function LatestVersion(const default: string = '0'): string; end; TInstallInfoPackageFileLocation = class(TInstallInfoFileLocation) private FName: string; FLanguages: TInstallInfoPackageLanguages; FLocalPackage: TPackage; protected procedure LoadFromJSON(o: TJSONObject); override; procedure SaveToJSON(o: TJSONObject); override; public constructor Create(ALocationType: TInstallInfoLocationType); override; destructor Destroy; override; function GetNameOrID(const id: string): string; function GetLanguageNameFromBCP47(const bcp47: string): string; property Name: string read FName write FName; property Languages: TInstallInfoPackageLanguages read FLanguages; property LocalPackage: TPackage read FLocalPackage; end; TInstallInfoPackageFileLocations = class(TObjectList) private procedure LoadFromJSON(a: TJSONArray); procedure SaveToJSON(a: TJSONArray); public function LatestVersion(const default: string = '0'): string; end; TInstallInfoPackage = class private FID: string; FBCP47: string; FLocations: TInstallInfoPackageFileLocations; FShouldInstall: Boolean; FInstallLocation: TInstallInfoPackageFileLocation; function GetBestLocation: TInstallInfoPackageFileLocation; public constructor Create(const AID: string; const ABCP47: string = ''); destructor Destroy; override; procedure SaveToJSON(o: TJSONObject); procedure LoadFromJSON(o: TJSONObject); property ID: string read FID; property BCP47: string read FBCP47 write FBCP47; property ShouldInstall: Boolean read FShouldInstall write FShouldInstall; property Locations: TInstallInfoPackageFileLocations read FLocations; property InstallLocation: TInstallInfoPackageFileLocation read FInstallLocation write FInstallLocation; end; TInstallInfoPackages = class(TObjectList) private procedure LoadFromJSON(a: TJSONArray); procedure SaveToJSON(a: TJSONArray); public function FindById(const id: string; createIfNotFound: Boolean): TInstallInfoPackage; end; TInstallInfo = class private FAppName: WideString; FPackages: TInstallInfoPackages; FStrings: TStrings; FTitleImageFilename: string; FStartDisabled: Boolean; FStartWithConfiguration: Boolean; FMsiLocations: TInstallInfoFileLocations; FBestMsi: TInstallInfoFileLocation; FInstalledVersion: TMSIInfo; FIsInstalled: Boolean; FIsNewerAvailable: Boolean; FTempPath: string; FShouldInstallKeyman: Boolean; FTier: string; FMsiInstallLocation: TInstallInfoFileLocation; function GetBestMsi: TInstallInfoFileLocation; function GetPackageMetadata(const KmpFilename: string; p: TPackage): Boolean; public constructor Create(const ATempPath: string); destructor Destroy; override; procedure SaveToJSONFile(const Filename: string); procedure LoadFromJSONFile(const Filename: string); procedure SaveToJSON(o: TJSONObject); procedure LoadFromJSON(o: TJSONObject); procedure LoadSetupInf(const SetupInfPath: string); procedure LocatePackagesAndTierFromFilename(Filename: string); procedure LocatePackagesFromParameter(const Param: string); procedure LoadLocalPackagesMetadata; function CheckMsiUpgradeScenarios: Boolean; procedure CheckPackageLocations; function Text(const Name: TInstallInfoText): WideString; overload; function Text(const Name: TInstallInfoText; const Args: array of const): WideString; overload; property TempPath: string read FTempPath; property EditionTitle: WideString read FAppName write FAppName; property MsiLocations: TInstallInfoFileLocations read FMsiLocations; property MsiInstallLocation: TInstallInfoFileLocation read FMsiInstallLocation write FMsiInstallLocation; // Keyman Installer information property InstalledVersion: TMSIInfo read FInstalledVersion write FInstalledVersion; property IsInstalled: Boolean read FIsInstalled; property IsNewerAvailable: Boolean read FIsNewerAvailable; property Packages: TInstallInfoPackages read FPackages; property TitleImageFilename: string read FTitleImageFilename write FTitleImageFilename; // write used only for tests property StartDisabled: Boolean read FStartDisabled write FStartDisabled; // write used only for tests property StartWithConfiguration: Boolean read FStartWithConfiguration write FStartWithConfiguration; // write used only for tests property Tier: string read FTier write FTier; property Strings: TStrings read FStrings; // Note: use .Text(), not .Strings to read localized strings property ShouldInstallKeyman: Boolean read FShouldInstallKeyman write FShouldInstallKeyman; end; implementation uses System.RegularExpressions, System.StrUtils, System.TypInfo, System.Zip, Winapi.Windows, JsonUtil, Keyman.Setup.System.MsiUtils, Keyman.Setup.System.SetupUILanguageManager, KeymanVersion, kmpinffile, utilfiletypes, versioninfo; function FileSizeByName(const AFilename: string): Int64; forward; { TInstallInfo } constructor TInstallInfo.Create(const ATempPath: string); begin inherited Create; FTempPath := ATempPath; FAppName := SKeymanDesktopName; FMsiLocations := TInstallInfoFileLocations.Create; FPackages := TInstallInfoPackages.Create; FStrings := TStringList.Create; FTier := KeymanVersion.CKeymanVersionInfo.Tier; FShouldInstallKeyman := True; end; destructor TInstallInfo.Destroy; begin FreeAndNil(FPackages); FreeAndNil(FStrings); FreeAndNil(FMsiLocations); inherited Destroy; end; function TInstallInfo.GetBestMsi: TInstallInfoFileLocation; var location: TInstallInfoFileLocation; begin if FBestMsi <> nil then Exit(FBestMsi); if FMsiLocations.Count = 0 then Exit(nil); Result := FMsiLocations[0]; for location in FMsiLocations do case CompareVersions(Result.Version, location.Version) of 0: if Result.LocationType = iilOnline then Result := location; // prefer local for same version 1: Result := location; end; FBestMsi := Result; end; function TInstallInfo.Text(const Name: TInstallInfoText): WideString; var s: WideString; begin s := GetEnumName(TypeInfo(TInstallInfoText), Ord(Name)); Result := FStrings.Values[s]; if Result = '' then Result := TSetupUILanguageManager.Get(Name); Result := ReplaceText(Result, '$VERSION', SKeymanVersion); Result := ReplaceText(Result, '$APPNAME', FAppName); end; procedure TInstallInfo.LoadSetupInf(const SetupInfPath: string); var i: Integer; FInSetup: Boolean; FInPackages: Boolean; FInStrings: Boolean; location: TInstallInfoFileLocation; s, val, nm: string; pack: TInstallInfoPackage; packLocation: TInstallInfoPackageFileLocation; FVersion, FMSIFileName: string; FVersionWithTag: string; begin FInSetup := False; FInPackages := False; FInStrings := False; with TStringList.Create do try LoadFromFile(SetupInfPath + 'setup.inf'); // We'll just use the preamble for encoding // I3476 for i := 0 to Count - 1 do begin s := Trim(Strings[i]); if s = '' then Continue; nm := Trim(KeyNames[i]); val := Trim(ValueFromIndex[i]); if Copy(s, 1, 1) = '[' then begin FInSetup := WideSameText(s, '[Setup]'); FInPackages := WideSameText(s, '[Packages]'); FInStrings := WideSameText(s, '[Strings]'); end else if FInSetup then begin if WideSameText(nm, 'AppName') then FAppName := val else if WideSameText(nm, 'MSIFileName') then FMSIFileName := SetupInfPath + val else if WideSameText(nm, 'TitleImage') then FTitleImageFileName := SetupInfPath + val else if WideSameText(nm, 'StartWithConfiguration') then FStartWithConfiguration := StrToBoolDef(val, False) else if WideSameText(nm, 'StartDisabled') then FStartDisabled := StrToBoolDef(val, False); end else if FInPackages then begin if System.SysUtils.FileExists(SetupInfPath + nm) then begin pack := FPackages.FindById(ChangeFileExt(nm, ''), True); packLocation := TInstallInfoPackageFileLocation.Create(iilLocal); packLocation.Path := SetupInfPath + nm; pack.Locations.Add(packLocation); // Previously, the Name and Version of the package would be read from // setup.inf. Now, we load these details from the packages themselves end; end else if FInStrings then FStrings.Add(s); end; if System.SysUtils.FileExists(FMSIFileName) then // I3476 begin FVersion := GetMsiVersion(FMSIFileName, FVersionWithTag); if FVersion <> '' then begin location := TInstallInfoFileLocation.Create(iilLocal); location.Path := FMSIFileName; location.Version := FVersion; location.VersionWithTag := FVersionWithTag; FMsiLocations.Add(location); end; end; if not System.SysUtils.FileExists(FTitleImageFileName) then // I3476 FTitleImageFileName := ''; finally Free; end; end; procedure TInstallInfo.LocatePackagesFromParameter(const Param: string); var n: Integer; opt, res: TArray; id, FBCP47: string; begin res := TRegEx.Split(Param, ','); for n := 0 to Length(res) - 1 do begin opt := TRegEx.Split(res[n], '='); id := opt[0].ToLower.Trim; if Length(opt) > 1 then FBCP47 := opt[1].ToLower.Trim else FBCP47 := '_'; FPackages.Add(TInstallInfoPackage.Create(id, FBCP47)); end; end; procedure TInstallInfo.LocatePackagesAndTierFromFilename(Filename: string); const SKeymanSetupPrefix = 'keyman-setup'; SKeymanSetup_Alpha = SKeymanSetupPrefix+'-'+TIER_ALPHA; SKeymanSetup_Beta = SKeymanSetupPrefix+'-'+TIER_BETA; SKeymanSetup_Stable = SKeymanSetupPrefix+'-'+TIER_STABLE; var n: Integer; res: TArray; p, id, FBCP47: string; m: TMatch; begin // Get just the base filename Filename := ExtractFileName(ChangeFileExt(Filename, '')); // Strip " (1)", "(1)" or "[1]" appended for multiple downloads of same file // by common browsers. Another pattern "_1" is dealt with later. See also #4886. m := TRegEx.Match(Filename, '^('+SKeymanSetupPrefix+'.+)((( ?)\(\d+\))|(\[\d+\]))$'); if m.Success then Filename := m.Groups[1].Value; // Look for our recognised pattern of keyman-setup.package_id.bcp47... res := TRegEx.Split(Filename, '\.'); if (Length(res) < 1) or not res[0].ToLower.StartsWith(SKeymanSetupPrefix) then // No packages embedded in filename, or not a recognised filename pattern Exit; // Look for an embedded tier in the filename, if not set, use default p := res[0].ToLower; if p.Equals(SKeymanSetup_Stable) then FTier := TIER_STABLE else if p.Equals(SKeymanSetup_Beta) then FTier := TIER_BETA else if p.Equals(SKeymanSetup_Alpha) then FTier := TIER_ALPHA; // There is at least one browser that appends `_n` to a filename for // duplicate downloads. This is a little tricky to handle because `_n` // is a valid pattern for package filenames, although not for our BCP 47 // tags. If the last component is a BCP 47 tag, then we strip it, otherwise // we are going to search for both the package id with `_n` and the package id // without it -- hopefully, there should be only one match -- and worst case // we end up with two packages installed which is better than none. Ref #4886. m := TRegEx.Match(res[High(res)], '^(.+)_\d+$'); if m.Success then begin if Length(res) mod 2 = 1 then begin // If this is for a BCP-47 code, we'll strip it and ignore // Array length will be odd if we have a final BCP 47: // [0]:keyman-setup [1]:package-name [2]:bcp-47 res[High(res)] := m.Groups[1].Value; end else begin // Let's add another result! SetLength(res, Length(res)+2); res[High(res)-1] := ''; // empty BCP 47 for previous package res[High(res)] := m.Groups[1].Value; // and our stripped package id end; end; n := 1; while n < Length(res) do begin id := res[n].ToLower.Trim; Inc(n); if n < Length(res) then FBCP47 := res[n].ToLower.Trim else FBCP47 := '_'; Inc(n); FPackages.Add(TInstallInfoPackage.Create(id, FBCP47)); end; end; procedure TInstallInfo.LoadFromJSON(o: TJSONObject); var a: TJSONArray; item: TJSONValue; n: Integer; begin FTempPath := o.GetValue('TempPath', ''); FAppName := o.GetValue('AppName', ''); FStartDisabled := o.GetValue('StartDisabled', False); FStartWithConfiguration := o.GetValue('StartWithConfiguration', False); FShouldInstallKeyman := o.GetValue('ShouldInstallKeyman', True); FTier := o.GetValue('Tier', KeymanVersion.CKeymanVersionInfo.Tier); FPackages.Clear; a := o.GetValue('Packages', nil); if Assigned(a) then FPackages.LoadFromJSON(a); FStrings.Clear; a := o.GetValue('Strings', nil); if Assigned(a) then for item in a do FStrings.Add(item.AsType); FMsiLocations.Clear; a := o.GetValue('MsiLocations', nil); if Assigned(a) then FMsiLocations.LoadFromJSON(a); n := o.GetValue('BestMsi', -1); if (n >= 0) and (n < FMsiLocations.Count) then FBestMsi := FMsiLocations[n] else FBestMsi := nil; n := o.GetValue('MsiInstallLocation', -1); if (n >= 0) and (n < FMsiLocations.Count) then FMsiInstallLocation := FMsiLocations[n] else FMsiInstallLocation := nil; FTitleImageFilename := o.GetValue('TitleImageFilename', ''); if (FTitleImageFilename <> '') and not FileExists(FTitleImageFilename) then FTitleImageFilename := ''; //>> always calculate this FInstalledVersion.Version := : TMSIInfo; //>> always calculate this FIsInstalled: Boolean; //>> always calculate this FIsNewerAvailable: Boolean; end; procedure TInstallInfo.SaveToJSON(o: TJSONObject); var a: TJSONArray; s: string; begin o.AddPair('TempPath', FTempPath); o.AddPair('AppName', FAppName); o.AddPair('StartDisabled', TJSONBool.Create(FStartDisabled)); o.AddPair('StartWithConfiguration', TJSONBool.Create(FStartWithConfiguration)); o.AddPair('ShouldInstallKeyman', TJSONBool.Create(FShouldInstallKeyman)); o.AddPair('Tier', FTier); a := TJSONArray.Create; o.AddPair('Packages', a); FPackages.SaveToJSON(a); a := TJSONArray.Create; o.AddPair('Strings', a); for s in FStrings do a.Add(s); a := TJSONArray.Create; o.AddPair('MsiLocations', a); FMsiLocations.SaveToJSON(a); o.AddPair('BestMsi', TJSONNumber.Create(FMsiLocations.IndexOf(FBestMsi))); o.AddPair('MsiInstallLocation', TJSONNumber.Create(FMsiLocations.IndexOf(FMsiInstallLocation))); o.AddPair('TitleImageFilename', FTitleImageFilename); end; procedure TInstallInfo.LoadFromJSONFile(const Filename: string); var o: TJSONObject; s: TStringStream; begin s := TStringStream.Create('', TEncoding.UTF8); try s.LoadFromFile(Filename); o := TJSONObject.ParseJSONValue(s.DataString) as TJSONObject; if Assigned(o) then try LoadFromJSON(o); finally o.Free; end; finally s.Free; end; end; procedure TInstallInfo.SaveToJSONFile(const Filename: string); var o: TJSONObject; ss: TStringStream; s: TStringList; begin o := TJSONObject.Create; SaveToJSON(o); s := TStringList.Create; try // We use this instead of o.Format because o.Format inserts // \ in front of / which is ugly and unnecessary PrettyPrintJSON(o, s); ss := TStringStream.Create(s.Text, TEncoding.UTF8); try ss.SaveToFile(Filename); finally ss.Free; end; finally s.Free; end; end; procedure TInstallInfo.LoadLocalPackagesMetadata; var pack: TInstallInfoPackage; packLocation: TInstallInfoPackageFileLocation; lang: TPackageKeyboardLanguage; iipl: TInstallInfoPackageLanguage; begin for pack in FPackages do begin for packLocation in pack.Locations do begin if (packLocation.LocationType = iilLocal) and GetPackageMetadata(packLocation.Path, packLocation.LocalPackage) then begin packLocation.Size := FileSizeByName(packLocation.Path); packLocation.Name := packLocation.LocalPackage.Info.Desc[PackageInfo_Name]; packLocation.Version := packLocation.LocalPackage.Info.Desc[PackageInfo_Version]; if packLocation.LocalPackage.Keyboards.Count = 1 then begin // We only populate the language list if there is a single keyboard // in the package. Otherwise, we let Keyman install default lang // for each keyboard in the package. for lang in packLocation.LocalPackage.Keyboards[0].Languages do begin iipl := TInstallInfoPackageLanguage.Create(lang.ID, lang.Name); packLocation.Languages.Add(iipl); end; end; end; end; end; end; function TInstallInfo.GetPackageMetadata(const KmpFilename: string; p: TPackage): Boolean; var zip: TZipFile; ms: TStream; h: TZipHeader; begin try zip := TZipFile.Create; try zip.Open(KmpFilename, zmRead); if zip.IndexOf('kmp.json') >= 0 then begin // Read kmp.json zip.Read('kmp.json', ms, h); try ms.Position := 0; p.LoadJSONFromStream(ms); finally ms.Free; end; Exit(True); end else if zip.IndexOf('kmp.inf') >= 0 then begin // Read kmp.inf (phooey) zip.Extract('kmp.inf', FTempPath, False); p.FileName := FTempPath + 'kmp.inf'; p.LoadIni; Exit(True); end else // Not a valid package Exit(False); finally zip.Free; end; except on E:EZipException do // TODO: log this to the logfile, but don't show Exit(False); end; end; function TInstallInfo.Text(const Name: TInstallInfoText; const Args: array of const): WideString; begin Result := WideFormat(Text(Name), Args); end; function TInstallInfo.CheckMsiUpgradeScenarios: Boolean; begin // Keyman install scenarios // 1. Keyman latest version already installed --> nothing to do // 2. Keyman outdated version already installed --> optional upgrade // 3. Keyman not installed --> install required // Keyman source scenarios // A. Available locally, latest version (or working offline) // B. Available locally, outdated version; online update available // C. Not available locally, online update available // D. Not available (working offline) // Matrix: // 1ABCD: Nothing to do // 2A: offer to upgrade from local // 2B: offer to upgrade from online (don't offer local update even if newer than current install, just for simplicity?) // 2C: offer to upgrade from online // 2D: Nothing to do // 3A: require install // 3B: offer to install from online (if update not taken, require install from local) // 3C: require download // 3D: Fail FBestMsi := GetBestMsi; FMsiInstallLocation := FBestMsi; FIsInstalled := FInstalledVersion.Version <> ''; if not Assigned(FBestMsi) then // Matrix 123D FIsNewerAvailable := False else if FIsInstalled then // Matrix 12ABC FIsNewerAvailable := CompareVersions(FInstalledVersion.Version, FBestMsi.Version) > 0 else // Matrix 3ABC FIsNewerAvailable := True; Result := FIsInstalled or FIsNewerAvailable; end; procedure TInstallInfo.CheckPackageLocations; var pack: TInstallInfoPackage; begin for pack in FPackages do pack.InstallLocation := pack.GetBestLocation; end; { TInstallInfoPackage } constructor TInstallInfoPackage.Create(const AID: string; const ABCP47: string = ''); begin inherited Create; FLocations := TInstallInfoPackageFileLocations.Create; FID := AID; if ABCP47 <> '_' then FBCP47 := ABCP47; FShouldInstall := True; end; destructor TInstallInfoPackage.Destroy; begin FreeAndNil(FLocations); inherited Destroy; end; function TInstallInfoPackage.GetBestLocation: TInstallInfoPackageFileLocation; var location: TInstallInfoPackageFileLocation; begin if FLocations.Count = 0 then Exit(nil); Result := FLocations[0]; for location in FLocations do case CompareVersions(Result.Version, location.Version) of 0: if Result.LocationType = iilOnline then Result := location; // prefer local for same version 1: Result := location; end; end; procedure TInstallInfoPackage.LoadFromJSON(o: TJSONObject); var a: TJSONArray; n: Integer; begin // FID: string; FBCP47 := o.GetValue('bcp47', ''); FLocations.Clear; a := o.GetValue('locations', nil); if Assigned(a) then FLocations.LoadFromJSON(a); FShouldInstall := o.GetValue('shouldInstall', True); n := o.GetValue('installLocation', -1); if (n >= 0) and (n < FLocations.Count) then FInstallLocation := FLocations[n] else FInstallLocation := nil; end; procedure TInstallInfoPackage.SaveToJSON(o: TJSONObject); var a: TJSONArray; begin o.AddPair('id', FID); o.AddPair('bcp47', FBCP47); a := TJSONArray.Create; o.AddPair('locations', a); FLocations.SaveToJSON(a); o.AddPair('shouldInstall', TJSONBool.Create(FShouldInstall)); o.AddPair('installLocation', TJSONNumber.Create(FLocations.IndexOf(FInstallLocation))); end; { TInstallInfoPackageLanguage } constructor TInstallInfoPackageLanguage.Create(const ABCP47, AName: string); begin inherited Create; FBCP47 := ABCP47; FName := AName; end; { TInstallInfoPackageFileLocations } function TInstallInfoPackageFileLocations.LatestVersion( const default: string): string; var location: TInstallInfoPackageFileLocation; begin Result := default; for location in Self do if CompareVersions(Result, location.Version) > 0 then Result := location.Version; end; procedure TInstallInfoPackageFileLocations.LoadFromJSON(a: TJSONArray); var item: TJSONValue; fileLocationItem: TJSONObject; fileLocation: TInstallInfoPackageFileLocation; begin for item in a do begin if item is TJSONObject then begin fileLocationItem := item as TJSONObject; fileLocation := TInstallInfoPackageFileLocation.Create(fileLocationItem.GetValue('locationType', iilLocal)); fileLocation.LoadFromJSON(fileLocationItem); Self.Add(fileLocation); end; end; end; procedure TInstallInfoPackageFileLocations.SaveToJSON(a: TJSONArray); var item: TInstallInfoPackageFileLocation; o: TJSONObject; begin for item in Self do begin o := TJSONObject.Create; item.SaveToJSON(o); a.Add(o); end; end; { TInstallInfoFileLocations } function TInstallInfoFileLocations.LatestVersion(const default: string): string; var location: TInstallInfoFileLocation; begin Result := default; for location in Self do if CompareVersions(Result, location.Version) > 0 then Result := location.Version; end; procedure TInstallInfoFileLocations.LoadFromJSON(a: TJSONArray); var item: TJSONValue; fileLocationItem: TJSONObject; fileLocation: TInstallInfoFileLocation; begin for item in a do begin if item is TJSONObject then begin fileLocationItem := item as TJSONObject; fileLocation := TInstallInfoFileLocation.Create(fileLocationItem.GetValue('locationType', iilLocal)); fileLocation.LoadFromJSON(fileLocationItem); Self.Add(fileLocation); end; end; end; procedure TInstallInfoFileLocations.SaveToJSON(a: TJSONArray); var item: TInstallInfoFileLocation; o: TJSONObject; begin for item in Self do begin o := TJSONObject.Create; item.SaveToJSON(o); a.Add(o); end; end; { TInstallInfoPackages } function TInstallInfoPackages.FindById(const id: string; createIfNotFound: Boolean): TInstallInfoPackage; begin for Result in Self do if SameText(id, Result.ID) then Exit; if createIfNotFound then begin Result := TInstallInfoPackage.Create(id); Add(Result); end else Result := nil; end; procedure TInstallInfoPackages.LoadFromJSON(a: TJSONArray); var item: TJSONValue; packageItem: TJSONObject; package: TInstallInfoPackage; begin for item in a do begin if item is TJSONObject then begin packageItem := item as TJSONObject; package := TInstallInfoPackage.Create(packageItem.GetValue('id', '')); package.LoadFromJSON(packageItem); Self.Add(package); end; end; end; procedure TInstallInfoPackages.SaveToJSON(a: TJSONArray); var item: TInstallInfoPackage; o: TJSONObject; begin for item in Self do begin o := TJSONObject.Create; item.SaveToJSON(o); a.Add(o); end; end; { TInstallInfoPackageFileLocation } constructor TInstallInfoPackageFileLocation.Create(ALocationType: TInstallInfoLocationType); begin inherited Create(ALocationType); FLanguages := TInstallInfoPackageLanguages.Create; FLocalPackage := TKmpInfFile.Create; end; destructor TInstallInfoPackageFileLocation.Destroy; begin FreeAndNil(FLanguages); FreeAndNil(FLocalPackage); inherited Destroy; end; function TInstallInfoPackageFileLocation.GetLanguageNameFromBCP47( const bcp47: string): string; var lang: TInstallInfoPackageLanguage; begin Result := ''; for lang in FLanguages do if SameText(lang.BCP47, bcp47) then Exit(lang.Name); end; function TInstallInfoPackageFileLocation.GetNameOrID(const id: string): string; begin if FName = '' then Result := id else Result := FName; end; procedure TInstallInfoPackageFileLocation.LoadFromJSON(o: TJSONObject); var a: TJSONArray; item: TJSONValue; begin inherited LoadFromJSON(o); FName := o.GetValue('name', ''); FLanguages.Clear; a := o.GetValue('languages', nil); if Assigned(a) then for item in a do begin if item is TJSONObject then begin FLanguages.Add(TInstallInfoPackageLanguage.Create( item.GetValue('bcp47', ''), item.GetValue('name', '') )); end; end; // TODO: FLocalPackage: TPackage; end; procedure TInstallInfoPackageFileLocation.SaveToJSON(o: TJSONObject); var a: TJSONArray; ol: TJSONObject; item: TInstallInfoPackageLanguage; begin inherited SaveToJSON(o); o.AddPair('name', FName); a := TJSONArray.Create; o.AddPair('languages', a); for item in FLanguages do begin ol := TJSONObject.Create; ol.AddPair('bcp47', item.BCP47); ol.AddPair('name', item.Name); a.Add(ol); end; // TODO: FLocalPackage: TPackage; end; { TInstallInfoFileLocation } constructor TInstallInfoFileLocation.Create( ALocationType: TInstallInfoLocationType); begin inherited Create; FLocationType := ALocationType; end; procedure TInstallInfoFileLocation.LoadFromJSON(o: TJSONObject); begin FUrl := o.GetValue('url', ''); FSize := o.GetValue('size', 0); FVersion := o.GetValue('version', ''); FPath := o.GetValue('path', ''); FProductCode := o.GetValue('productCode', ''); FVersionWithTag := o.GetValue('versionWithTag', ''); end; procedure TInstallInfoFileLocation.SaveToJSON(o: TJSONObject); begin o.AddPair('locationType', TJSONNumber.Create(Ord(FLocationType))); o.AddPair('url', FUrl); o.AddPair('size', TJSONNumber.Create(FSize)); o.AddPair('version', FVersion); o.AddPair('path', FPath); o.AddPair('productCode', FProductCode); o.AddPair('versionWithTag', FVersionWithTag); end; procedure TInstallInfoFileLocation.UpgradeToLocalPath(const ARootPath: string); begin Assert(FLocationType = iilOnline); Assert(FileExists(ARootPath + FPath)); FPath := ARootPath + FPath; FLocationType := iilLocal; end; // Pulled from IdGlobalProtocols so we don't have to import it // OS-independent version function FileSizeByName(const AFilename: string): Int64; var LHandle : THandle; LRec : TWin32FindData; begin Result := -1; LHandle := Winapi.Windows.FindFirstFile(PChar(AFileName), LRec); if LHandle <> INVALID_HANDLE_VALUE then begin Winapi.Windows.FindClose(LHandle); if (LRec.dwFileAttributes and Winapi.Windows.FILE_ATTRIBUTE_DIRECTORY) = 0 then begin Result := (Int64(LRec.nFileSizeHigh) shl 32) + LRec.nFileSizeLow; end; end; end; end.