Solved

Saving data from limited access accounts

Posted on 2007-04-06
13
278 Views
Last Modified: 2008-01-09
I have written (and sell) about 27 programs in visual basic. Some of these programs need to save data. I am finding that these programs do not work if they are run from a user account with limited access (after being installed using an account with administrative access, of course). I have tried saving the files in the application folder, which is usually something like C:\MyApp (as opposed to the Windows folder). I have tried naming the file with a txt extension (instead of dat, which seems to cause problems). I have tried everything I can think of. Can someone please tell me exactly how I can have my VB program save data while running under a user account with limited privileges? Note that the files are sometimes somewhat confidential, so they should not be put someplace too common, like the My Documents folder.
0
Comment
Question by:Leithauser
  • 7
  • 4
  • 2
13 Comments
 
LVL 4

Expert Comment

by:quiklearner
ID: 18868351
If it is alot of repetetive data, consider an actual service-based database (as opposed to a file based one).  If the data is related to single or one off settings or not a bunch of data, consider using getsetting() and savesetting() which write to the registry..  Obviously, because limitations in user settings can be non-existent to extreme and everywhere between, you'll need to test to verify exactly what works with the priveledges these users are granted...
0
 
LVL 2

Accepted Solution

by:
IanWelch earned 250 total points
ID: 18868607
The files should be saved in the application data folder (for either all users or current user).

A normal user cannot write to the common app data files. You can make your application require 'elevation' though (i.e. have your app run as administrator). But that's a whole different subject.

These folders are located in different places depending on version of windows. So the correct way to go about it is to use the windows API to find where these folder are.

Below is code you can use to get these paths and create folders within for your company/application. Just paste the code in a module, and change the constants at the very top.

The 'Default' parameters are used when the special folder doesn't exist (eg on win98) and would be the path you've been using in the past (app.path?).






Option Explicit

'constants used for building path in filesystem
Private Const COMPANY_NAME As String = "ABC Ltd"
Private Const APP_NAME     As String = "My App"

'required for get specialfolder api
Private Type SHITEMID
    cb   As Long
    abID As Byte
End Type

Private Type ITEMIDLIST
    mkid As SHITEMID
End Type

'for getting special folder locations
Private Declare Function SHGetPathFromIDList _
                Lib "shell32.dll" _
                Alias "SHGetPathFromIDListA" (ByVal pidl As Long, _
                                              ByVal pszPath As String) As Long
Private Declare Function SHGetSpecialFolderLocation _
                Lib "shell32.dll" (ByVal HWndOwner As Long, _
                                   ByVal nFolder As Long, _
                                   pidl As ITEMIDLIST) As Long
Private Declare Function lstrlen _
                Lib "kernel32" _
                Alias "lstrlenA" (ByVal lpString As Any) As Long

