C++ program needs an file association - c++

I'm distributing a freeware product that reads and writes text files with a unique extension. I hoped that double-clicking such a file would automatically start the app.
While developing on Windows 7 Professional, I set up an association to open my files upon double-click, by right-clicking the file->Open With...->Choose Default Program...->Browse... followed by "Always use the selected program to open this type of file." Good. It did just what it needed to. I was going to ship my program with instructions for the users to do the same.
However, when I moved the location of the binary, I see the "Always use" is now grayed out/insensitive, so while I could browse to the new binary I couldn't make it default. Since I thought my users would have trouble with this too, I wanted to see if I could have installation or run of the program take care of the mapping.
I looked at Windows Installer for about 5 minutes before determining it was far more power and complexity than I needed (For my needs, a zip file would be sufficient except for this file mapping.)
So I took a look at having my program, at start-up, set up the mapping itself if it wasn't there already. (I know this would be very bad behavior if we were talking about a common file type such as .html or .jpg, but in this case its some .blahblah extension that surely no-one else uses for anything.)
Based on information at http://www.cplusplus.com/forum/windows/26987/ and http://msdn.microsoft.com/en-us/library/cc144148(v=vs.85).aspx I was able to have my program, at startup, open HKEY_CLASSES_ROOT\.blahblah and confirm (and change if needed) the default text to be an accurate description of the file (replacing some text that may have been created by default when I did the manual association last summer). However, when it came to creating HKEY_CLASSES_ROOT\firm.app.1\shell\open\command, my RegCreateKeyEx() wrapper that works fine to change the value of \.blahblah is now giving return code 5, apparently a lack of permission.
Upon further research it seems that the permissions model may cause all such requests to fail. Can anyone confirm or deny this? If confirm, is there a good reference I should study on the matter?
Otherwise, what are the suggestions? Should I bite the bullet and study Windows Installer? Or is there a way to get the permissions I need to edit the registry when my own software starts the first time?
Note I'm developing with Visual Studio 2008 on Windows 7 Professional, and although still an amateur Windows programmer I've been doing C++ since the '80s on Unix/Linux...

