How to get Excel version and macro security level - delphi

Microsoft has recently broken our longtime (and officially recommended by them) code to read the version of Excel and its current omacro security level.
What used to work:
// Get the program associated with workbooks, e.g. "C:\Program Files\...\Excel.exe"
SHELLAPI.FindExecutable( 'OurWorkbook.xls', ...)
// Get the version of the .exe (from it's Properties...)
WINDOWS.GetFileVersionInfo()
// Use the version number to access the registry to determine the security level
// '...\software\microsoft\Office\' + VersionNumber + '.0\Excel\Security'
(I was always amused that the security level was for years in an insecure registry entry...)
In Office 2010, .xls files are now associated with "“Microsoft Application Virtualization DDE Launcher," or sftdde.exe. The version number of this exe is obviously not the version of Excel.
My question:
Other than actually launching Excel and querying it for version and security level (using OLE CreateOLEObject('Excel.Application')), is there a cleaner, faster, or more reliable way to do this that would work with all versions starting with Excel 2003?

Use
function GetExcelPath: string;
begin
result := '';
with TRegistry.Create do
try
RootKey := HKEY_LOCAL_MACHINE;
if OpenKey('SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\excel.exe', false) then
result := ReadString('Path') + 'excel.exe';
finally
Free;
end;
end;
to get the full file name of the excel.exe file. Then use GetFileVersionInfo as usual.
As far as I know, this approach will always work.

using OLE CreateOLEObject('Excel.Application'))
you can get installed Excel versions by using the same registry place, that this function uses.
Basically you have to clone a large part of that function registry code.
You can spy on that function call by tools like Microsoft Process Monitor too see exactly how does Windows look for installed Excel - and then to do it exactly the same way.
You have to open registry at HKEY_CLASSES_ROOT\ and enumerate all the branches, whose name starts with "Excel.Application."
For example at this my workstation I only have Excel 2013 installed, and that corresponds to HKEY_CLASSES_ROOT\Excel.Application.15
But on my another workstation I have Excel 2003 and Excel 2010 installed, testing different XLSX implementations in those two, so I have two registry keys.
HKEY_CLASSES_ROOT\Excel.Application.12
HKEY_CLASSES_ROOT\Excel.Application.14
So, you have to enumerate all those branches with that name, dot, and number.
Note: the key HKEY_CLASSES_ROOT\Excel.Application\CurVer would have name of "default" Excel, but what "default" means is ambiguous when several Excels are installed. You may take that default value, if you do not care, or you may decide upon your own idea what to choose, like if you want the maximum Excel version or minimum or something.
Then when for every specific excel branch you should read the default key of its CLSID sub-branch.
Like HKEY_CLASSES_ROOT\Excel.Application.15\CLSID has nil-named key equal to
{00024500-0000-0000-C000-000000000046} - fetch that index to string variable.
Then do a second search - go into a branch named like HKEY_CLASSES_ROOT\CLSID\{00024500-0000-0000-C000-000000000046}\LocalServer ( use the fetched index )
If that branch exists - fetch the nil-named "default key" value to get something like C:\PROGRA~1\MICROS~1\Office15\EXCEL.EXE /automation
The last result is the command line. It starts with a filename (non-quoted in this example, but may be in-quotes) and is followed by optional command line.
You do not need command line, so you have to extract initial commanlind, quoted or not.
Then you have to check if such an exe file exists. If it does - you may launch it, if not - check the registry for other Excel versions.

Related

Synapse LDAPSend library requires username = Domain\User

I'm using Synapse LDAPSend.pas library to connect and authenticate in an Active Directory server. The code is pretty simple and direct, but the problem is that the username needs "DomainName\Username" or "Username#DomainName" format to bind command works correctly. Are there anyhow to make it work only with Username specified?
var
ldap: TLDAPsend;
begin
ldap:= TLDAPsend.Create;
try
ldap.TargetHost := '192.168.0.12';
ldap.UserName:= 'AD\Owner'; //here I need to specify only Owner
ldap.Password:= 'Password!';
if ldap.Login then
if ldap.Bind then
begin
//do stuff
ldap.Logout;
end;
finally
ldap.Free;
end;
end;
I was thinking maybe something like "*\Owner", or any other command from Active Directory to make it works? Or maybe a Synapse setting to make it.
If the current user is logged on to the domain there is a variety of options to retrieve the domain name. The easiest way is to read the environment variable USERDOMAIN using the function SysUtils.GetEnvironmentVariable. There are also the Windows API functions NetWkstaUserGetInfo available on Windows NT and better as well as GetUserNameEx introduced with Windows 2000 among a lot of others, which are usually harder to use.
If your version of Delphi does not contain declarations for these API functions please use the Jedi Windows API headers.