'special folder ids
Private Const CSIDL_ADMINTOOLS              As Long = &H30
Private Const CSIDL_ALTSTARTUP              As Long = &H1D
Private Const CSIDL_APPDATA                 As Long = &H1A
Private Const CSIDL_BITBUCKET               As Long = &HA
Private Const CSIDL_COMMON_ADMINTOOLS       As Long = &H2F
Private Const CSIDL_COMMON_ALTSTARTUP       As Long = &H1E
Private Const CSIDL_COMMON_APPDATA          As Long = &H23
Private Const CSIDL_COMMON_DESKTOPDIRECTORY As Long = &H19
Private Const CSIDL_COMMON_DOCUMENTS        As Long = &H2E
Private Const CSIDL_COMMON_FAVORITES        As Long = &H1F
Private Const CSIDL_COMMON_PROGRAMS         As Long = &H17
Private Const CSIDL_COMMON_STARTMENU        As Long = &H16
Private Const CSIDL_COMMON_STARTUP          As Long = &H18
Private Const CSIDL_COMMON_TEMPLATES        As Long = &H2D
Private Const CSIDL_CONNECTIONS             As Long = &H31
Private Const CSIDL_CONTROLS                As Long = &H3
Private Const CSIDL_COOKIES                 As Long = &H21
Private Const CSIDL_DESKTOP                 As Long = &H0
Private Const CSIDL_DESKTOPDIRECTORY        As Long = &H10
Private Const CSIDL_DRIVES                  As Long = &H11
Private Const CSIDL_FAVORITES               As Long = &H6
Private Const CSIDL_FLAG_CREATE             As Long = &H8000
Private Const CSIDL_FONTS                   As Long = &H14
Private Const CSIDL_HISTORY                 As Long = &H22
Private Const CSIDL_INTERNET                As Long = &H1
Private Const CSIDL_INTERNET_CACHE          As Long = &H20
Private Const CSIDL_LOCAL_APPDATA           As Long = &H1C
Private Const CSIDL_MYPICTURES              As Long = &H27
Private Const CSIDL_NETHOOD                 As Long = &H13
Private Const CSIDL_NETWORK                 As Long = &H12
Private Const CSIDL_PERSONAL                As Long = &H5
Private Const CSIDL_PRINTERS                As Long = &H4
Private Const CSIDL_PRINTHOOD               As Long = &H1B
Private Const CSIDL_PROFILE                 As Long = &H28
Private Const CSIDL_PROGRAM_FILES           As Long = &H26
Private Const CSIDL_PROGRAM_FILES_COMMON    As Long = &H2B
Private Const CSIDL_PROGRAM_FILES_COMMONX86 As Long = &H2C
Private Const CSIDL_PROGRAM_FILESX86        As Long = &H2A
Private Const CSIDL_PROGRAMS                As Long = &H2
Private Const CSIDL_RECENT                  As Long = &H8
Private Const CSIDL_SENDTO                  As Long = &H9
Private Const CSIDL_STARTMENU               As Long = &HB
Private Const CSIDL_STARTUP                 As Long = &H7
Private Const CSIDL_SYSTEM                  As Long = &H25
Private Const CSIDL_SYSTEMX86               As Long = &H29
Private Const CSIDL_TEMPLATES               As Long = &H15
Private Const CSIDL_WINDOWS                 As Long = &H24

'for checking directory exists
Private Const MAX_PATH = 260
Private Const INVALID_HANDLE_VALUE = -1
Private Const FILE_ATTRIBUTE_DIRECTORY = &H10

Private Type FILETIME
    dwLowDateTime  As Long
    dwHighDateTime As Long
End Type

Private Type WIN32_FIND_DATA
    dwFileAttributes As Long
    ftCreationTime   As FILETIME
    ftLastAccessTime As FILETIME
    ftLastWriteTime  As FILETIME
    nFileSizeHigh    As Long
    nFileSizeLow     As Long
    dwReserved0      As Long
    dwReserved1      As Long
    cFileName        As String * MAX_PATH
    cAlternate       As String * 14
End Type

Private Declare Function FindFirstFile _
                Lib "kernel32" _
                Alias "FindFirstFileA" (ByVal lpFileName As String, _
                                        lpFindFileData As WIN32_FIND_DATA) As Long

Private Declare Function FindClose Lib "kernel32" (ByVal hFindFile As Long) As Long

Public Function GetCommonAppDataPath(Default As String) As String

    On Error GoTo ErrHandler

    Dim sPath As String

    On Error Resume Next

    'try and retrieve path, ignore errors (generated on OS < xp)
    sPath = GetSpecialFolderPath(CSIDL_COMMON_APPDATA)

    'if not got path then quit returning default value
    On Error GoTo ErrHandler

    If sPath = "" Then
        GetCommonAppDataPath = QualifyPath(Default)

        Exit Function

    End If

    'append company/product names to path and make sure they exist
    sPath = QualifyPath(sPath) & COMPANY_NAME & "\"

    If Not DirExists(sPath) Then
        MkDir sPath
    End If

    sPath = sPath & APP_NAME & "\"

    If Not DirExists(sPath) Then
        MkDir sPath
    End If

    GetCommonAppDataPath = sPath

    Exit Function

ErrHandler:
    'just default out
    GetCommonAppDataPath = QualifyPath(Default)
End Function

Public Function GetUserAppDataPath(Default As String) As String

    On Error GoTo ErrHandler

    Dim sPath As String

    On Error Resume Next

    'try and retrieve path, ignore errors (generated on OS < xp)
    sPath = GetSpecialFolderPath(CSIDL_APPDATA)

    'if not got path then quit returning default value
    On Error GoTo ErrHandler

    If sPath = "" Then
        GetUserAppDataPath = QualifyPath(Default)

        Exit Function

    End If

    'append company/product names to path and make sure they exist
    sPath = QualifyPath(sPath) & COMPANY_NAME & "\"

    If Not DirExists(sPath) Then
        MkDir sPath
    End If

    sPath = sPath & APP_NAME & "\"

    If Not DirExists(sPath) Then
        MkDir sPath
    End If

    GetUserAppDataPath = sPath

    Exit Function

