How to change file association programmatically without require elevation - delphi

How to change file association programmatically when the user does not have admin/elevated rights (Win XP, Vista, 7)? Any ideas about how to work around this? Basically I would like to keep my application as lite as it is now (it doesn't need elevated rights to install and run).
At the moment I offer a GUI interface where the user can change the file association, but if the user has limited rights all it does is to show a message that it cannot do that and it explains to it how to activate the "Run this program as administrator" box then restart the program. If the user has the rights, then I just change the association.
There is a better way to do it and stay 'lite'?

In Windows (since windows 2000) you're allowed to have system-wide file association, which require elevated privileges to be set, and per user file associations.
If you want to stay lite, make a per_user file association and that's it.
Take a look on this article: Changes in File Types and File Association Features in Windows 2000 and Windows Server 2003.

You can use the ShellExecute to spawn your external utility. Make sure to include the Shield icon on your action to indicate it will require elevated permissions. It will then prompt the user and let them know it requires special permissions.
One thing you could do is add flags to your own application that indicate it will be changing permissions. And then run your application again, with the special flags.
For example if your application is
MyApplication.exe
you can spawn
MyApplication.exe /setfiles
which would only set the file associations then exit. That way you only have to ship one executable.
function RunAsAdmin(hWnd: HWND; filename: string; Parameters: string): Boolean;
var
sei: TShellExecuteInfo;
begin
ZeroMemory(#sei, SizeOf(sei));
sei.cbSize := SizeOf(TShellExecuteInfo);
sei.Wnd := hwnd;
sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
sei.lpVerb := PChar('runas');
sei.lpFile := PChar(Filename); // PAnsiChar;
if parameters <> '' then
sei.lpParameters := PChar(parameters); // PAnsiChar;
sei.nShow := SW_SHOWNORMAL; //Integer;
Result := ShellExecuteEx(#sei);
end;

My solution (waiting for better alternatives):
It looks like only the admin can change the association globally. In this light, the best way I can imagine now (but not even by far perfect) is to create a small external utility that implicitly runs with elevated rights. This tool will then change the association. Of course the users without elevated rights will still be unable to change the association.

You can find a solution at this place using the registry (OS is Windows XP) - so it may not be applicable to your request : http://volvox.wordpress.com/2006/06/02/extensions-101/ - Sorry it's in french ... Complete sources (well documented) and executable to download.

Related

Why can't I open 'HKLM\\SYSTEM\CurrentControlSet\Enum' from my Delphi 2010 program?

I am trying to write a utility to automate the process of setting up a PC to test devices using the FTDI serial>USB converter. This requires finding installed device information and writing to the above hive. I have implemented elevation of privileges and signed the program but it will not allow me to open the key. I can open the parent but not the "Enum" key or it's children.
procedure TForm4.Button1Click(Sender: TObject);
var
aReg: TRegistry;
sl: TStringList;
begin
aReg := TRegistry.Create;
try
aReg.RootKey := HKEY_LOCAL_MACHINE;
if aReg.OpenKey('\SYSTEM\CurrentControlSet\Enum', false) then // \Enum\USB\VID_0403&PID_6001', false) then
begin
sl := TStringList.Create;
try
aReg.GetKeyNames(sl);
Memo1.Lines.Assign(sl);
Memo1.Lines.Add(IntToStr(sl.Count) + ' keys found.');
finally
sl.Free;
end;
end else
Memo1.Lines.Add('Could not open key');
finally
aReg.Free;
end;
end;
"Administrators" do not have full access to the Enum key by default. Thus, elevation of privileges will not help when you want to open the key with KEY_ALL_ACCESS access rights.
TRegisty uses KEY_ALL_ACCESS by default on its operations, unless you specify a different access in its constructor or Access property.
When you are opening the key to read it contents, you can either:
use the overloaded constructor and specify KEY_READ as your desired access.
use the Access property to set the desired access to KEY_READ before opening the key.
use OpenKeyReadOnly() instead of OpenKey().
But beware this note on MSDN:
HKLM\SYSTEM\CurrentControlSet\Enum Registry Tree
The Enum tree is reserved for use by operating system components, and its layout is subject to change. Drivers and user-mode Device Installation Components must use system-supplied functions, such as IoGetDeviceProperty and SetupDiGetDeviceRegistryProperty, to extract information from this tree. Drivers and Windows applications must not access the Enum tree directly. You can view the Enum tree directly by using Registry Editor when you debug drivers.
You should consider using the alternative API mentioned in the documentation.

run program with administrator access to write to registery

i tried to use this tutorial to work:
https://stackoverflow.com/a/14710803/1149172
first create a file with name uac.manifest with provided content
then create uac.rc width content of 1 24 "uac.manifest"
then copied the files to program folder (in delphi program sources folder) and changed the project file like this:
program Project4;
{.$R 'uac.res' 'uac.rc'} // UAC only
uses
Vcl.Forms,
Unit6 in 'Unit6.pas' {Form6};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm6, Form6);
Application.Run;
end.
at last i put my registery code at the form
procedure AddEntryToRegistry;
var key: string;
Reg: TRegIniFile;
begin
key := 'Software\Microsoft\Windows\CurrentVersion\Run';
Reg := TRegIniFile.Create;
try
Reg.RootKey:=HKEY_LOCAL_MACHINE;
Reg.CreateKey(Key);
if Reg.OpenKey(Key,False) then Reg.WriteString(key, 'MyApp', 'c:MyApp.exe');
finally
Reg.Free;
end;
end;
every thing seems ok and i dont got any runtime rror
but after clicking the button nothing happened (dont shoe any error and dont add the key to registery)!!!
where is wrong with my works!?
my delphi is xe5 and working on win 8ul
Firstly, I'm going to take it as read that your program is running elevated. If that's not happening then there's no need to look at any code. You did not say otherwise, so let us proceed under the assumption that you are succeeding to elevate.
You are suffering from the registry redirector. Your 32 bit process is running on a 64 bit machine. And so HKLM\Software is redirected to the 32 bit view, stored at HKLM\Software\Wow6432Node.
You can, if you need, use the KEY_WOW64_64KEY flag to access the 64 bit view. Combine this with the flags in the registry object's Access property.
However, the system reads keys from both 32 and 64 bit views of the registry when enumerating the startup programs so you do not need to do this. For the sake of simplicity and predictability I would leave your 32 bit program writing to the 32 bit view.
Your call to CreateKey should be removed. The system creates that key and you can safely assume it exists. And you should not use TRegIniFile. Use TRegistry instead.
Your code should look like this:
procedure AddEntryToRegistry;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create(KEY_ALL_ACCESS);
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', False) then
Reg.WriteString('MyApp', 'C:\MyApp.exe');
finally
Reg.Free;
end;
end;
Should you feel that you need to write to the 64 bit view then it is done like this:
procedure AddEntryToRegistry;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
Reg.Access := KEY_ALL_ACCESS or KEY_WOW64_64KEY;
if Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', False) then
Reg.WriteString('MyApp', 'C:\MyApp.exe');
finally
Reg.Free;
end;
end;
Judging by your comments, it looks like you might be failing to elevate. The lack of a UAC dialog when your program starts is the tell-tale sign that this is happening. Once your program starts without a UAC dialog, there's no point continuing. You will not write to HKLM without elevation.
Regarding your manifest, you can link only one. So if you want to specify a manifest other than the Enable runtime themes manifest that the IDE can provide, you need to do it all yourself.
In the project options specify that you want to use a custom manifest. That's under Project | Options | Application | Runtime themes. Set the drop down to Use custom manifest. And then supply the file name of your manifest. You'll want to add in the comctl32 v6 part to make sure that you get runtime themes. But don't worry about that now. Just concentrate on getting elevation sorted, and the registry code working.
You are also silently ignoring any errors which does make things a little harder to debug. If it so happens that you are not elevating, then running the code is rather pointless. You know it must fail. But you could at least make it easier to diagnose the problem by throwing an error if OpenKey fails.
procedure AddEntryToRegistry;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create(KEY_ALL_ACCESS);
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
if not Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', False) then
raise EMyExceptionClass.Create('Could not open registry key');
Reg.WriteString('MyApp', 'C:\MyApp.exe');
finally
Reg.Free;
end;
end;
One final point to make is that writing to this registry key is an admin task. You should require elevation once only, not every time your application starts. If you are planning to require elevation for your application just for this purpose, then you must re-design. This admin task should be performed outside your main application. The most natural place is inside your install program which users will accept requiring elevation.
You are saving your app path to the Run key of the HKEY_LOCAL_MACHINE hive. You should be using the HKEY_CURRENT_USER hive instead, then you will not need to use UAC elevation anymore (unless your app is doing other things that require admin rights).

LogonUser + CreateProcessAsUser at Service = error 1314

I have a windows service created with Delphi 7, with StartType = stSystem.
Now I need to launch an application to make some things for me.
This application has a MainForm and other GDI resources.
The parameter passed to the application assigns values for some controls (like edits and memos) and then click at a button....
I'm trying this:
var
token: cardinal;
si: TStartupInfo;
pi: TProcessInformation;
begin
if not LogonUser('admintest', '', 'secret123', LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, token) then
RaiseLastOSError;
try
if not ImpersonateLoggedOnUser(token) then
RaiseLastOSError;
fillchar(si, sizeof(si), 0);
si.cb := sizeof(si);
si.lpDesktop := PChar('winsta0\default');
if not CreateProcessAsUser(token, nil, '"c:\...\myapp.exe" -doCrazyThings', nil, nil, false, NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE, nil, nil, si, pi) then
RaiseLastOSError;
CloseHandle(pi.hThread);
waitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
finally
CLoseHandle(token);
end;
end;
When I run my service executable as a normal application (-noservice), it starts as a Forms.Application and creates a MainForm with a button "Start".
*The button runs the same code that service run, but it doesn't works and it's rasing the eror code 1314 at createprocessasuser.*
Why? What is the diference between SYSTEM service and a normal application launched by a administrator?
My environment is a Windows 7 Pro x64
What am I doing wrong?
How can I solve this?
Can someone post an example?
Error 1314 is ERROR_PRIVILEGE_NOT_HELD, which means your calling thread is missing a required privilege to run CreateProcessAsUser(). You don't need to, nor should you be, impersonating the user token in order to launch a new process in the user's desktop. You should be letting the thread use the service's credentials, not the user's credentials, when calling CreateProcessAsUser(). It will make sure the new process is run inside the user's account and desktop for you, so get rid of the call to ImpersonateLoggedOnUser() and see if CreateProcessAsUser() starts working.
Update: read the documentation:
Typically, the process that calls the CreateProcessAsUser function must have the SE_INCREASE_QUOTA_NAME privilege and may require the SE_ASSIGNPRIMARYTOKEN_NAME privilege if the token is not assignable. If this function fails with ERROR_PRIVILEGE_NOT_HELD (1314), use the CreateProcessWithLogonW function instead. CreateProcessWithLogonW requires no special privileges, but the specified user account must be allowed to log on interactively. Generally, it is best to use CreateProcessWithLogonW to create a process with alternate credentials.
On Vista and later, windows services run in session 0. Interactive users exist in session 1 and up. This means that windows services cannot show user interface and indeed cannot easily start processes in the interactive session.
Now, there are ways to launch interactive processes from a service. If you are dead set on launching an interactive process from your service, then that article tells you all your need to know. But such techniques are very tricky and absolutely not to be recommended. The recommendation is that you find a different way to communicate between your service and the interactive desktop.
The normal approach is to run a standard desktop app, perhaps started using HKLM\Software\Microsoft\Windows\CurrentVersion\Run. And use some form of IPC to communicate between the desktop app and the service.
You should be creating the service to do the background work, and the GUI application should only call the service. IE you always have a service running.
Consider using the DataSnap stuff for the back end. An MVC approach is not pure in Delphi like it is in some other languages. The controller goes wherever it is convenient. Datasets are mostly a compromise, and the only really fast way to do data is with DBexpress and a whack of components on the client to keep it all happy. But it works and it is worth learning.
Services cant have gui controls. No TForm descendents allowed. TService only. New project under Delphi Projects / Service Application. You get a project with a unit / form that is pretty much the same as a data module. ie, no visual controls allowed. Reasons are pretty obvious. You need to learn records, object design etc. to use services. Datasnap is your best bet for that. It does most of the hard work for you.
Dr. Bob has a pretty good book on it for XE2/3. If you are new to object oriented design you need to learn that thoroughly first.
Here is the code i use to do this kind of thing
procedure CreateNewProcess;
var
CmdLine: AnsiString;
ErrorCode: Cardinal;
ConnSessID: Cardinal;
Token: Cardinal;
App: AnsiString;
FProcessInfo: _PROCESS_INFORMATION;
FStartupInfo: _STARTUPINFOA;
begin
ZeroMemory(#FStartupInfo, SizeOf(FStartupInfo));
FStartupInfo.cb := SizeOf(FStartupInfo);
FStartupInfo.lpDesktop := nil;
ConnSessID := WTSGetActiveConsoleSessionId;
if WTSQueryUserToken(ConnSessID, Token) then
begin
if CreateProcessAsUser(Token, PAnsiChar(App), PAnsiChar(CmdLine),
nil, nil, false, 0, nil, nil, FStartupInfo, FProcessInfo) = False
then
begin
ErrorCode := GetLastError;
try
RaiseLastOSError(ErrorCode);
except on E: Exception do
EventLog.LogError(e.ClassName +': '+ e.Message);
end;
end;
end;
end;
Hope this helps
if you want the service to wait for the new process to terminate before continuing add this
if CreateProcessAsUser(Token, PAnsiChar(App), PAnsiChar(CmdLine),
nil, nil, false, 0, nil, nil, FStartupInfo, FProcessInfo) = False
then
begin
ErrorCode := GetLastError;
try
RaiseLastOSError(ErrorCode);
except on E: Exception do
EventLog.LogError(e.ClassName +': '+ e.Message);
end;
end
else
WaitForSingleObject(FProcessInfo.hProcess, INFINITE);
although and infinite wait is probably not advisable.

How to not trigger exception when trying to write to HKLM on standard user?

I am attempting to write a value to the HKLM registry using TRegistry component in Delphi.
Since I am running on Windows 2000 as a standard user (or XP as a standard user, or Windows Vista as a standard user, or Windows 7 with a standard user), I fully expect that I will not be able to write to the HKEY_LOCAL_MACHINE portion of the registry:
reg := TRegistry.Create(KEY_WRITE);
try
reg.Access := KEY_WRITE; //sure, set it again, why not
reg.RootKey := HKEY_LOCAL_MACHINE;
if not reg.OpenKey('\Software\Microsoft\SQMClient', True) then
Exit;
reg.WriteString('MachineId', s);
finally
reg.Free;
end;
Unfortunately, the WriteString throws an ERegistryException:
Failed to set data for 'MachineId`
This is fully expected, which is why I'm trying to avoid the exception. I do not see any CanWriteString or TryWriteString in TRegistry.
How can I not trigger an exception when trying to write to HKLM?
Self-evident notes:
if the user actually is an administrator then the write should be able to succeed
wrapping the call to WriteString in a try-except:
reg := TRegistry.Create(KEY_WRITE);
try
reg.RootKey := HKEY_LOCAL_MACHINE;
if not reg.OpenKey('\Software\Microsoft\SQMClient', True) then
Exit;
try
reg.WriteString('MachineId', s);
except
on E:ERegistryException do
{nothing};
end;
finally
reg.Free;
end;
doesn't prevent the exception from being thrown in the first place.
Update: From RTL source:
KEY_WRITE = (STANDARD_RIGHTS_WRITE or
KEY_SET_VALUE or
KEY_CREATE_SUB_KEY) and not
SYNCHRONIZE;
from MSDN:
KEY_WRITE (0x20006)
Combines the STANDARD_RIGHTS_WRITE, KEY_SET_VALUE, and KEY_CREATE_SUB_KEY access rights.
You can't get TRegistry to behave the way you want. There are no TryXXX methods and there are not parameters that disable exceptions. You can be sure that this is so because the TRegistry methods do not provide any error or status codes.
You will have to write your own wrappers around the Win32 registry API.
As an aside, I agree with your opinion, expressed in the comments, that TRegistry is lacking in functionality here. We expect registry operations to fail, and so we should not have to catch exceptions to deal with that.
Use KEY_SET_VALUE instead of KEY_WRITE when opening the key, as KEY_WRITE includes other permissions in it. The fact that OpenKey() succeeds means that your standard user account has some of those permissions, so the key is allowed to be opened, but the key does not know exactly what you are going to do with it until you do it, so it cannot actually validate all of the permissions up front in case you dont use them. If you use just KEY_SET_VALUE instead (which is all you really need in your example), OpenKey() has a better chance of failing right away if your user account does not have any permissions to write data into the key. When it comes to accessing securable resources, always request just the minimum permissions you actually need.

Delphi ini file vista/xp/win7

Update: I've added the following code:
function TSettingsForm.AppDataPath: string;
//CSIDL_APPDATA Individual user Data
//CSIDL_COMMON_APPDATA Common to Computer Data
// works so long as people have at least IE 4. (and Win95 or better)
var
r: Bool;
path: array[0..Max_Path] of Char;
begin
r := ShGetSpecialFolderPath(0, path, CSIDL_APPDATA, False) ;
if r then result := path
else result := '';
end;
And I've changed the setinifilename function (See below). It will not create the folder structure.
--End update--
I'm behind the times, on what to and not to do. This is how I am currently saving the settings for my software. I just tested it on Vista not logged in as an administrator, and it gives me an error message cannot write ini file. So I'm guessing I'm supposed to write the data to a data folder? I've never used vista/win7 before, and want this software to be windows 2K+ compatible. What should I do to save the settings. I also really didn't want to mess with the registry, because every little bit you add to it, slows down the computer just that much more... (or so It seems)
Thanks for any input.
procedure TSettingsForm.setinifilename;
var filename:string;
Path:string;
begin
filename:='key.ini';
path:=AppDataPath+'\MyCompanyName\ProductName\';
if NOT DirectoryExists(path) then
CreateDir(path);
inifilename:= path+filename;
end;
procedure TSettingsForm.SaveSettings;
var
appINI: TIniFile;
begin
appINI := TIniFile.Create(inifilename) ;
try
low:= Trunc (edt_low.value);
high:=Trunc (edt_high.value);
appINI.WriteInteger('SPEED','LOW',low);
appINI.WriteInteger('SPEED','HIGH',high);
appINI.WriteString('PROXY','SERVER',edtProxyServer.Text);
appINI.WriteString('PROXY','PORT',edtProxyPort.Text);
appINI.WriteString('PROXY','USERNAME',edtProxyUserName.Text);
appINI.WriteString('PROXY','PASSWORD',edtProxyPass.Text);
// status.text:='Saved Data';
finally
appIni.Free;
end;
end;
procedure TSettingsForm.GetSettings;
Var
appINI : TIniFile;
begin
appINI := TIniFile.Create(inifilename) ;
try
//if no last user return an empty string
edt_low.value:= appINI.ReadInteger('SPEED','LOW',0);
edt_high.value:= appINI.ReadInteger('SPEED','HIGH',0);
low:= Trunc (edt_low.Value);
high := Trunc (edt_high.Value);
edtProxyServer.Text:=appINI.ReadString('PROXY','SERVER','');
edtProxyPort.Text:=appINI.ReadString('PROXY','PORT','0');
edtProxyUserName.Text:=appINI.ReadString('PROXY','USERNAME','');
edtProxyPass.Text:= appINI.ReadString('PROXY','PASSWORD','');
finally
appINI.Free;
end;
end;
In Vista, your program is NOT allowed to write to the program files directory where your program is located.
You now have to save your ini files in the AppData directory.
A description of how to do this in delphi is at:
http://www.theabsolute.net/sware/delphivista.html#datafolder
And to be Vista/Windows 7 compatible, the rest of that web page will be a good guideline.
For your update, you cannot CreateDir more than 1 level deep at once. Use the ForceDirectories function instead:
path:=AppDataPath+'\MyCompanyName\ProductName\';
if NOT DirectoryExists(path) then
ForceDirectories(path);
p.s. Don't be afraid to write program settings to the Registry. That's what the registry is for. In fact, it properly handles settings for different users for you when different users are logged in. The Registry works in the same way in 98/Vista/7. Whereas ini files have actually been depreciated, and are no longer used by Windows.
You say you don't want to mess with the registry because "every little bit you add to it, slows down the computer just that much more". Actually that is NOT true. The registry is simply a database. And if it is 10 MB or 100 MB, the difference in time it takes to access is imperceptable.
It's all those companies selling Registry Cleaner programs that are trying to keep this fairy tale going. Using their cleaners can do you more harm than good. All they need to do is wipe out one or two important entries and you can be in deep doo-doo. Please read this article about Registry Cleaners, and especially the "Marginal performance benefit" section which explains correctly that the problems Windows 98 and earlier had with the Registry have been mostly fixed.
If your program adds more than 2 or 3 KB to the Registry, that will be a lot, and it is an insignificant amount. Use the registry. Do it right.
You should use the ApplicationData directory for your app data, In Delphi you can find this folder programatically using the shell api function SHGetSpecialFolderLocation
Embarcadero have a FAQ page on this, here.
As already mentioned - dont save anything in the app folder.
You should split your configuration settings into two parts :
One part containing the settings that must work regardlees of the user - that part should be stored in COMMON_APPDATA.
A Second part containing the individual users settings (users personal choice of font etc) - that part should be stored in APPDATA
As for the CreateDir, it is true that you cannot create more than one level at a time - however, Delphi has the ForceDirectories function that can do exactly that.
e.g. ForceDirectories('C:\MyFolder\SubFolder\SubSubFolder');

Resources