Why is COMMON_APPDATA returned as a null string on Windows XP

One of my users at a large university (with, I imagine, the aggressive security settings that university IT departments general have on their computers) is getting an empty string returned by Windows XP for CSIDL_COMMON_APPDATA or CSIDL_PERSONAL. (I'm not sure which of these is returning the empty string, because I haven't yet examined his computer to see how he's installed the software, but I'm pretty sure it's the COMMON_APPDATA...)
Has anyone encountered this or have suggestions on how to deal with this?
Here's the Delphi code I'm using to retrieve the value:
Function GetSpecialFolder( FolderID: Integer):String;
var
PIDL: PItemIDList;
Path: array[0..MAX_PATH] of Char;
begin
SHGetSpecialFolderLocation(Application.Handle, FolderID, PIDL);
SHGetPathFromIDList(PIDL, Path);
Result := Path;
end; { GetSpecialFolder }
ShowMessage(GetSpecialFolder(CSIDL_COMMON_APPDATA)); <--- This is an empty string
Edit:
Figuring out this API made me feel like I was chasing my tail - I went in circles trying to find the right call. This method and others similar to it are said to be deprecated by Microsoft (as well as by a earlier poster to this question (#TLama?) who subsequently deleted the post.) But, it seems like most of us, including me, regularly and safely ignore that status.
In my searches, I found a good answer here on SO from some time ago, including sample code for the non-deprecated way of doing this: what causes this error 'Unable to write to application file.ini'.
If you want to find out why an API call is failing you need to check the return values. That's what is missing in this code.
You need to treat each function on its own merits. Read the documentation on MSDN. In the case of SHGetSpecialFolderLocation, the return value is an HRESULT. For SHGetPathFromIDList you get back a BOOL. If that is FALSE then the call failed.
The likely culprit here is SHGetSpecialFolderLocation, the code that receives the CSIDL, but you must check for errors whenever you call Windows API functions.
Taking a look at the documentation for CSIDL we see this:
CSIDL_COMMON_APPDATA
Version 5.0. The file system directory that contains application data for all users. A typical path is C:\Documents and Settings\All
Users\Application Data. This folder is used for application data that
is not user specific. For example, an application can store a
spell-check dictionary, a database of clip art, or a log file in the
CSIDL_COMMON_APPDATA folder. This information will not roam and is
available to anyone using the computer.
If the machine has a shell version lower than 5.0, then this CSIDL value is not supported. That's the only documented failure mode for this CSIDL value. I don't think that applies to your situation, so you'll just have to see what the HRESULT status code has to say.

Unit source code does not match code execution path when breakpoint hit

I am debugging a DirectShow filter I created with the DSPACK code library using Delphi 6 Pro. When a breakpoint I set is hit in one particular unit named BaseClass.pas, and I begin tracing, the Execution Point jumps to strange places in the source code. This usually indicates that the source code being traced does not match the source code that was compiled into one of the packages being used by the Delphi application. Oddly enough it is only the BaseClass unit since I have traced other units belonging to the DSPACK code library and they do not exhibit this problem. I am not using run-time packages.
I scanned my disk and found only one copy of BaseClass.dcu with a modification date equal to the last time I built the program. I have not modified the source for that unit or any other belonging to DSPACK. Since my Filter is part of the main application this indicates that BaseClass.pas would be subject to a dual use situation since it is used to build the DSPACK component package (dpk), and is also referenced by my main application directly via the TBCSource object my Filter descends from. Note, I did try adding the unit PAS file directly to my Project but that didn't fix anything.
I also went back and re-opened each of the DSPACK package files and did a full re-build. None of this helped. Is there something else I can try to get the source synchronized with the compiled image of the BaseClass unit? Or is a different problem altogether and if so, what is it and how can I fix it?
Sometimes this happens when code is copied/pasted from web pages or other sources, and the lines don't end with CR/LF pairs (#13#10 or 0x0D0A, standard for Windows) but end in only LF (#10 or 0x0A, typically the line ending in *nix systems) or CR (#13 or 0x0D, typical with Mac OSX/iOS). The incorrect line terminators confuse the debugger - this has been an issue for the past several Delphi versions.
You can sometimes fix this by opening the source file using a text editor like Notepad, making a small meaningless change (insert and then delete a blank line, for instance), and then save the file.
I had same problem and made a similar utility. Fixed it.
Basically, just this:
procedure adjustCRLF(filename : String);
var
strList : TStringList;
begin
strList := TStringList.Create;
try
strList.LoadFromFile(filename);
strList.Text := AdjustLineBreaks(strList.Text);
strList.SaveToFile(filename);
finally
strList.Free;
end;
end;
There is another way this can happen: if the IDE erroneously opens another source file with the same name (but different, such as an earlier version) then all the debug points will be incorrect, and the debugger will even allow you to step through the incorrect file.
I've seen Delphi 7 do this once.
Make sure that when you rebuild it, that in the compiler options for your project that you have "Debug Information" turned on. In fact, most of the options under Debugging should be set in your project's Compiler options.
Also, if you haven't already, restart Delphi.

How to read the current machine NTFS settings?

Before inserting filestream data I'd like to check the following NTFS settings:
1) 8.3 naming status (this is disabled by using fsutil behavior set disable8dot3 1)
2) last access status (this is disabled by using fsutil behavior set disablelastaccess 1)
3) cluster size (this is set with format F: /FS:NTFS /V:MyFILESTREAMContainer /A:64K)
The filestream recomendation is to disable (1) and (2) and to set (3) at 64kb.
But before setting this I'd like to know the existing settings. How do I check this? Answer can be in Delphi but not necessarly.
The GetDiskFreeSpace Windows API call returns the sector_per_cluster and bytes_per_sector values. I think this function should be in Windows unit.
You can read the registry for points 1 and 2 (using xp_regread in SQL)
Number 3 is not essential but helps and has been SQL Server best practice for a decade or more. You'd have to use sp_OA% or a CLR function to read this in SQL.