OK, I got it working and I'll share what I learned.
1) Decide a ProgID. It should be vender.app.versionnumber according to docs, but regedit shows that practically no vendor follows that rule. I did though.
2) Most of the MSFT docs on this topic talk about creating entries under HKEY_CLASSES_ROOT, but I found important info on http://msdn.microsoft.com/en-us/library/cc144148(v=vs.85).aspx:
Important considerations about file types include: The
HKEY_CLASSES_ROOT subtree is a view formed by merging
HKEY_CURRENT_USER\Software\Classes and
HKEY_LOCAL_MACHINE\Software\Classes In general, HKEY_CLASSES_ROOT is
intended to be read from but not written to. For more information, see
the HKEY_CLASSES_ROOT article.
3) to have the association show up without rebooting, you must call SHChangeNotify(). (This threw me, because even when I had the right code, the results didn't show up correctly in Explorer.)
Here's the code I ended up with. If I go through with REGEDIT and delete all mention of .moselle (my extention) and MoselleIDE (my application) then run my program once by hand, I get the click-to-open behavior, file icon becomes the same as the app, etc. Note the code uses a logging function, and it also verbosely reports which type of success it has: 1) variable already correct, 2) variable changed, 3) variable didn't exist.
void RegSet( HKEY hkeyHive, const char* pszVar, const char* pszVa
lue ) {
HKEY hkey;
char szValueCurrent[1000];
DWORD dwType;
DWORD dwSize = sizeof( szValueCurrent );
int iRC = RegGetValue( hkeyHive, pszVar, NULL, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize );
bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND;
if ( iRC != ERROR_SUCCESS && !bDidntExist )
AKS( AKSFatal, "RegGetValue( %s ): %s", pszVar, strerror( iRC ) );
if ( !bDidntExist ) {
if ( dwType != REG_SZ )
AKS( AKSFatal, "RegGetValue( %s ) found type unhandled %d", pszVar, dwType );
if ( strcmp( szValueCurrent, pszValue ) == 0 ) {
AKS( AKSTrace, "RegSet( \"%s\" \"%s\" ): already correct", pszVar, pszValue );
return;
}
}
DWORD dwDisposition;
iRC = RegCreateKeyEx( hkeyHive, pszVar, 0, 0, 0, KEY_ALL_ACCESS, NULL, &hkey, &dwDisposition );
if ( iRC != ERROR_SUCCESS )
AKS( AKSFatal, "RegCreateKeyEx( %s ): %s", pszVar, strerror( iRC ) );
iRC = RegSetValueEx( hkey, "", 0, REG_SZ, (BYTE*) pszValue, strlen( pszValue ) + 1 );
if ( iRC != ERROR_SUCCESS )
AKS( AKSFatal, "RegSetValueEx( %s ): %s", pszVar, strerror( iRC ) );
if ( bDidntExist )
AKS( AKSTrace, "RegSet( %s ): set to \"%s\"", pszVar, pszValue );
else
AKS( AKSTrace, "RegSet( %s ): changed \"%s\" to \"%s\"", pszVar, szValueCurrent, pszValue );
RegCloseKey(hkey);
}
int SetUpRegistry() {
//app doesn't have permission for this when run as normal user, but may for Admin? Anyway, not needed.
//RegSet( HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\MoselleIDE.exe", "C:\\Moselle\\bin\\MoselleIDE.exe" );
RegSet( HKEY_CURRENT_USER, "Software\\Classes\\.moselle", "Moselle.MoselleIDE.1" );
// Not needed.
RegSet( HKEY_CURRENT_USER, "Software\\Classes\\.moselle\\Content Type", "text/plain" );
RegSet( HKEY_CURRENT_USER, "Software\\Classes\\.moselle\\PerceivedType", "text" );
//Not needed, but may be be a way to have wordpad show up on the default list.
//RegSet( HKEY_CURRENT_USER, "Software\\Classes\\.moselle\\OpenWithProgIds\\Moselle.MoselleIDE.1", "" );
RegSet( HKEY_CURRENT_USER, "Software\\Classes\\Moselle.MoselleIDE.1", "Moselle IDE" );
RegSet( HKEY_CURRENT_USER, "Software\\Classes\\Moselle.MoselleIDE.1\\Shell\\Open\\Command", "C:\\Moselle\\bin\\MoselleIDE.exe %1" );
SHChangeNotify( SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL );
return 0;
}
Finally, yes, I know I should figure out an installer, but I'm a C++ expert, not a Windows configuration and terminology expert and its a lot easier for me to write the above 50 lines than it is to even start figuring out how to configure an installer. This is for an alpha release and I'll watch this topic for better ideas for future releases.

Registry changes and all actions that require higher permissions should be done at installation stage, not at application startup. You probably want to use installation software. Otherwise your software will create serious security breach.

Related

Check a user is an admin on local machine in C++ in Windows

I am trying to find a solution or winapi to check if a username is admin or not on the local machine.
The process is running under Windows Credential Provider so I believe checking the current user thread won't give me the value I want.
Please note, that I have gone through the other answers in StackOverflow
which points to MSDN which evaluates current thread's user access token.
I have the username for which I need to find the access information, whether user has admin access rights on the current machine
Any pointers or suggestions are welcome.
I'm not sure it'll do what you want, but NetUserGetInfo has a field that's at least pretty similar to what you seem to be asking for:
bool is_user_admin()
{
bool result;
DWORD rc;
wchar_t user_name[256];
USER_INFO_1 *info;
DWORD size = sizeof( user_name );
GetUserNameW( user_name, &size);
rc = NetUserGetInfo( NULL, user_name, 1, (byte **) &info );
if ( rc != NERR_Success )
return false;
result = info->usri1_priv == USER_PRIV_ADMIN;
NetApiBufferFree( info );
return result;
}
There's also LsaEnumerateAccountRights, but (if memory serves) this only shows rights/privileges assigned directly to the account in question, not those it gets via group membership and such (but it's been quite a while and my memory's a long ways from perfect).

Getting application version from within application

Is there a simple way of obtaining the application version information from the resource file at runtime?
Effectively what I'd like to do is be able to have a "Version X.Y.Z" displayed at runtime without having a separate variable somewhere that I'd have to keep in sync with my ProductVersion and FileVersion.
To clarify: yes this is a standard C++ Windows project. I am aware of the GetFileVersionInfo method but it seems silly to have to open the binary from within the version in memory just to query the version information - I'm sure I'm missing something obvious here :-)
If the OS is Windows, use the GetFileVersionInfo and VerQueryValue functions.
I don't believe that there's an easier way (than opening the file and using GetFileVersionInfo and VerQueryValue). I use the following code, in case it's helpful:
static CString GetProductVersion()
{
CString strResult;
char szModPath[ MAX_PATH ];
szModPath[ 0 ] = '\0';
GetModuleFileName( NULL, szModPath, sizeof(szModPath) );
DWORD dwHandle;
DWORD dwSize = GetFileVersionInfoSize( szModPath, &dwHandle );
if( dwSize > 0 )
{
BYTE* pbBuf = static_cast<BYTE*>( alloca( dwSize ) );
if( GetFileVersionInfo( szModPath, dwHandle, dwSize, pbBuf ) )
{
UINT uiSize;
BYTE* lpb;
if( VerQueryValue( pbBuf,
"\\VarFileInfo\\Translation",
(void**)&lpb,
&uiSize ) )
{
WORD* lpw = (WORD*)lpb;
CString strQuery;
strQuery.Format( "\\StringFileInfo\\%04x%04x\\ProductVersion", lpw[ 0 ], lpw[ 1 ] );
if( VerQueryValue( pbBuf,
const_cast<LPSTR>( (LPCSTR)strQuery ),
(void**)&lpb,
&uiSize ) && uiSize > 0 )
{
strResult = (LPCSTR)lpb;
}
}
}
}
return strResult;
}
David
The only officially supported approach is to use GetFileVersionInfo() and VerQueryValue(). However, as you have noticed, GetFileVersionInfo() requires you to pass in the filename of the executable. There is a reason for this. Although it is simple to obtain the running process's filename using GetModuleFileName(), it is not the most efficient option, especially if the executable is running from a remote share, and it is not even guaranteed to be accurate if the executable has been modified on the HDD after the process has started running.
You can access the version info of the process that is already running in memory, by calling FindResource() to locate the process's RT_VERSION resource, then use LoadResource() and LockResource() to obtain a pointer to its data. It is tempting to then pass that pointer as the pBlock parameter of VerQueryValue(), but beware because doing so can crash your code! If you access the RT_VERSION resource directly then you are better off not using VerQueryValue() at all. The format of the RT_VERSION resource is documented, so you can parse the raw data manually, it is not very difficult.
As already said there is no easy way.
You can find here a great working example (ATL free).