ErrHandler:
    'just default out
    GetUserAppDataPath = QualifyPath(Default)
End Function

Private Function DirExists(ByVal DirPath As String) As Boolean
    Dim hFile As Long
    Dim WFD   As WIN32_FIND_DATA

    DirPath = UnqualifyPath(DirPath)
    DirExists = False
    hFile = FindFirstFile(DirPath, WFD)

    If hFile <> INVALID_HANDLE_VALUE Then
        If WFD.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY Then
            DirExists = True
        End If
    End If

    FindClose hFile
End Function

Private Function GetSpecialFolderPath(ByVal CSIDL_VALUE As Long) As String

    On Error Resume Next

    Dim IDL   As ITEMIDLIST
    Dim lRet  As Long
    Dim sPath As String
    Dim sTemp As String

    ' Get the location into our ITEMIDLIST
    lRet = SHGetSpecialFolderLocation(0, CSIDL_VALUE, IDL)

    If lRet = 0 Then
        ' Build a buffer string to handle the returned value
        sPath = Space$(255)
        lRet = SHGetPathFromIDList(ByVal IDL.mkid.cb, ByVal sPath)

        If lRet <> 0 Then
            ' Make sure we append a '\' to the path
            sTemp = Left$(sPath, lstrlen(sPath))
            GetSpecialFolderPath = QualifyPath(sTemp)
        End If
    End If

End Function

Private Function QualifyPath(ByVal sPath As String) As String

    If Not Right$(sPath, 1) = "\" Then
        sPath = sPath & "\"
    End If

    QualifyPath = sPath
End Function

Private Function UnqualifyPath(ByVal sPath As String) As String
    Do While Right$(sPath, 1) = "\"
        sPath = Left$(sPath, Len(sPath) - 1)
    Loop
    UnqualifyPath = sPath
End Function
0
 
LVL 5

Author Comment

by:Leithauser
ID: 18869563
IanWelch:

<<The files should be saved in the application data folder (for either all users or current user).>>

Are you saying that Windows actually has special folders designed expressly to save data from programs running in a limited user account?

<<A normal user cannot write to the common app data files.>>

Assuming that the answer to the question above is Yes, does the above comment mean that these folders cannot be viewed by normal programs like Windows Explorer?

<< You can make your application require 'elevation' though (i.e., have your app run as administrator). But that's a whole different subject.>>

Are you saying here that there are some kind of settings I can build into my program that would make it run with administrative privileges, even when it is running under a limited account? If so, I would be interested to hear more. That could be an alternate solution to the problem.

<<These folders are located in different places depending on version of windows. So the correct way to go about it is to use the windows API to find where these folder are. Below is code you can use to get these paths and create folders within for your company/application. Just paste the code in a module, and change the constants at the very top.>>

If I understand you correctly, the code you listed would allow me to find the special folders that I can write to even under a limited user account, then create special folders within these folders where my programs can store their data. That might be a workable solution. However, I need a bit of clarification:

Your code lists a lot of different subs, but does not clearly explain how to use them (like which to call). Could you give me a few lines of code to show exactly how I would use the functions you listed? Something like a few lines in a Form_Load Sub that would set a global string variable named DataFolder to the folder where my program can store its data.

The functions are all defined as Private. I assume that if I put all your code in a module specifically designed to set the data folder, I will have to change one or more of them to Public. Or maybe just put a public sub containing the code I requested in the previous paragraph in the same module and have it call all the other functions, then return the result as a string.

There is a reference to a folder for my company and my application. Is this really necessary? Couldn't I just create a folder for my application?

There is a reference to errors in OS's lower than XP ("'try and retrieve path, ignore errors (generated on OS < XP)"). Does this mean the code does not work on anything other than XP? Which OS's does it work on?

From your statement <<The 'Default' parameters are used when the special folder doesn't exist (e.g., on win98) and would be the path you've been using in the past (app.path?).>>, I take it the code simply does not work on OS's that lack such folders. I guess I just have to accept that, but it will not be a problem if the code works on all (or even most) new systems.

