From 5464272445d192cb3f5045f2ed747c95c46558f5 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Sat, 5 Aug 2023 22:20:06 +0000 Subject: [PATCH] fpspreadsheet: Improved password handling and format detection for the decryption readers. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8913 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../source/common/fpsopendocument.pas | 37 +++++---- .../source/common/fpsreaderwriter.pas | 67 ++++++++++++++-- .../fpspreadsheet/source/common/fpstypes.pas | 5 ++ .../fpspreadsheet/source/common/xlsxooxml.pas | 25 ++++++ .../source/crypto/fpsopendocument_crypto.pas | 3 +- .../source/crypto/xlsxdecrypter.pas | 2 +- .../source/crypto/xlsxooxml_crypto.pas | 35 +++++++-- .../source/visual/fpspreadsheetctrls.pas | 77 ++++++++++++++----- 8 files changed, 200 insertions(+), 51 deletions(-) diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index a5abc51ad..8f33377a0 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -156,7 +156,7 @@ type var AFontColor: TsColor); function ReadHeaderFooterText(ANode: TDOMNode): String; procedure ReadMetaData(ANode: TDOMNode); - procedure ReadMetaInfManifest(ANode: TDOMNode; out IsEncrypted: Boolean); + procedure ReadMetaInfManifest(ANode: TDOMNode); procedure ReadPictures(AStream: TStream); procedure ReadPrintRanges(ATableNode: TDOMNode; ASheet: TsBasicWorksheet); procedure ReadRowsAndCells(ATableNode: TDOMNode); @@ -177,6 +177,7 @@ type protected FPointSeparatorSettings: TFormatSettings; procedure AddBuiltinNumFormats; override; + function NeedsPassword(AStream: TStream): Boolean; override; procedure ReadAutomaticStyles(AStylesNode: TDOMNode); procedure ReadMasterStyles(AStylesNode: TDOMNode); procedure ReadNumFormats(AStylesNode: TDOMNode); @@ -203,7 +204,6 @@ type function Decrypt(AStream: TStream; ADecryptionInfo: TsOpenDocManifestFileEntry; APassword: String; ADestStream: TStream; out AErrorMsg: String): Boolean; virtual; - function SupportsDecryption: Boolean; virtual; function UnzipToStream(AStream: TStream; AZippedFile: String; ADestStream: TStream): Boolean; virtual; function UnzipToStream(AStream: TStream; AZippedFile: String; @@ -1630,6 +1630,22 @@ begin Result := -1; end; +{ Returns true if the file is password-protected, i.e. if there are entries + in the META-INF/manifest.xml while contain decryption information. } +function TsSpreadOpenDocReader.NeedsPassword(AStream: TStream): Boolean; +var + i: Integer; +begin + Unused(AStream); + for i := 0 to FManifestFileEntries.Count-1 do + if TsOpenDocManifestFileEntry(FManifestFileEntries[i]).Encrypted then + begin + Result := true; + exit; + end; + Result := false; +end; + function TsSpreadOpenDocReader.NodeIsEmptyCell(ACellNode: TDOMNode): Boolean; var valuestr: String; @@ -2062,8 +2078,7 @@ end; // Reads the file META-INF/manifest.xml. It is never encrypted and contains // decryption information. -procedure TsSpreadOpenDocReader.ReadMetaInfManifest(ANode: TDOMNode; - out IsEncrypted: Boolean); +procedure TsSpreadOpenDocReader.ReadMetaInfManifest(ANode: TDOMNode); function GetAlgorithmName(ASubNode: TDOMNode; AttrName: String): String; var @@ -2093,7 +2108,6 @@ var nodeName: String; entry: TsOpenDocManifestFileEntry; begin - IsEncrypted := false; while ANode <> nil do begin nodeName := ANode.NodeName; @@ -2107,7 +2121,6 @@ begin nodeName := encryptionDataNode.NodeName; if nodeName = 'manifest:encryption-data' then begin - IsEncrypted := true; entry.Encrypted := true; entry.EncryptionData_ChecksumType := GetAlgorithmName(encryptionDataNode, 'manifest:checksum-type'); entry.EncryptionData_Checksum := GetAttrValue(encryptionDataNode, 'manifest:checksum'); @@ -2924,9 +2937,8 @@ begin ReadXMLStream(Doc, XMLStream); if Assigned(Doc) then begin - ReadMetaInfManifest(Doc.DocumentElement.FindNode('manifest:file-entry'), isEncrypted); - if isEncrypted and not SupportsDecryption then - raise EFpSpreadsheetReader.Create('File is encrypted.'); + ReadMetaInfManifest(Doc.DocumentElement.FindNode('manifest:file-entry')); + CheckPassword(XMLStream, APassword); end; end; finally @@ -5612,13 +5624,6 @@ begin Result := false; end; -{ If a descendant reader class supports decryption it must return true - here. The standard ods reader is not able to read encrypted file content. } -function TsSpreadOpenDocReader.SupportsDecryption: Boolean; -begin - Result := false; -end; - function TsSpreadOpenDocReader.UnzipToStream(AStream: TStream; AZippedFile: String; ADestStream: TStream): Boolean; begin diff --git a/components/fpspreadsheet/source/common/fpsreaderwriter.pas b/components/fpspreadsheet/source/common/fpsreaderwriter.pas index 6eab237ac..493bcf042 100644 --- a/components/fpspreadsheet/source/common/fpsreaderwriter.pas +++ b/components/fpspreadsheet/source/common/fpsreaderwriter.pas @@ -48,12 +48,16 @@ type public { File format detection } class function CheckFileFormat(AStream: TStream): boolean; virtual; abstract; - { General writing methods } + { General reading methods } procedure ReadFromFile(AFileName: string; APassword: String = ''; AParams: TsStreamParams = []); virtual; abstract; procedure ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); virtual; abstract; procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); virtual; abstract; + { Related to password-protected files } + procedure CheckPassword(AStream: TStream; var APassword: String); virtual; + function NeedsPassword(AStream: TStream): Boolean; virtual; + function SupportsDecryption: Boolean; virtual; end; { TsBasicSpreadWriter } @@ -263,6 +267,53 @@ begin end; +{------------------------------------------------------------------------------} +{ TsBasicSpreadReader } +{------------------------------------------------------------------------------} + +{@@ ---------------------------------------------------------------------------- + Checks whether the currently processed stream is password-protected. + If true, it checks whether the reader class supports decryption and makes + sure that a password is provided to the calling routine (ReadFromStream). + Exceptions are raised in the error cases. + Must be called at the beginning of ReadFromStream when the stream potentially + can be decrypted in order to provide the user a reasonable error message. +-------------------------------------------------------------------------------} +procedure TsBasicSpreadReader.CheckPassword(AStream: TStream; + var APassword: String); +begin + if NeedsPassword(AStream) then + begin + if SupportsDecryption then + begin + if (APassword = '') and Assigned(FWorkbook.OnQueryPassword) then + APassword := FWorkbook.OnQueryPassword(); + if (APassword = '') then + raise EFpSpreadsheetReader.Create('Password required to open this workbook.'); + end else + raise EFpSpreadsheetReader.Create('File is encrypted.'); + end; +end; + +{@@ ---------------------------------------------------------------------------- + Should return whether the workbook to be loaded is password-protected by + encryption. +-------------------------------------------------------------------------------} +function TsBasicSpreadReader.NeedsPassword(AStream: TStream): Boolean; +begin + Unused(AStream); + Result := false; +end; + +{@@ ---------------------------------------------------------------------------- + Returns true if this reader class is able to decrypt a password-protected + workbook file/stream. +-------------------------------------------------------------------------------} +function TsBasicSpreadReader.SupportsDecryption: Boolean; +begin + Result := false; +end; + {------------------------------------------------------------------------------} { TsBasicSpreadWriter } {------------------------------------------------------------------------------} @@ -507,7 +558,9 @@ end; Opens the file and calls ReadFromStream. Data are stored in the workbook specified during construction. - @param AFileName The input file name. + @param (AFileName The input file name.) + @param (APassword The password needed to open a password-protected workbook. + Note: Password-protected files are not supported by all readers.) @see TsWorkbook -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.ReadFromFile(AFileName: string; @@ -545,11 +598,9 @@ end; Its basic implementation here assumes that the stream is a TStringStream and the data are provided by calling ReadFromStrings. This mechanism is valid - for wikitables. + for wiki-tables. Data will be stored in the workbook defined at construction. - - @param AData Workbook which is filled by the data from the stream. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.ReadFromStream(AStream: TStream; APassword: String; AParams: TsStreamParams = []); @@ -573,7 +624,7 @@ end; {@@ ---------------------------------------------------------------------------- Reads workbook data from a string list. This abstract implementation does - nothing and raises an exception. Must be overridden, like for wikitables. + nothing and raises an exception. Must be overridden, like for wiki-tables. -------------------------------------------------------------------------------} procedure TsCustomSpreadReader.ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); @@ -593,8 +644,8 @@ end; Creates an internal instance of the number format list according to the file format being read/written. - @param AWorkbook Workbook from with the file is written. This parameter is - passed from the workbook which creates the writer. + @param (AWorkbook Workbook from which the file is written. This parameter is + passed from the workbook which creates the writer.) -------------------------------------------------------------------------------} constructor TsCustomSpreadWriter.Create(AWorkbook: TsBasicWorkbook); begin diff --git a/components/fpspreadsheet/source/common/fpstypes.pas b/components/fpspreadsheet/source/common/fpstypes.pas index d0b1abf72..2c863dcd8 100644 --- a/components/fpspreadsheet/source/common/fpstypes.pas +++ b/components/fpspreadsheet/source/common/fpstypes.pas @@ -1228,6 +1228,8 @@ type property Keywords: TStrings read FKeywords write FKeywords; end; + TsOnQueryPassword = function: String of object; + {@@ Basic worksheet class to avoid circular unit references. It has only those properties and methods which do not require any other unit than fpstypes. } TsBasicWorksheet = class @@ -1256,6 +1258,7 @@ type TsBasicWorkbook = class private FLog: TStringList; + FOnQueryPassword: TsOnQueryPassword; function GetErrorMsg: String; protected FFileName: String; @@ -1292,6 +1295,8 @@ type property Protection: TsWorkbookProtections read FProtection write FProtection; {@@ Units of row heights and column widths } property Units: TsSizeUnits read FUnits; + {@@ Event returning the password to open a password-protected workbook } + property OnQueryPassword: TsOnQueryPassword read FOnQueryPassword write FOnQueryPassword; end; {@@ Ancestor of the fpSpreadsheet exceptions } diff --git a/components/fpspreadsheet/source/common/xlsxooxml.pas b/components/fpspreadsheet/source/common/xlsxooxml.pas index 1703344af..03a7e5f2f 100644 --- a/components/fpspreadsheet/source/common/xlsxooxml.pas +++ b/components/fpspreadsheet/source/common/xlsxooxml.pas @@ -141,6 +141,8 @@ type protected FFirstNumFormatIndexInFile: Integer; procedure AddBuiltinNumFormats; override; + class function IsEncrypted(AStream: TStream): Boolean; + function NeedsPassword(AStream: TStream): Boolean; override; public constructor Create(AWorkbook: TsBasicWorkbook); override; destructor Destroy; override; @@ -359,6 +361,8 @@ const LAST_PALETTE_INDEX = 63; + CFB_SIGNATURE = $E11AB1A1E011CFD0; // Compound File Binary Signature + type TFillListData = class PatternType: String; @@ -951,6 +955,25 @@ begin Result := TMemoryStream.Create; end; +{ Checks the file header for the signature of the decrypted file format. } +class function TsSpreadOOXMLReader.IsEncrypted(AStream: TStream): Boolean; +var + p: Int64; + buf: Cardinal; +begin + p := AStream.Position; + AStream.Position := 0; + AStream.Read(buf, SizeOf(buf)); + Result := (buf = Cardinal(CFB_SIGNATURE)); + AStream.Position := p; +end; + +{ Returns TRUE if the file is encrypted and requires a password. } +function TsSpreadOOXMLReader.NeedsPassword(AStream: TStream): Boolean; +begin + Result := IsEncrypted(AStream); +end; + procedure TsSpreadOOXMLReader.ReadActiveSheet(ANode: TDOMNode; out ActiveSheetIndex: Integer); var @@ -4411,6 +4434,8 @@ begin Unused(APassword, AParams); Doc := nil; + CheckPassword(AStream, APassword); + try // Retrieve theme colors XMLStream := CreateXMLStream; diff --git a/components/fpspreadsheet/source/crypto/fpsopendocument_crypto.pas b/components/fpspreadsheet/source/crypto/fpsopendocument_crypto.pas index f10b21b55..be22d681a 100644 --- a/components/fpspreadsheet/source/crypto/fpsopendocument_crypto.pas +++ b/components/fpspreadsheet/source/crypto/fpsopendocument_crypto.pas @@ -10,7 +10,7 @@ uses {$IFDEF UNZIP_ABBREVIA} ABUnzper, {$ENDIF} - fpsTypes, fpsOpenDocument; + fpsTypes, fpsUtils, fpsOpenDocument; type TsSpreadOpenDocReaderCrypto = class(TsSpreadOpenDocReader) @@ -141,7 +141,6 @@ begin raise EFpSpreadsheetReader.Create('Unsupported key generation method ' + ADecryptionInfo.KeyDerivationName); end; - { Tells the calling routine that this reader is able to decrypt ods files. } function TsSpreadOpenDocReaderCrypto.SupportsDecryption: Boolean; begin diff --git a/components/fpspreadsheet/source/crypto/xlsxdecrypter.pas b/components/fpspreadsheet/source/crypto/xlsxdecrypter.pas index 59d958b13..3c510f6b4 100644 --- a/components/fpspreadsheet/source/crypto/xlsxdecrypter.pas +++ b/components/fpspreadsheet/source/crypto/xlsxdecrypter.pas @@ -18,7 +18,7 @@ uses CFB_Signature = $E11AB1A1E011CFD0; // Compound File Binary Signature // Weird is the documentation is equal to // $D0CF11E0A1B11AE1, but here is inversed - // maybe related to litle endian thing?!! + // maybe related to little endian thing?!! // EncryptionHeaderFlags as defined in 2.3.1 [MS-OFFCRYPTO] ehfAES = $00000004; diff --git a/components/fpspreadsheet/source/crypto/xlsxooxml_crypto.pas b/components/fpspreadsheet/source/crypto/xlsxooxml_crypto.pas index ad086ec5a..36c347a0d 100644 --- a/components/fpspreadsheet/source/crypto/xlsxooxml_crypto.pas +++ b/components/fpspreadsheet/source/crypto/xlsxooxml_crypto.pas @@ -6,11 +6,17 @@ interface uses Classes, - fpstypes, xlsxooxml, xlsxdecrypter; + fpstypes, fpsUtils, xlsxooxml, xlsxdecrypter; type TsSpreadOOXMLReaderCrypto = class(TsSpreadOOXMLReader) + private + FNeedsPassword: Boolean; + protected + function NeedsPassword(AStream: TStream): Boolean; override; + function SupportsDecryption: Boolean; override; public + class function CheckFileFormat(AStream: TStream): boolean; override; procedure ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); override; end; @@ -24,17 +30,34 @@ implementation uses fpsReaderWriter; +class function TsSpreadOOXMLReaderCrypto.CheckFileFormat(AStream: TStream): boolean; +begin + Result := inherited; // This checks for a normal xlsx format ... + if not Result then + Result := IsEncrypted(AStream); // ... and this for a decrypted one. +end; + +function TsSpreadOOXMLReaderCrypto.NeedsPassword(AStream: TStream): Boolean; +begin + Unused(AStream); + Result := FNeedsPassword; +end; + procedure TsSpreadOOXMLReaderCrypto.ReadFromStream(AStream: TStream; APassword: String = ''; AParams: TsStreamParams = []); var ExcelDecrypt : TExcelFileDecryptor; DecryptedStream: TStream; begin + FNeedsPassword := false; + ExcelDecrypt := TExcelFileDecryptor.Create; try AStream.Position := 0; if ExcelDecrypt.isEncryptedAndSupported(AStream) then begin + FNeedsPassword := true; + CheckPassword(AStream, APassword); DecryptedStream := TMemoryStream.Create; try ExcelDecrypt.Decrypt(AStream, DecryptedStream, UnicodeString(APassword)); @@ -42,12 +65,9 @@ begin AStream.Free; AStream := TMemoryStream.Create; DecryptedStream.Position := 0; - - TMemoryStream(decryptedStream).SaveToFile('decr.zip'); - DecryptedStream.Position := 0; - AStream.CopyFrom(DecryptedStream, DecryptedStream.Size); AStream.Position := 0; + FNeedsPassword := false; // AStream is not encrypted any more. finally DecryptedStream.Free; end; @@ -60,6 +80,11 @@ begin inherited; end; +function TsSpreadOOXMLReaderCrypto.SupportsDecryption: Boolean; +begin + Result := true; +end; + initialization diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas index 07ab70dd3..38c206361 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas @@ -27,8 +27,8 @@ unit fpspreadsheetctrls; interface uses - LMessages, LResources, LCLVersion, - Classes, Graphics, SysUtils, Controls, StdCtrls, ComCtrls, ValEdit, ActnList, + LCLType, LCLIntf, LCLProc, LCLVersion, LMessages, LResources, + Classes, Types, Graphics, SysUtils, Controls, StdCtrls, ComCtrls, ValEdit, ActnList, fpstypes, fpspreadsheet; const @@ -70,6 +70,7 @@ type FPendingOperation: TsCopyOperation; FOptions: TsWorkbookOptions; FOnError: TsWorkbookSourceErrorEvent; + FOnQueryPassword: TsOnQueryPassword; // Getters / setters function GetFileFormat: TsSpreadsheetFormat; @@ -96,10 +97,11 @@ type protected procedure AbortSelection; + function DoQueryPassword: String; procedure DoShowError(const AErrorMsg: String); procedure InternalCreateNewWorkbook(AWorkbook: TsWorkbook = nil); procedure InternalLoadFromFile(AFileName: string; AAutoDetect: Boolean; - AFormatID: TsSpreadFormatID; AWorksheetIndex: Integer = -1); + AFormatID: TsSpreadFormatID; AWorksheetIndex: Integer; APassword: String); procedure InternalLoadFromWorkbook(AWorkbook: TsWorkbook; AWorksheetIndex: Integer = -1); procedure Loaded; override; @@ -116,15 +118,13 @@ type public procedure CreateNewWorkbook; + procedure LoadFromProtectedSpreadsheetFile(AFileName: String; + AFormatID: TsSpreadFormatID; APassword: String; AWorksheetIndex: Integer = -1); procedure LoadFromSpreadsheetFile(AFileName: string; AFormat: TsSpreadsheetFormat; AWorksheetIndex: Integer = -1); overload; procedure LoadFromSpreadsheetFile(AFileName: string; AFormatID: TsSpreadFormatID = sfidUnknown; AWorksheetIndex: Integer = -1); overload; procedure LoadFromWorkbook(AWorkbook: TsWorkbook; AWorksheetIndex: Integer = -1); - { - procedure LoadFromSpreadsheetFile(AFileName: string; - AWorksheetIndex: Integer = -1); overload; - } procedure SaveToSpreadsheetFile(AFileName: string; AOverwriteExisting: Boolean = true); overload; @@ -133,9 +133,6 @@ type procedure SaveToSpreadsheetFile(AFileName: string; AFormatID: TsSpreadFormatID; AOverwriteExisting: Boolean = true); overload; -// procedure DisableControls; -// procedure EnableControls; - procedure SelectCell(ASheetRow, ASheetCol: Cardinal); procedure SelectWorksheet(AWorkSheet: TsWorksheet); @@ -183,6 +180,8 @@ type {@@ A message box is displayey if an error occurs during loading of a spreadsheet. This behavior can be replaced by means of the event OnError. } property OnError: TsWorkbookSourceErrorEvent read FOnError write FOnError; + {@@ Event fired when a password is required. Handler must return the pwd. } + property OnQueryPassword: TsOnQueryPassword read FOnQueryPassword write FOnQueryPassword; end; @@ -710,8 +709,7 @@ function ScalePPI(ALength: Integer): Integer; implementation uses - Types, Math, StrUtils, TypInfo, LCLType, LCLIntf, LCLProc, - Dialogs, Forms, Clipbrd, + Math, StrUtils, TypInfo, Dialogs, Forms, Clipbrd, fpsStrings, fpsCrypto, fpsReaderWriter, fpsUtils, fpsNumFormat, fpsImages, fpsHTMLUtils, fpsExprParser; @@ -1016,6 +1014,22 @@ begin SelectWorksheet(FWorksheet); end; +function TsWorkbookSource.DoQueryPassword: String; +var + crs: TCursor; +begin + crs := Screen.Cursor; + Screen.Cursor := crDefault; + try + if Assigned(FOnQueryPassword) then + Result := FOnQueryPassword() + else + Result := InputBox('Password required to open workbook', 'Password', ''); + finally + Screen.Cursor := crs; + end; +end; + {@@ ---------------------------------------------------------------------------- An error has occured during loading of the workbook. Shows a message box by default. But a different behavior can be obtained by means of the OnError @@ -1137,20 +1151,23 @@ end; for the loader. Is ignored when AAutoDetect is @false.) @param(AWorksheetIndex Index of the worksheet to be selected after loading.) + @param(APassword Password to open encrypted workbook. Note: this is + supported only by ods and xlsx readers.) -------------------------------------------------------------------------------} procedure TsWorkbookSource.InternalLoadFromFile(AFileName: string; - AAutoDetect: Boolean; AFormatID: TsSpreadFormatID; - AWorksheetIndex: Integer = -1); + AAutoDetect: Boolean; AFormatID: TsSpreadFormatID; AWorksheetIndex: Integer; + APassword: String); var book: TsWorkbook; begin book := TsWorkbook.Create; try book.Options := FOptions; + book.OnQueryPassword := @DoQueryPassword; if AAutoDetect then - book.ReadfromFile(AFileName) + book.ReadfromFile(AFileName, APassword) else - book.ReadFromFile(AFileName, AFormatID); + book.ReadFromFile(AFileName, AFormatID, APassword); InternalLoadFromWorkbook(book, AWorksheetIndex); except // book is normally used as current workbook. But it must be destroyed @@ -1220,9 +1237,11 @@ begin end; {@@ ---------------------------------------------------------------------------- - Public spreadsheet loader to be used if file format is known. + Public loader of a spreadsheet file. - Call this methdd for both built-in and user-provided file formats. + Call this method for both built-in and user-provided file formats. + + If the workbook is password-protected the password is prompted by a dialog. @param(AFilename Name of the spreadsheet file to be loaded.) @param(AFormatID Identifier of the spreadsheet file format assumed @@ -1236,7 +1255,7 @@ var autodetect: Boolean; begin autodetect := (AFormatID = sfidUnknown); - InternalLoadFromFile(AFileName, autodetect, AFormatID, AWorksheetIndex); + InternalLoadFromFile(AFileName, autodetect, AFormatID, AWorksheetIndex, ''); end; (* {@@ ------------------------------------------------------------------------------ @@ -1272,6 +1291,26 @@ begin InternalLoadFromWorkbook(AWorkbook, AWorksheetIndex); end; +{@@ ---------------------------------------------------------------------------- + Public loader of a spreadsheet file. + + Should be called in case of password-protected files when the password is + already known and no password dialog should appear. + + Call this method for both built-in and user-provided file formats. + + @param(AFilename Name of the spreadsheet file to be loaded.) + @param(AFormatID Identifier of the spreadsheet file format assumed + for the file.) + @param(AWorksheetIndex Index of the worksheet to be selected after loading. + (If empty then the active worksheet is loaded) ) +-------------------------------------------------------------------------------} +procedure TsWorkbookSource.LoadFromProtectedSpreadsheetFile(AFileName: String; + AFormatID: TsSpreadFormatID; APassword: String; AWorksheetIndex: Integer = -1); +begin + InternalLoadFromFile(AFileName, false, AFormatID, AWorksheetIndex, APassword); +end; + {@@ ---------------------------------------------------------------------------- Notifies listeners of workbook, worksheet, cell, or selection changes. The changed item is identified by the parameter AChangedItems.