Why is CreateFile failing to open a file across a network share?

I wrote a small program which frequently opens small, user text files and until now haven't encountered any problems with read/write access or any sorts of conflict. The files are selected in another piece of software which I have no control over, and are passed to me as a string.
When attempting to open a file from a mapped network drive I am getting a "The system cannot find the path specified" error (GetLastError() = 3).
The call is shown below, *iNCfileName = "z:\\Validation\\Sample Files\\1_1-4 120MM.CC", where Z: is a mapped folder on our domain.
iNCfile = CreateFile( iNCfileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if ( iNCfile == INVALID_HANDLE_VALUE )
{
string msg; // lots of better ways to get this printed ... but ...
dw = GetLastError();
msg = iNCfileName;
msg += ": ";
msg += _com_error(dw).ErrorMessage();
print_error(dw , (char*)msg.c_str() );
return 102;
}
The file opens from my program if I copy it to the local hard drive. It also opens in notepad from the mapped drive.
Could this be a problem between the "Z:\whatever.txt" mapped representation and the true file name (\mydomain\Validation\S....??)?
If so, how can I convert from one to the other in a programmatic way (assume I won't know the domain/share names ahead of time)?
If it makes any difference I use VS2010 and the application executes on a Win XP machine.
Related: my follow up question
I've encountered this before. When using a path like \\DOMAIN\PATH\FILE.TXT I had to first call WNetAddConnection2().
Here is my code (of course you can exclude the NULL members):
NETRESOURCE nr = {0}; //new structure for network resource
nr.dwType = RESOURCETYPE_ANY; //generic resource (any type allowed)
nr.lpLocalName = NULL; //does not use a device
nr.lpRemoteName = "\\\\DOMAIN\\PATH\\FOLDER"; //For me, this pointed to an account's documents folder, and from there I could use a subfolder
nr.lpProvider = NULL; //no provider
DWORD ret = WNetAddConnection2 (&nr, NULL, NULL, CONNECT_TEMPORARY); //add connection
Don't forget the header and library.
I just had the same issue; trying to create a file using API CreateFileW under a mapped drive ( Z:\folder ) did not worked; howerver, after researching this subject i tried to create the file using the real path ( \\Shared_computer_name\folder\ ) immediately worked successfully.
Now I have to work a function to retrieve the real name of a mapped drive, to use it when necessary... just found WNetGetUniversalName, have to make it to work.

How to check if a process has the administrative rights

How do I properly check if a process is running with administrative rights?
I checked the IsUserAnAdim function in MSDN, but it is not recommended as it might be altered or unavailable in subsequent versions of Windows. Instead, it is recommended to use the CheckTokenMembership function.
Then I looked at the alternate example in MSDN from a description of the CheckTokenMembership function. However, there is Stefan Ozminski's comment in MSDN that mentions that this example does not work properly in Windows Vista if UAC is disabled.
Finally I tried to use Stefan Ozminski's code from MSDN, but it determines that the process has administrative rights even if I launch it under an ordinary user without the administrative rights in Windows 7.
This will tell you if you are running with elevated privileges or not. You can set the manifest to run with most possible if you want it to prompt. There are also other ways to ask windows through code for alternate credentials.
BOOL IsElevated( ) {
BOOL fRet = FALSE;
HANDLE hToken = NULL;
if( OpenProcessToken( GetCurrentProcess( ),TOKEN_QUERY,&hToken ) ) {
TOKEN_ELEVATION Elevation;
DWORD cbSize = sizeof( TOKEN_ELEVATION );
if( GetTokenInformation( hToken, TokenElevation, &Elevation, sizeof( Elevation ), &cbSize ) ) {
fRet = Elevation.TokenIsElevated;
}
}
if( hToken ) {
CloseHandle( hToken );
}
return fRet;
}
You can use LsaOpenPolicy() function.
The LsaOpenPolicy function opens a handle to the Policy object on a local or remote system.
You must run the process "As Administrator" so that the call doesn't fail with ERROR_ACCESS_DENIED.
Source: MSDN

Why does my code fail to create a directory in "C:\Program Files" under Windows 7?

I am using Windows 7 and I have to run one program in that windows but that program working in Windows XP. This is a Visual C++ program and I am using Visual Studio 2008 for this. When I am running my application, it does not throw any errors, but it does not create a directory in "c:\program files\". So can anyone help me to create directory and exe file?
This is the code I am using:
char szAppPath[MAX_PATH];
char szFileName[MAX_PATH];
DWORD dwResult;
WIN32_FIND_DATA FindFileData;
HANDLE hFind;
dwResult = ExpandEnvironmentStrings( NULL, szAppPath, MAX_PATH); // "%ProgramFiles%"
// do same for NSim directory
strcat(szAppPath,"\\NSim");
hFind = FindFirstFile(szAppPath, &FindFileData);
if (hFind == INVALID_HANDLE_VALUE)
{
//Directory Does't Exists create New
if(!CreateDirectory(szAppPath,NULL)) //Throw Error
{
MessageBox("Unable to Create N-SIM directory","NSim Installer");
return ;
}
}
else
{
//check if is directory or not
if(!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
{
MessageBox("Can't Create N-SIM directory\n Another file with same name exists","NSim Installer");
return ;
}
FindClose(hFind);
}
//***************************************N-SIM Application****************************
strcpy(szFileName, szAppPath);
HRSRC hRes;
if( bRegister == FALSE)
{
strcat(szFileName,"\\NSim.exe"); //make same name of the Client & Server in program file
hRes = FindResource(NULL, MAKEINTRESOURCE(IDR_LANSIMSERVER),RT_RCDATA);
if(flagUpgrade ==0)
{
CString trial = installationDate(); //----- Detemine Expiry Date -----
setRegistry(trial);
}
}
It's a file permissions issue, plain and simple. Programs can't just go rooting around system directories in Windows 7. That's why it works "properly" in Windows XP, but not in newer versions.
I can't tell for sure, but it looks like you're trying to write an installer. If so, why are you reinventing the wheel? There are tons of great setup utilities availableā€”Visual Studio provides a setup project that you can customize to your needs, or look into Inno Setup, my personal favorite. A Google search will turn up plenty of other options that have already solved this problem for you, and innumerable others.
If this isn't an installer, and you're just trying to store application and/or user data in the Program Files folder, I highly recommend that you look elsewhere. You weren't supposed to shove data into the app folder under earlier versions of Windows, and Windows 7 just cuts you off at the knees if you do this. Your best bet is to follow the recommendations that existed from the beginning: Investigate the user and common Application Data folders carefully. Use the SHGetKnownFolderPath function to retrieve the full path to a known folder using its KNOWNFOLDERID. A couple of suggestions:
FOLDERID_ProgramData (a shared program data directory for all users)
FOLDERID_LocalAppData (a per-user program data directory, non-roaming)
FOLDERID_RoamingAppData (a per-user program data directory, roaming)
Alternatively, you can try running the application as an Administrator. You might want to look into creating a manifest that indicates the application requires administrator-level permissions to execute.
[edit] I edited the code in the question for readability and removed the commented out code (to see the wood for the trees). It is now obvious that nothing initialises szAppPath before calling strcat(), and calling ExpandEnvironmentStrings with NULL as the first argument is undefined (and certainly useless). Calling strcat() on an unitialised string is not likely to have the desired result. This may be an artefact of not posting the real code, or even of other peoples edits (including mine).
CreateDirectory sets the system error code on error; if you want to know what went wrong, check it! Any answer you get here will be an educated guess.
if(!CreateDirectory(szAppPath,NULL)) //Throw Error
{
DWORD errorcode = GetLastError();
LPVOID lpMsgBuf;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errorcode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL );
MessageBox(NULL, (LPCTSTR)lpMsgBuf, TEXT("Error"), MB_OK);
return ;
}
If you just want to get the error code and look it up manually, then a complete directory of codes is available on MSDN, here, I would guess that ERROR_ACCESS_DENIED
(5) is most probable. A more elaborate example of error code display is given here.
windows7?
Ok, the problem is not with your program. Its with the file system permissions in Windows 7. User programs cannot create files there.
I think the problem is lack of privileges. You can debug your project to see whether the CreateDirectory function sets an error as ERROR_ACCESS_DENIED, if it does, you should make your program run with an administrator privilege. Add manifest in your project to do so.
It is intended to protect your computer against attack. Well maybe. Or Microsoft deciding to tell you what you are and not allowed to do on your own computer.
In any case you can change your UAC settings if you really have to write there in that way although that obviously exposes you to risk.
Otherwise play nice and do things the Microsoft way, using a proper installer.