If I am understanding you correctly and you can provide the code to help me use the functions you have given me, it sounds like this will be an acceptable (although complicated) solution.

David Leithauser

0
 
LVL 5

Author Comment

by:Leithauser
ID: 18870138
IanWelch:

Your solution does not seem to work. I used your code and figured out how to get the folder. I had the program save the data in that folder. It works fine from an administrative account, but still does not save the data from a limited account.

The program contains a Sub that opens the data file for output, saves all the data, then closes it.
 I ran the program in the environment mode of VB. I traced it through the procees perfectly. Then I ran it from the limited user account. It was bizzare. As soon as the program tried to execute the Open statement, the program flow jumped right out of the Sub, with no error generated or anything. It did not even try to execute the code after the Open statement in the code. I hit F8 to step through the Sub. It got to the Open statement, and when I hit F8 again, it was out of the Sub.

I could really use help on this.
0
 
LVL 2

Expert Comment

by:IanWelch
ID: 18871633
Ok. Sorry for the delay - different time zones.

On a limited user account you cannot write to the folder returned by GetCommonAppDataPath. You need to use GetUserAppDataPath or have your app request elevation.

To get elevation take a look at
http://www.experts-exchange.com/Programming/Languages/Visual_Basic/Q_22156349.html

I've tested the code I gave you with the following routine (on a form with 1 command button) and all worked fine for me.




Option Explicit

Private Sub Command1_Click()
    Dim sPath As String

    sPath = mAppDataPaths.GetUserAppDataPath(App.Path)
    Debug.Print sPath
    pSaveFileString "This is test output", sPath & "myappdata.txt"
End Sub

Private Sub pSaveFileString(ByVal sStr As String, ByVal sFile As String)
    On Error GoTo ErrHandler
    Dim fNum As Integer
    Dim bOpen As Boolean

    fNum = FreeFile
    Open sFile For Output As #fNum
        bOpen = True
        Print #fNum, sStr
    Close #fNum
    Exit Sub
ErrHandler:

    If bOpen Then
        Close #fNum
    End If
End Sub
0
 
LVL 4

Expert Comment

by:quiklearner
ID: 18871636
I will ask again, Is it repetative data?  For example, would storing it on a spreadsheet of some kind make sense?
0
Highfive Gives IT Their Time Back

Highfive is so simple that setting up every meeting room takes just minutes and every employee will be able to start or join a call from any room with ease. Never be called into a meeting just to get it started again. This is how video conferencing should work!

 
LVL 5

Author Comment

by:Leithauser
ID: 18873856
<<Quicklearner:
I will ask again, Is it repetative data?  For example, would storing it on a spreadsheet of some kind make sense?>>

Most of the data is repetitive, but I am not sure I see any relevance to that. The quantity of data does change radically, even though the format of all data items is usually the same, so I cannot save it in a set number of bytes. Also, I cannot seem to save any kind of data. In addition, I want a solution general enough for me to use it in ALL my programs, although at the moment I am most concerned about one in particular. This problem of people purchasing my programs and then having trouble running them on limited user accounts has been bugging me for years.
0
 
LVL 4

Expert Comment

by:quiklearner
ID: 18874283
Repetative data could be stored in a database.  Data sould be quicker to retieve from, and would increase the amount of data that would be reasonalbe to deal with.  
MS even offers a service-based database, SQL Server Express, that is fairly decent, and licensing to re-distrubute it is free.  Should be pretty easy to add to your install.  I have yet to find privledges so low it wouldn't work although I suppose it's possible.  It is a porgram that runs with is own permissions that can write files no matter who's currently logged in.  You communicate to the database from your program.  I hate to give MS so much credit in one comment but ADO (Microsoft ActiveX Data Objects) is about the easiest I have found to handle the communications to and from a database in VB.  Obviously it would be more of a change than simply finding a path, but if you were looking at larger markets for your stuff, a databse would definitely make it more scalable.
0
 
LVL 5

Author Comment

by:Leithauser
ID: 18876849
IamWelch:

<<On a limited user account you cannot write to the folder returned by GetCommonAppDataPath. You need to use GetUserAppDataPath or have your app request elevation.>>