Getting an error with excel ole "Add method of Workbooks class failed"

I am trying to open and refresh an excel 2003 spreadsheet via ole. However I am getting the error "Add method of Workbooks class failed" with no further information.
The code works in unit tests, and works on 3 servers but fails with the error on our web server. It is being run from a service app running under the Local System Account.
The same version of excel is installed on all servers (2003 sp3). The file(s) in question all exist and are at the expected location.
There are no macros in the spreadsheets, but there are database queries. The spreadsheets can all be opened.
The calling code is
if VarIsEmpty(XLApp) then
begin
XLApp := CreateOleObject('Excel.Application');
try
XLApp.DisplayAlerts:= wdAlertsNone;
except
...
end;
XLApp.AutomationSecurity:= msoAutomationSecurityForceDisable;
end;
fullFileName:= ExpandReportFileName( partialFilename);
if not FileExists(fullFileName) then
raise Exception.Create('File not found: ' + fullFileName);
XLAPP.Workbooks.Add(fullFileName); << fail here
Any ideas on what else I can try?
I had been getting the same error for
xls.WorkBooks.Add(xlWBATWorksheet);
I changed that line to
xls.Application.Workbooks.Add;
Now it is working. You might try to get a Workbook first then try to call its methods.
Automating Office applications in a service is not supported.
While it is possible, it's very difficult, and you'll run into many problems, such as this one. It'll also be very slow.
You should look for a Delphi component that manipulates Excel files.
Depending on what you're trying to do, you might be able to use OLE DB instead.
Workbooks.Open might be the method you're looking for
Add creates a new empty workbook. If you supply a filename, that file is used as a template for the new file - see here
Open just opens the file as you would expect - see here
I ran into this error when I happened to have the Excel spreadsheet file selected in a Windows Explorer window, not open, just selected. When I deselect the file, I do not get the error.

Resources