I assume that this path is different for each user. This could be a problem, since my programs generally would want to share data for all users, although I can see some situations where it might be handy to have each user have different data. It just does not fit well in most of my programs. What I really had hoped for was a simple solution, like a path where all users, even those with limited access, could write data.

I will look into this further. If absolutely necessary, I might be able to use the GetUserAppDataPath in some situations, but it would require a lot of extra work on the part of the administrative user because they would need to sign on as each user to access the data from that user, and would not be approprite at all in cases where the data had to be in common for all users.

Can you tell me more about the process of have my app request elevation? The information in the link you provided was a bit sketchy.

Incidentally, what is this mAppDataPaths.GetUserAppDataPath(App.Path)? I do not understand the mAppDataPaths. It seems to me that a simple GetUserAppDataPath(App.Path) should return the user app path.
0
 
LVL 5

Author Comment

by:Leithauser
ID: 18876897
quiklearner:

<<I have yet to find privledges so low it wouldn't work although I suppose it's possible.>>

It seems very unlikely to me that a user account with limited access would be able to access such a file. In any case, I strongly prefer not to encomber my programs with a third party database.
0
 
LVL 4

Expert Comment

by:quiklearner
ID: 18877221
It wont access a "file" it will access a "service".  This service runs under the Local System Account and is basically a form of user elevation but without you have to code it.  Believe me, I can completely understand that you don't want to "encomber my programs with a third party database", I just know it would solve the problem you have bneen facing for years, and in my professional opinion, could arguably be your best way forward.  Good Luck!
0
 
LVL 5

Author Comment

by:Leithauser
ID: 18899591
IamWelch:

Your solution using GetUserAppDataPath seems to provide a solution in some cases, although not as universally applicable as I would like because it does not allow my programs to share data among uses.

I would like to hear more about this requesting elevation. I have not been able to get it to work, partly because the description in the link you provided merely says "now add this file to your vb project, compile, and voila!". I do not understand how to add this file to my project. VB 6 does not appear to recognize res as a file type, and if I simply add the file to the list of files in the project and compile it, the exe will not run. It gives an error message that says "This application has failed to start because the application configuration is incorrect." Obviously, I am not familiar with res files and do not know how to use them.

Also, once it is installed and operating properly, does the program actually ask the user if they want to elevate the status, or does it happen automatically? If this process causes my program to actually ask if the status should be elevated to administrative, it is of little use to me. My programs are security related (restricting Internet access and things like that). They are designed to run automatically when the computer boots and restrict the user. If the program has to ask for permission from the user to have administrative access to run, the user will obviously deny the access and block the program.

I am willing to assign the points to you for your GetUserAppDataPath solution, but if you can help me get the elevated access process working, I will double the points for you.
0
 
LVL 5

Author Comment

by:Leithauser
ID: 18946792
quiklearner's suggestion for using savesetting might be useful in some cases, but not mine, because I am talking about storing basically an unlimited number of data items. For example, the program that I was most concerned with needs to store hundreds of constantly changing passwords. Although I had hoped for a more general solution that would allow sharing of data between users on one computer, IamWelch's solution did at least provide a way for my programs to save data from a limited user account. It thus solves my most immediate problem with a few programs that need to write data. Since it appears no further elaboration on this solution is coming, I am awarding the 250 points to IamWelch.
0

Featured Post

How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

Join & Write a Comment

Suggested Solutions

Title # Comments Views Activity
Mysql vs Oracle 10 120
Input past end of file vbs script 9 68
using Access 8 52
Add a task in Outlook from access 11 32
Introduction I needed to skip over some file processing within a For...Next loop in some old production code and wished that VB (classic) had a statement that would drop down to the end of the current iteration, bypassing the statements that were c…
Enums (shorthand for ‘enumerations’) are not often used by programmers but they can be quite valuable when they are.  What are they? An Enum is just a type of variable like a string or an Integer, but in this case one that you create that contains…
As developers, we are not limited to the functions provided by the VBA language. In addition, we can call the functions that are part of the Windows operating system. These functions are part of the Windows API (Application Programming Interface). U…
Get people started with the process of using Access VBA to control Excel using automation, Microsoft Access can control other applications. An example is the ability to programmatically talk to Excel. Using automation, an Access application can laun…

706 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

16 Experts available now in Live!

Get 1:1 Help Now