Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1669 lines
47 KiB

//+-------------------------------------------------------------------------
//
// Microsoft Windows
//
// Copyright (C) Microsoft Corporation, 1996 - 1999
//
// File: rshx32.cpp
//
// Remote administration shell extension.
//
//--------------------------------------------------------------------------
///////////////////////////////////////////////////////////////////////////////
// //
// Include files //
// //
///////////////////////////////////////////////////////////////////////////////
#include "rshx32.h"
#include <winnetwk.h> // WNetGetConnection
#include <lm.h>
#include <lmdfs.h> // NetDfsGetClientInfo
#include <atlconv.h>
#include <initguid.h>
DEFINE_GUID(CLSID_NTFSSecurityExt, 0x1f2e5c40, 0x9550, 0x11ce, 0x99, 0xd2, 0x00, 0xaa, 0x00, 0x6e, 0x08, 0x6c);
DEFINE_GUID(CLSID_PrintSecurityExt, 0xf37c5810, 0x4d3f, 0x11d0, 0xb4, 0xbf, 0x00, 0xaa, 0x00, 0xbb, 0xb7, 0x23);
#define IID_PPV_ARG(IType, ppType) IID_##IType, reinterpret_cast<void**>(static_cast<IType**>(ppType))
#define RSX_SECURITY_CHECKED 0x00000001L
#define RSX_HAVE_SECURITY 0x00000002L
#define DOBJ_RES_CONT 0x00000001L
#define DOBJ_RES_ROOT 0x00000002L
#define DOBJ_VOL_NTACLS 0x00000004L // NTFS or OFS
class CRShellExtCF : public IClassFactory
{
protected:
ULONG m_cRef;
SE_OBJECT_TYPE m_seType;
public:
CRShellExtCF(SE_OBJECT_TYPE seType);
~CRShellExtCF();
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
// IClassFactory methods
STDMETHODIMP CreateInstance(LPUNKNOWN, REFIID, void **);
STDMETHODIMP LockServer(BOOL);
};
class CRShellExt : public IShellExtInit, IShellPropSheetExt, IContextMenu
{
protected:
ULONG m_cRef;
SE_OBJECT_TYPE m_seType;
IDataObject *m_lpdobj; // interface passed in by shell
HRESULT m_hrSecurityCheck;
DWORD m_dwSIFlags;
LPTSTR m_pszServer;
LPTSTR m_pszObject;
HDPA m_hItemList;
public:
CRShellExt(SE_OBJECT_TYPE seType);
~CRShellExt();
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
// IShellExtInit method
STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
// IShellPropSheetExt methods
STDMETHODIMP AddPages(LPFNADDPROPSHEETPAGE, LPARAM);
STDMETHODIMP ReplacePage(UINT, LPFNADDPROPSHEETPAGE, LPARAM);
//IContextMenu methods
STDMETHODIMP QueryContextMenu(HMENU hMenu,
UINT indexMenu,
UINT idCmdFirst,
UINT idCmdLast,
UINT uFlags);
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi);
STDMETHODIMP GetCommandString(UINT_PTR idCmd,
UINT uFlags,
UINT *reserved,
LPSTR pszName,
UINT cchMax);
private:
STDMETHODIMP DoSecurityCheck(LPIDA pIDA);
STDMETHODIMP CheckForSecurity(LPIDA pIDA);
STDMETHODIMP CreateSI(LPSECURITYINFO *ppsi);
STDMETHODIMP AddSecurityPage(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam);
BOOL IsAddPrinterWizard() const;
#if (_WIN32_WINNT >= 0x0500)
STDMETHODIMP AddMountedVolumePage(LPFNADDPROPSHEETPAGE lpfnAddPage,
LPARAM lParam);
#endif
};
typedef CRShellExt* PRSHELLEXT;
///////////////////////////////////////////////////////////////////////////////
// //
// Global variables //
// //
///////////////////////////////////////////////////////////////////////////////
HINSTANCE g_hInstance = NULL;
LONG g_cRefThisDll = 0;
CLIPFORMAT g_cfShellIDList = 0;
CLIPFORMAT g_cfPrinterGroup = 0;
CLIPFORMAT g_cfMountedVolume = 0;
HMODULE g_hAclui = NULL;
///////////////////////////////////////////////////////////////////////////////
// //
// Private prototypes //
// //
///////////////////////////////////////////////////////////////////////////////
void GetFileInfo(LPCTSTR pszPath,
LPDWORD pdwFileType,
LPTSTR pszServer,
ULONG cchServer,
LPTSTR *ppszAlternatePath);
///////////////////////////////////////////////////////////////////////////////
// //
// General routines //
// //
///////////////////////////////////////////////////////////////////////////////
/*++
Routine Description:
Dll's entry point.
In order to service requests for file selection information from
any of the file manager extensions to be included in this library,
we must first register a window class to accept these requests.
The Microsoft_Network provider transfers information via a private
clipboard format called "Net Resource" which we must register.
Arguments:
Same as DllEntryPoint.
Return Values:
Same as DllEntryPoint.
--*/
STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void * /*lpReserved*/)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
g_hInstance = hInstance;
g_cfShellIDList = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST);
g_cfPrinterGroup = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_PRINTERGROUP);
g_cfMountedVolume = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_MOUNTEDVOLUME);
DebugProcessAttach();
TraceSetMaskFromCLSID(CLSID_NTFSSecurityExt);
#ifndef DEBUG
DisableThreadLibraryCalls(hInstance);
#endif
break;
case DLL_PROCESS_DETACH:
if (g_hAclui)
FreeLibrary(g_hAclui);
DebugProcessDetach();
break;
case DLL_THREAD_DETACH:
DebugThreadDetach();
break;
}
return TRUE;
}
/*++
Routine Description:
Called by shell to create a class factory object.
Arguments:
rclsid - reference to class id specifier.
riid - reference to interface id specifier.
ppv - pointer to location to receive interface pointer.
Return Values:
Returns HRESULT signifying success or failure.
--*/
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv)
{
HRESULT hr;
SE_OBJECT_TYPE seType;
*ppv = NULL;
if (IsEqualCLSID(rclsid, CLSID_NTFSSecurityExt))
seType = SE_FILE_OBJECT;
else if (IsEqualCLSID(rclsid, CLSID_PrintSecurityExt))
seType = SE_PRINTER;
else
return CLASS_E_CLASSNOTAVAILABLE;
CRShellExtCF *pShellExtCF = new CRShellExtCF(seType); // ref == 1
if (!pShellExtCF)
return E_OUTOFMEMORY;
hr = pShellExtCF->QueryInterface(riid, ppv);
pShellExtCF->Release(); // release initial ref
return hr;
}
/*++
Routine Description:
Called by shell to find out if dll can be unloaded.
Arguments:
None.
Return Values:
Returns S_OK if dll can be unloaded, S_FALSE if not.
--*/
STDAPI DllCanUnloadNow()
{
return (g_cRefThisDll == 0 ? S_OK : S_FALSE);
}
STDAPI DllRegisterServer(void)
{
return CallRegInstall(g_hInstance, "DefaultInstall");
}
STDAPI DllUnregisterServer(void)
{
return CallRegInstall(g_hInstance, "DefaultUninstall");
}
///////////////////////////////////////////////////////////////////////////////
// //
// Class factory object implementation //
// //
///////////////////////////////////////////////////////////////////////////////
CRShellExtCF::CRShellExtCF(SE_OBJECT_TYPE seType) : m_cRef(1), m_seType(seType)
{
InterlockedIncrement(&g_cRefThisDll);
}
CRShellExtCF::~CRShellExtCF()
{
InterlockedDecrement(&g_cRefThisDll);
}
///////////////////////////////////////////////////////////////////////////////
// //
// Class factory object implementation (IUnknown) //
// //
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP_(ULONG) CRShellExtCF::AddRef()
{
return ++m_cRef;
}
STDMETHODIMP_(ULONG) CRShellExtCF::Release()
{
if (--m_cRef == 0)
{
delete this;
return 0;
}
return m_cRef;
}
STDMETHODIMP CRShellExtCF::QueryInterface(REFIID riid, void ** ppv)
{
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory))
{
*ppv = (IClassFactory *)this;
m_cRef++;
return S_OK;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
}
///////////////////////////////////////////////////////////////////////////////
// //
// Class factory object implementation (IClassFactory) //
// //
///////////////////////////////////////////////////////////////////////////////
/*++
Routine Description:
Support for IClassFactory::CreateInstance.
Arguments:
pUnkOuter - pointer to controlling unknown.
riid - reference to interface id specifier.
ppvObj - pointer to location to receive interface pointer.
Return Values:
Returns HRESULT signifying success or failure.
--*/
STDMETHODIMP CRShellExtCF::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void ** ppvObj)
{
*ppvObj = NULL;
if (pUnkOuter)
return CLASS_E_NOAGGREGATION;
CRShellExt *pShellExt = new CRShellExt(m_seType);// ref count == 1
if (!pShellExt)
return E_OUTOFMEMORY;
HRESULT hr = pShellExt->QueryInterface(riid, ppvObj);
pShellExt->Release(); // release initial ref
return hr;
}
/*++
Routine Description:
Support for IClassFactory::LockServer (not implemented).
Arguments:
fLock - true if lock count to be incremented.
Return Values:
Returns E_NOTIMPL.
--*/
STDMETHODIMP CRShellExtCF::LockServer(BOOL /*fLock*/)
{
return E_NOTIMPL;
}
///////////////////////////////////////////////////////////////////////////////
// //
// Shell extension object implementation //
// //
///////////////////////////////////////////////////////////////////////////////
CRShellExt::CRShellExt(SE_OBJECT_TYPE seType) : m_cRef(1), m_seType(seType),
m_dwSIFlags(SI_EDIT_ALL | SI_ADVANCED | SI_EDIT_EFFECTIVE), m_hrSecurityCheck((HRESULT)-1),
m_hItemList(NULL)
{
InterlockedIncrement(&g_cRefThisDll);
}
CRShellExt::~CRShellExt()
{
DoRelease(m_lpdobj);
LocalFreeString(&m_pszServer);
LocalFreeString(&m_pszObject);
LocalFreeDPA(m_hItemList);
InterlockedDecrement(&g_cRefThisDll);
}
///////////////////////////////////////////////////////////////////////////////
// //
// Shell extension object implementation (IUnknown) //
// //
///////////////////////////////////////////////////////////////////////////////
STDMETHODIMP_(ULONG)
CRShellExt::AddRef()
{
return ++m_cRef;
}
STDMETHODIMP_(ULONG)
CRShellExt::Release()
{
if (--m_cRef == 0)
{
delete this;
return 0;
}
return m_cRef;
}
STDMETHODIMP CRShellExt::QueryInterface(REFIID riid, void ** ppv)
{
if (IsEqualIID(riid, IID_IShellExtInit) || IsEqualIID(riid, IID_IUnknown))
{
*ppv = (LPSHELLEXTINIT)this;
m_cRef++;
return S_OK;
}
else if (IsEqualIID(riid, IID_IContextMenu))
{
*ppv = (LPCONTEXTMENU)this;
m_cRef++;
return S_OK;
}
else if (IsEqualIID(riid, IID_IShellPropSheetExt))
{
*ppv = (LPSHELLPROPSHEETEXT)this;
m_cRef++;
return S_OK;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
}
///////////////////////////////////////////////////////////////////////////////
// //
// Shell extension object implementation (IShellExtInit) //
// //
///////////////////////////////////////////////////////////////////////////////
/*++
Routine Description:
Support for IShellExtInit::Initialize.
Arguments:
pidlFolder - pointer to id list identifying parent folder.
lpdobj - pointer to IDataObject interface for selected object(s).
hKeyProgId - registry key handle.
Return Values:
Returns HRESULT signifying success or failure.
--*/
STDMETHODIMP CRShellExt::Initialize(LPCITEMIDLIST /*pidlFolder*/, IDataObject *lpdobj, HKEY /*hKeyProgID*/)
{
DoRelease(m_lpdobj);
m_lpdobj = lpdobj; // processed in AddPages
if (m_lpdobj)
m_lpdobj->AddRef();
return S_OK;
}
///////////////////////////////////////////////////////////////////////////////
// //
// Shell extension object implementation (IShellPropSheetExt) //
// //
///////////////////////////////////////////////////////////////////////////////
/*++
Routine Description:
Support for IShellPropSheetExt::AddPages.
Arguments:
lpfnAddPage - pointer to function called to add a page.
lParam - lParam parameter to be passed to lpfnAddPage.
Return Values:
Returns HRESULT signifying success or failure.
--*/
STDMETHODIMP
CRShellExt::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage,
LPARAM lParam)
{
HRESULT hr;
STGMEDIUM medium = {0};
FORMATETC fe = { g_cfShellIDList, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
LPIDA pIDA = NULL;
TraceEnter(TRACE_RSHX32, "CRShellExt::AddPages");
if (IsSimpleUI())
ExitGracefully(hr, E_FAIL, "No Security page in simple mode");
//
//Check if Security Tab is hidden by privacy policy
//NTRAID#NTBUG9-223899-2001/03/06-hiteshr
//
if(IsUIHiddenByPrivacyPolicy())
ExitGracefully(hr, E_FAIL, "Security Page is hidden by Privacy Policy");
// Get the ID List data
hr = m_lpdobj->GetData(&fe, &medium);
#if (_WIN32_WINNT >= 0x0500)
if (FAILED(hr) && m_seType == SE_FILE_OBJECT)
TraceLeaveResult(AddMountedVolumePage(lpfnAddPage, lParam));
#endif
FailGracefully(hr, "Can't get ID List format from data object");
pIDA = (LPIDA)GlobalLock(medium.hGlobal);
TraceAssert(pIDA != NULL);
// Only support single selection for printers
if (m_seType == SE_PRINTER && pIDA->cidl != 1)
ExitGracefully(hr, E_FAIL, "Printer multiple selection not supported");
hr = DoSecurityCheck(pIDA);
if (S_OK == hr)
hr = AddSecurityPage(lpfnAddPage, lParam);
exit_gracefully:
if (pIDA)
GlobalUnlock(medium.hGlobal);
ReleaseStgMedium(&medium);
TraceLeaveResult(hr);
}
/*++
Routine Description:
Support for IShellPropSheetExt::ReplacePages (not supported).
Arguments:
uPageID - page to replace.
lpfnReplaceWith - pointer to function called to replace a page.
lParam - lParam parameter to be passed to lpfnReplaceWith.
Return Values:
Returns E_FAIL.
--*/
STDMETHODIMP
CRShellExt::ReplacePage(UINT /* uPageID */,
LPFNADDPROPSHEETPAGE /* lpfnReplaceWith */,
LPARAM /* lParam */)
{
return E_NOTIMPL;
}
///////////////////////////////////////////////////////////////////////////////
// //
// Shell extension object implementation (IContextMenu) //
// //
///////////////////////////////////////////////////////////////////////////////
//
// FUNCTION: IContextMenu::QueryContextMenu(HMENU, UINT, UINT, UINT, UINT)
//
// PURPOSE: Called by the shell just before the context menu is displayed.
// This is where you add your specific menu items.
//
// PARAMETERS:
// hMenu - Handle to the context menu
// indexMenu - Index of where to begin inserting menu items
// idCmdFirst - Lowest value for new menu ID's
// idCmtLast - Highest value for new menu ID's
// uFlags - Specifies the context of the menu event
//
// RETURN VALUE:
// HRESULT signifying success or failure.
//
// COMMENTS:
//
STDMETHODIMP
CRShellExt::QueryContextMenu(HMENU hMenu,
UINT indexMenu,
UINT idCmdFirst,
UINT /*idCmdLast*/,
UINT uFlags)
{
HRESULT hr = ResultFromShort(0);
STGMEDIUM medium = {0};
FORMATETC fe = { g_cfShellIDList, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
if (uFlags & (CMF_DEFAULTONLY | CMF_VERBSONLY))
return hr;
TraceEnter(TRACE_RSHX32, "CRShellExt::QueryContextMenu");
// Get the ID List data
hr = m_lpdobj->GetData(&fe, &medium);
if (SUCCEEDED(hr))
{
LPIDA pIDA = (LPIDA)GlobalLock(medium.hGlobal);
TraceAssert(pIDA != NULL);
// Only support single selection
if (pIDA->cidl == 1)
{
if (S_OK == DoSecurityCheck(pIDA))
{
TCHAR szSecurity[32];
if (LoadString(g_hInstance, IDS_SECURITY_MENU, szSecurity, ARRAYSIZE(szSecurity)))
{
MENUITEMINFO mii;
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_TYPE | MIIM_ID;
mii.fType = MFT_STRING;
mii.wID = idCmdFirst;
mii.dwTypeData = szSecurity;
mii.cch = lstrlen(szSecurity);
InsertMenuItem(hMenu, indexMenu, TRUE /*fByPosition*/, &mii);
hr = ResultFromShort(1); // Return number of items we added
}
}
}
GlobalUnlock(medium.hGlobal);
ReleaseStgMedium(&medium);
}
TraceLeaveResult(hr);
}
//
// FUNCTION: IContextMenu::InvokeCommand(LPCMINVOKECOMMANDINFO)
//
// PURPOSE: Called by the shell after the user has selected on of the
// menu items that was added in QueryContextMenu().
//
// PARAMETERS:
// lpcmi - Pointer to an CMINVOKECOMMANDINFO structure
//
// RETURN VALUE:
// HRESULT signifying success or failure.
//
// COMMENTS:
//
STDMETHODIMP
CRShellExt::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
HRESULT hr = S_OK;
STGMEDIUM medium;
FORMATETC fe = { g_cfShellIDList, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
// Don't support named verbs
if (HIWORD(lpcmi->lpVerb))
return E_NOTIMPL;
TraceEnter(TRACE_RSHX32, "CRShellExt::InvokeCommand");
// We only have one command, so we should get zero here
TraceAssert(LOWORD(lpcmi->lpVerb) == 0);
// This must be true for us to have added the command to the menu
TraceAssert(S_OK == m_hrSecurityCheck);
//
// Call ShellExecuteEx to execute the "Properties" verb on this object, and
// tell it to select the security property page.
//
// Get the ID List data
hr = m_lpdobj->GetData(&fe, &medium);
if (SUCCEEDED(hr))
{
LPIDA pIDA = (LPIDA)GlobalLock(medium.hGlobal);
LPITEMIDLIST pidl;
// We only support single selection for context menus
TraceAssert(pIDA && pIDA->cidl == 1);
// Build a fully qualified ID List for this object
pidl = ILCombine((LPCITEMIDLIST)ByteOffset(pIDA, pIDA->aoffset[0]),
(LPCITEMIDLIST)ByteOffset(pIDA, pIDA->aoffset[1]));
if (pidl != NULL)
{
TCHAR szTitle[64];
SHELLEXECUTEINFO sei =
{
sizeof(SHELLEXECUTEINFO),
(lpcmi->fMask & (SEE_MASK_HOTKEY | SEE_MASK_ICON)) | SEE_MASK_INVOKEIDLIST,
lpcmi->hwnd,
c_szProperties, // lpVerb ("Properties")
NULL, // lpFile
szTitle, // lpParameters ("Security")
NULL, // lpDirectory,
lpcmi->nShow, // nShow
NULL, // hInstApp
(LPVOID)pidl, // lpIDList
NULL, // lpClass
NULL, // hkeyClass
lpcmi->dwHotKey, // dwHotKey
lpcmi->hIcon, // hIcon
NULL // hProcess
};
LoadString(g_hInstance, IDS_PROPPAGE_TITLE, szTitle, ARRAYSIZE(szTitle));
// Put up the properties dialog
if (!ShellExecuteEx(&sei))
{
DWORD dwErr = GetLastError();
hr = HRESULT_FROM_WIN32(dwErr);
}
ILFree(pidl);
}
GlobalUnlock(medium.hGlobal);
ReleaseStgMedium(&medium);
}
#if 0
//
// SHObjectProperties builds a pidl to the object and then calls
// ShellExecuteEx. Similar to above, but it does more work to obtain the
// ID lists (which we already have).
//
SHObjectProperties(lpcmi->hwnd,
m_seType == SE_PRINTER ? SHOP_PRINTERNAME : SHOP_FILEPATH,
m_pszObject,
TEXT("Security"));
#endif
TraceLeaveResult(hr);
}
//
// FUNCTION: IContextMenu::GetCommandString(UINT, UINT, UINT, LPSTR, UINT)
//
// PURPOSE: Called by the shell after the user has selected on of the
// menu items that was added in QueryContextMenu().
//
// PARAMETERS:
// lpcmi - Pointer to an CMINVOKECOMMANDINFO structure
//
// RETURN VALUE:
// HRESULT signifying success or failure.
//
// COMMENTS:
//
STDMETHODIMP
CRShellExt::GetCommandString(UINT_PTR /*idCmd*/,
UINT uFlags,
LPUINT /*reserved*/,
LPSTR pszName,
UINT cchMax)
{
if (uFlags == GCS_HELPTEXT)
{
LoadString(g_hInstance, IDS_SECURITY_HELPSTRING, (LPTSTR)pszName, cchMax);
return S_OK;
}
// Must be some other flag that we don't handle
return E_NOTIMPL;
}
//
// FUNCTION: CRShellExt::DoSecurityCheck(LPIDA)
//
// PURPOSE: Helper function called by the Property Sheet and Context Menu
// extension code. Used to determine whether to add the menu item
// or property sheet.
//
// PARAMETERS:
// pIDA - pointer to ID List Array specifying selected objects
//
// RETURN VALUE: none
//
// COMMENTS:
// The results are stored in m_hrSecurityCheck, m_dwSIFlags, m_pszServer, and m_pszObject
//
STDMETHODIMP CRShellExt::DoSecurityCheck(LPIDA pIDA)
{
if (((HRESULT)-1) == m_hrSecurityCheck)
{
if (m_seType == SE_PRINTER && IsAddPrinterWizard())
m_hrSecurityCheck = HRESULT_FROM_WIN32(ERROR_NO_SECURITY_ON_OBJECT);
else
m_hrSecurityCheck = CheckForSecurity(pIDA);
}
return m_hrSecurityCheck;
}
//
// PURPOSE: Helper function called by CRShellExt::DoSecurityCheck
//
// PARAMETERS: pIDA - pointer to ID List array
//
// RETURN VALUE: HRESULT - S_OK if ACL editing can proceed
//
// COMMENTS:
// The results are stored in m_dwSIFlags, m_pszServer, and m_pszObject
//
STDMETHODIMP CRShellExt::CheckForSecurity(LPIDA pIDA)
{
HRESULT hr;
TCHAR szServer[MAX_PATH];
LPTSTR pszItem = NULL;
// LPTSTR pszAlternate = NULL;
DWORD dwFlags = 0;
UINT cItems;
IShellFolder2 * psf = NULL;
LPCITEMIDLIST pidl;
DWORD dwAttr;
DWORD dwPrivs[] = { SE_SECURITY_PRIVILEGE, SE_TAKE_OWNERSHIP_PRIVILEGE };
HANDLE hToken = INVALID_HANDLE_VALUE;
ACCESS_MASK dwAccess = 0;
UINT i;
TraceEnter(TRACE_RSHX32, "CRShellExt::CheckForSecurity");
TraceAssert(m_pszServer == NULL); // Shouldn't get called twice
TraceAssert(pIDA != NULL);
szServer[0] = TEXT('\0');
cItems = pIDA->cidl;
TraceAssert(cItems >= 1);
//We don't show effective perm page for multiple selection
if (cItems > 1)
m_dwSIFlags &= ~SI_EDIT_EFFECTIVE;
IShellFolder2 *psfRoot = NULL;
LPCITEMIDLIST pidlFolder = (LPCITEMIDLIST)ByteOffset(pIDA, pIDA->aoffset[0]);
hr = BindToObjectEx(NULL, pidlFolder, NULL, IID_PPV_ARG(IShellFolder2, &psfRoot));
FailGracefully(hr, "Unable to bind to folder");
TraceAssert(psfRoot);
// Create list for item paths
TraceAssert(NULL == m_hItemList);
m_hItemList = DPA_Create(4);
if (NULL == m_hItemList)
ExitGracefully(hr, E_OUTOFMEMORY, "Unable to create DPA");
//
// Get the first item and see if it supports security
//
LPCITEMIDLIST pidlItem = (LPCITEMIDLIST)ByteOffset(pIDA, pIDA->aoffset[1]);
hr = BindToFolderIDListParent(psfRoot, pidlItem, IID_PPV_ARG(IShellFolder2, &psf), &pidl);
FailGracefully(hr, "Unable to get item name");
hr = IDA_GetItemName(psf, pidl, &pszItem);
FailGracefully(hr, "Unable to get item name");
dwAttr = SFGAO_FOLDER | SFGAO_STREAM | SFGAO_FILESYSTEM;
hr = psf->GetAttributesOf(1, &pidl, &dwAttr);
FailGracefully(hr, "Unable to get item attributes");
DoRelease(psf);
//
//If ACLUI is invoked for filesystem and object is not of filesystem
//return E_FAIL
//
if ((m_seType == SE_FILE_OBJECT) && !(dwAttr & SFGAO_FILESYSTEM))
ExitGracefully(hr, E_FAIL, "Not a filesystem object");
// in the case that an item is both folder and stream, assume its a stream (.zip, .cab file)
// and not a container
if ((dwAttr & (SFGAO_FOLDER | SFGAO_STREAM)) == SFGAO_FOLDER)
dwFlags |= DOBJ_RES_CONT;
//
// Check access on the first item only. If we can write the DACL
// on the first one, we will try (later) to write to all items
// in the selection and report any errors at that time.
//
hToken = EnablePrivileges(dwPrivs, ARRAYSIZE(dwPrivs));
switch (m_seType)
{
case SE_FILE_OBJECT:
GetFileInfo(pszItem, &dwFlags, szServer, ARRAYSIZE(szServer), NULL);
if (dwFlags & DOBJ_VOL_NTACLS)
hr = CheckFileAccess(pszItem, &dwAccess);
else
hr = HRESULT_FROM_WIN32(ERROR_NO_SECURITY_ON_OBJECT);
break;
case SE_PRINTER:
// Printers are containers (they contain documents)
// and they don't have a parent (for acl editing purposes)
dwFlags = DOBJ_RES_CONT | DOBJ_RES_ROOT;
hr = CheckPrinterAccess(pszItem, &dwAccess, szServer, ARRAYSIZE(szServer));
break;
default:
hr = E_UNEXPECTED;
}
FailGracefully(hr, "No access");
// If we can't do anything security related, don't continue.
if (!(dwAccess & ALL_SECURITY_ACCESS))
ExitGracefully(hr, E_ACCESSDENIED, "No access");
// Remember the server name
if (TEXT('\0') != szServer[0])
{
hr = LocalAllocString(&m_pszServer, szServer);
FailGracefully(hr, "LocalAlloc failed");
}
// Remember the item path
DPA_AppendPtr(m_hItemList, pszItem);
pszItem = NULL;
if (!(dwAccess & WRITE_DAC))
m_dwSIFlags |= SI_READONLY;
if (!(dwAccess & WRITE_OWNER))
{
if (!(dwAccess & READ_CONTROL))
m_dwSIFlags &= ~SI_EDIT_OWNER;
else
m_dwSIFlags |= SI_OWNER_READONLY;
}
if (!(dwAccess & ACCESS_SYSTEM_SECURITY))
m_dwSIFlags &= ~SI_EDIT_AUDITS;
//
// Check the rest of the selection. If any part of a multiple
// selection doesn't support ACLs or the selection isn't homogenous,
// then we can't create the security page.
//
for (i = 2; i <= cItems; i++)
{
DWORD dw = 0;
// We only do multiple selections for files
TraceAssert(SE_FILE_OBJECT == m_seType);
LPCITEMIDLIST pidlItem = (LPCITEMIDLIST)ByteOffset(pIDA, pIDA->aoffset[i]);
hr = BindToFolderIDListParent(psfRoot, pidlItem, IID_PPV_ARG(IShellFolder2, &psf), &pidl);
FailGracefully(hr, "Unable to get item name");
hr = IDA_GetItemName(psf, pidl, &pszItem);
FailGracefully(hr, "Unable to get item name");
dwAttr = SFGAO_FOLDER | SFGAO_STREAM | SFGAO_FILESYSTEM;
hr = psf->GetAttributesOf(1, &pidl, &dwAttr);
FailGracefully(hr, "Unable to get item attributes");
DoRelease(psf);
//
//If ACLUI is invoked for filesystem and object is not of filesystem
//return E_FAIL
//
if ((m_seType == SE_FILE_OBJECT) && !(dwAttr & SFGAO_FILESYSTEM))
ExitGracefully(hr, E_FAIL, "Not a filesystem object");
if ((dwAttr & (SFGAO_FOLDER | SFGAO_STREAM)) == SFGAO_FOLDER)
dw |= DOBJ_RES_CONT;
if ((dw & DOBJ_RES_CONT) != (dwFlags & DOBJ_RES_CONT))
ExitGracefully(hr, E_FAIL, "Incompatible multiple selection");
GetFileInfo(pszItem, &dw, szServer, ARRAYSIZE(szServer), NULL);
// Compare against first item. All flags and the server name
// must match, otherwise we can't edit the ACLs.
if (dw == dwFlags &&
((NULL == m_pszServer && TEXT('\0') == szServer[0]) ||
(NULL != m_pszServer && 0 == lstrcmpi(m_pszServer, szServer))))
{
// Remember the item path
DPA_AppendPtr(m_hItemList, pszItem);
pszItem = NULL;
}
else
ExitGracefully(hr, E_FAIL, "Incompatible multiple selection");
}
//
// If everything has succeeded up to this point, save some flags
// and the server and object name strings
//
if (dwFlags & DOBJ_RES_CONT)
m_dwSIFlags |= SI_CONTAINER;
//
// For Root objects (e.g. "D:\") hide the ACL Protection checkbox,
// since these objects don't appear to have parents.
//
if (dwFlags & DOBJ_RES_ROOT)
m_dwSIFlags |= SI_NO_ACL_PROTECT;
// Get the "Normal" display name to use as the object name
hr = IDA_GetItemName(psfRoot, (LPCITEMIDLIST)ByteOffset(pIDA, pIDA->aoffset[1]),
szServer, ARRAYSIZE(szServer), SHGDN_NORMAL);
FailGracefully(hr, "Unable to get item name");
if (cItems > 1)
{
int nLength = lstrlen(szServer);
LoadString(g_hInstance, IDS_MULTISEL_ELLIPSIS, szServer + nLength, ARRAYSIZE(szServer) - nLength);
}
hr = LocalAllocString(&m_pszObject, szServer);
exit_gracefully:
ReleasePrivileges(hToken);
DoRelease(psf);
DoRelease(psfRoot);
LocalFreeString(&pszItem);
TraceLeaveResult(hr);
}
//
// FUNCTION: CRShellExt::CreateSI(LPSECURITYINFO *)
//
// PURPOSE: Create a SecurityInformation object of the correct type
//
// PARAMETERS: ppsi - Location to store ISecurityInformation pointer
//
// RETURN VALUE: HRESULT signifying success or failure
//
// COMMENTS:
//
STDMETHODIMP
CRShellExt::CreateSI(LPSECURITYINFO *ppsi)
{
HRESULT hr;
CSecurityInformation *psi;
TraceEnter(TRACE_RSHX32, "CRShellExt::CreateSI");
TraceAssert(ppsi != NULL);
*ppsi = NULL;
switch (m_seType)
{
case SE_FILE_OBJECT:
psi = new CNTFSSecurity(m_seType); // ref == 1
break;
case SE_PRINTER:
psi = new CPrintSecurity(m_seType); // ref == 1
break;
default:
TraceLeaveResult(E_UNEXPECTED);
}
if (psi == NULL)
TraceLeaveResult(E_OUTOFMEMORY);
hr = psi->Initialize(m_hItemList,
m_dwSIFlags,
m_pszServer,
m_pszObject);
if (SUCCEEDED(hr))
{
*ppsi = psi;
// The SecurityInfo object takes responsibility for these
m_hItemList = NULL;
m_pszServer = NULL;
m_pszObject = NULL;
m_hrSecurityCheck = (HRESULT)-1;
}
else
psi->Release();
TraceLeaveResult(hr);
}
typedef HPROPSHEETPAGE (WINAPI *PFN_CREATESECPAGE)(LPSECURITYINFO);
HPROPSHEETPAGE _CreateSecurityPage(LPSECURITYINFO psi)
{
HPROPSHEETPAGE hPage = NULL;
const TCHAR szAclui[] = TEXT("aclui.dll");
const char szCreateSecPage[] = "CreateSecurityPage";
if (!g_hAclui)
g_hAclui = LoadLibrary(szAclui);
if (g_hAclui)
{
static PFN_CREATESECPAGE s_pfnCreateSecPage = NULL;
if (!s_pfnCreateSecPage)
s_pfnCreateSecPage = (PFN_CREATESECPAGE)GetProcAddress(g_hAclui, szCreateSecPage);
if (s_pfnCreateSecPage)
hPage = (*s_pfnCreateSecPage)(psi);
}
return hPage;
}
STDMETHODIMP
CRShellExt::AddSecurityPage(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
HRESULT hr;
LPSECURITYINFO psi;
hr = CreateSI(&psi); // ref == 1
if (SUCCEEDED(hr))
{
HPROPSHEETPAGE hPermPage = _CreateSecurityPage(psi);
if (hPermPage)
{
if (!lpfnAddPage(hPermPage, lParam))
DestroyPropertySheetPage(hPermPage);
}
else
{
DWORD dwErr = GetLastError();
hr = HRESULT_FROM_WIN32(dwErr);
}
psi->Release(); // release initial ref
}
return hr;
}
//
// PURPOSE: Check for the Add Printer wizard
//
// PARAMETERS: none
//
// RETURN VALUE: TRUE if the selected object is the Add Printer wizard,
// FALSE otherwise
//
// COMMENTS:
//
BOOL CRShellExt::IsAddPrinterWizard() const
{
BOOL bRetval = FALSE;
STGMEDIUM medium;
FORMATETC fe = { g_cfPrinterGroup, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
TCHAR szFile[MAX_PATH];
TraceEnter(TRACE_RSHX32, "CRShellExt::IsAddPrinterWizard");
TraceAssert(m_seType == SE_PRINTER);
//
// Fail the call if m_lpdobj is NULL.
//
if ( m_lpdobj && SUCCEEDED( m_lpdobj->GetData( &fe, &medium ) ) )
{
#if (_WIN32_WINNT < 0x0500)
//
// In NT 4.0 the printer context menus in the shell failed
// to handle UNICODE strings properly during a call to DragQueryFile.
// The strings returned were always ANSI. This has been fixed
// in build 1393 and greater.
//
#ifdef UNICODE
LPDROPFILES pdf = (LPDROPFILES)GlobalLock(medium.hGlobal);
pdf->fWide = TRUE;
GlobalUnlock(medium.hGlobal);
#endif
#endif
//
// Get the selected item name.
//
if ( DragQueryFile( (HDROP)medium.hGlobal, 0, szFile, ARRAYSIZE( szFile ) ) )
{
//
// Check if this is the magic Add Printer Wizard shell object.
// The check is not case sensitive and the string is not localized.
//
if ( 0 == lstrcmpi( szFile, TEXT("WinUtils_NewObject") ) )
{
TraceMsg("Found Add Printer wizard");
bRetval = TRUE;
}
}
//
// Release the storage medium.
//
ReleaseStgMedium( &medium );
}
TraceLeaveValue(bRetval);
}
#if (_WIN32_WINNT >= 0x0500)
//
// FUNCTION: CRShellExt::AddMountedVolumePage()
//
// PURPOSE: Create Security page for mounted volume properties
//
// PARAMETERS: lpfnAddPage - pointer to function called to add a page.
// lParam - lParam parameter to be passed to lpfnAddPage.
//
// RETURN VALUE: HRESULT signifying success or failure
//
// COMMENTS:
//
STDMETHODIMP
CRShellExt::AddMountedVolumePage(LPFNADDPROPSHEETPAGE lpfnAddPage,
LPARAM lParam)
{
HRESULT hr = S_OK;
STGMEDIUM medium = {0};
FORMATETC fe = { g_cfMountedVolume, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
TCHAR szMountPoint[MAX_PATH];
TCHAR szVolumeID[MAX_PATH];
TCHAR szLabel[64];
LPTSTR pszVolID = NULL;
DWORD dwVolFlags = 0;
DWORD dwPrivs[] = { SE_SECURITY_PRIVILEGE, SE_TAKE_OWNERSHIP_PRIVILEGE };
HANDLE hToken = INVALID_HANDLE_VALUE;
ACCESS_MASK dwAccess = 0;
BOOL bHasSecurity = FALSE;
TraceEnter(TRACE_RSHX32, "CRShellExt::AddMountedVolumePage");
TraceAssert(m_seType == SE_FILE_OBJECT);
TraceAssert(m_lpdobj);
// Try to get the mounted volume host folder path
hr = m_lpdobj->GetData(&fe, &medium);
FailGracefully(hr, "Not a mounted volume");
// Get the host folder path
if (!DragQueryFile((HDROP)medium.hGlobal, 0, szMountPoint, ARRAYSIZE(szMountPoint)))
ExitGracefully(hr, E_FAIL, "Can't get mount point from storage medium");
PathAddBackslash(szMountPoint);
// Get the volume ID, which looks like
// "\\?\Volume{9e2df3f5-c7f1-11d1-84d5-000000000000}\"
if (!GetVolumeNameForVolumeMountPoint(szMountPoint, szVolumeID, ARRAYSIZE(szVolumeID)))
ExitGracefully(hr, E_FAIL, "GetVolumeNameForVolumeMountPoint failed");
if (GetVolumeInformation(szMountPoint, //szVolumeID,
szLabel,
ARRAYSIZE(szLabel),
NULL,
NULL,
&dwVolFlags,
NULL,
0))
{
if (dwVolFlags & FS_PERSISTENT_ACLS)
{
bHasSecurity = TRUE;
}
}
else if (GetLastError() == ERROR_ACCESS_DENIED)
{
// If we can't get the volume information because we don't have
// access, then there must be security!
bHasSecurity = TRUE;
}
if (!bHasSecurity)
ExitGracefully(hr, E_FAIL, "Volume inaccessible or not NTFS");
hToken = EnablePrivileges(dwPrivs, ARRAYSIZE(dwPrivs));
hr = CheckFileAccess(szVolumeID, &dwAccess);
FailGracefully(hr, "Volume inaccessible");
// If we can't do anything security related, don't continue.
if (!(dwAccess & ALL_SECURITY_ACCESS))
ExitGracefully(hr, E_ACCESSDENIED, "No security access");
if (!(dwAccess & WRITE_DAC))
m_dwSIFlags |= SI_READONLY;
if (!(dwAccess & WRITE_OWNER))
{
if (!(dwAccess & READ_CONTROL))
m_dwSIFlags &= ~SI_EDIT_OWNER;
else
m_dwSIFlags |= SI_OWNER_READONLY;
}
if (!(dwAccess & ACCESS_SYSTEM_SECURITY))
m_dwSIFlags &= ~SI_EDIT_AUDITS;
m_dwSIFlags |= SI_CONTAINER | SI_NO_ACL_PROTECT;
if (!FormatStringID(&m_pszObject,
g_hInstance,
IDS_FMT_VOLUME_DISPLAY,
szLabel,
szMountPoint))
{
LocalAllocString(&m_pszObject, szLabel);
}
if (!m_pszObject)
ExitGracefully(hr, E_OUTOFMEMORY, "Unable to build volume display string");
m_hItemList = DPA_Create(1);
if (!m_hItemList)
ExitGracefully(hr, E_OUTOFMEMORY, "Unable to create item list");
hr = LocalAllocString(&pszVolID, szVolumeID);
FailGracefully(hr, "Unable to copy volume ID string");
DPA_AppendPtr(m_hItemList, pszVolID);
pszVolID = NULL;
hr = AddSecurityPage(lpfnAddPage, lParam);
exit_gracefully:
ReleasePrivileges(hToken);
LocalFreeString(&pszVolID);
ReleaseStgMedium(&medium);
TraceLeaveResult(hr);
}
#endif // _WIN32_WINNT >= 0x0500
///////////////////////////////////////////////////////////////////////////////
// //
// Miscellaneous helper functions //
// //
///////////////////////////////////////////////////////////////////////////////
#if (_WIN32_WINNT < 0x0500)
#undef PathIsUNC
STDAPI_(BOOL)
PathIsUNC(LPCTSTR psz)
{
return (psz && psz[0] == TEXT('\\') && psz[1] == TEXT('\\'));
}
static const TCHAR c_szColonSlash[] = TEXT(":\\");
#undef PathIsRoot
STDAPI_(BOOL)
PathIsRoot(LPCTSTR pPath)
{
return (pPath && !lstrcmpi(pPath + 1, c_szColonSlash));
}
#endif // #if (_WIN32_WINNT < 0x0500)
BOOL
IsDfsPath(LPTSTR pszPath, // in
LPTSTR pszServer, // out
UINT cchServer, // in
LPTSTR pszAltPath) // out
{
BOOL bIsDfs = FALSE;
WCHAR szPath[MAX_PATH];
PDFS_INFO_3 pDI3 = NULL;
WCHAR szServer[MAX_PATH];
WCHAR szStorage[MAX_PATH];
USES_CONVERSION;
if (!PathIsUNC(pszPath))
return FALSE; // local machine
lstrcpynW(szPath, T2CW(pszPath), ARRAYSIZE(szPath));
// Check for DFS
for (;;)
{
DWORD dwErr;
__try
{
// This is delay-loaded by the linker, so
// must wrap with an exception handler.
dwErr = NetDfsGetClientInfo(szPath,
NULL,
NULL,
3,
(LPBYTE*)&pDI3);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}
if (NERR_Success == dwErr)
{
for (ULONG i = 0; i < pDI3->NumberOfStorages; i++)
{
if (DFS_STORAGE_STATE_ONLINE & pDI3->Storage[i].State)
{
bIsDfs = TRUE;
szServer[0] = L'\\';
szServer[1] = L'\\';
lstrcpynW(&szServer[2], pDI3->Storage[i].ServerName, ARRAYSIZE(szServer)-2);
lstrcpynW(szStorage, szServer, ARRAYSIZE(szStorage));
PathAppendW(szStorage, pDI3->Storage[i].ShareName);
// If this server is active, quit looking
if (DFS_STORAGE_STATE_ACTIVE & pDI3->Storage[i].State)
break;
}
}
break;
}
else if (NERR_DfsNoSuchVolume == dwErr)
{
// If we're at the root, then we can't go any farther.
if (PathIsRoot(szPath))
break;
// Remove the last path element and try again, if nothing is
//removed, break, don't go in infinite loop
if (!PathRemoveFileSpec(szPath))
break;
}
else
{
// Some other error, bail
break;
}
}
if (bIsDfs)
{
lstrcpyn(pszServer, W2T(szServer), cchServer);
// Note that EntryPath has only a single leading backslash, hence +1
LPCTSTR pszEnd = pszPath + lstrlen(pDI3->EntryPath) + 1;
while (TEXT('\\') == *pszEnd)
pszEnd++;
PathCombine(pszAltPath, szStorage, pszEnd);
}
if (NULL != pDI3)
NetApiBufferFree(pDI3);
return bIsDfs;
}
void
GetVolumeInfo(LPCTSTR pszPath,
BOOL bIsFolder,
LPDWORD pdwFlags,
LPTSTR pszVolume,
ULONG cchVolume)
{
TCHAR szVolume[MAX_PATH];
TCHAR szVolumeID[MAX_PATH];
//
// The path can be DFS or contain volume mount points, so start
// with the full path and try GetVolumeInformation on successively
// shorter paths until it succeeds or we run out of path.
//
// However, if it's a volume mount point, we're interested in the
// the host folder's volume so back up one level to start. The
// child volume is handled separately (see AddMountedVolumePage).
//
lstrcpyn(szVolume, pszPath, ARRAYSIZE(szVolume));
if (!bIsFolder
|| GetVolumeNameForVolumeMountPoint(szVolume, szVolumeID, ARRAYSIZE(szVolumeID)))
{
PathRemoveFileSpec(szVolume);
}
for (;;)
{
PathAddBackslash(szVolume); // GetVolumeInformation likes a trailing '\'
if (GetVolumeInformation(szVolume,
NULL,
NULL,
NULL,
NULL,
pdwFlags,
NULL,
0))
{
break;
}
// Access denied implies that we've reached the deepest volume
// in the path; we just can't get the flags. It also implies
// security, so assume persistent acls.
if (ERROR_ACCESS_DENIED == GetLastError())
{
*pdwFlags = FS_PERSISTENT_ACLS;
break;
}
// If we're at the root, then we can't go any farther.
if (PathIsRoot(szVolume))
break;
// Remove the last path element and try again
PathRemoveBackslash(szVolume);
//if nothing is removed break instead of going in infinite loop
if (!PathRemoveFileSpec(szVolume))
break;
}
if (pszVolume)
{
PathRemoveBackslash(szVolume);
lstrcpyn(pszVolume, szVolume, cchVolume);
}
}
void
GetFileInfo(LPCTSTR pszPath,
LPDWORD pdwFileType,
LPTSTR pszServer,
ULONG cchServer,
LPTSTR *ppszAlternatePath)
{
DWORD dwVolumeFlags = 0;
TCHAR szVolume[MAX_PATH];
LPTSTR pszUNC = NULL;
TraceEnter(TRACE_RSHX32, "GetFileInfo");
TraceAssert(NULL != pszServer);
pszServer[0] = TEXT('\0');
if (ppszAlternatePath)
*ppszAlternatePath = NULL;
if (!PathIsUNC(pszPath) && S_OK == GetRemotePath(pszPath, &pszUNC))
pszPath = pszUNC;
if (PathIsRoot(pszPath))
*pdwFileType |= DOBJ_RES_ROOT;
GetVolumeInfo(pszPath,
*pdwFileType & DOBJ_RES_CONT,
&dwVolumeFlags,
szVolume,
ARRAYSIZE(szVolume));
if (dwVolumeFlags & FS_PERSISTENT_ACLS)
{
TCHAR szAltVolume[MAX_PATH];
*pdwFileType |= DOBJ_VOL_NTACLS;
if (IsDfsPath(szVolume, pszServer, cchServer, szAltVolume))
{
if (ppszAlternatePath)
{
LPCTSTR pszEnd = pszPath + lstrlen(szVolume);
while (TEXT('\\') == *pszEnd)
pszEnd++;
*ppszAlternatePath = (LPTSTR)LocalAlloc(LPTR, StringByteSize(szAltVolume)
+ StringByteSize(pszEnd));
if (*ppszAlternatePath)
{
// PathCombine asserts that the buffer is at least MAX_PATH,
// so don't use it here. We know the buffer is big enough.
lstrcpy(*ppszAlternatePath, szAltVolume);
LPTSTR pszT = PathAddBackslash(*ppszAlternatePath);
if (pszT)
lstrcpy(pszT, pszEnd);
//PathCombine(*ppszAlternatePath, szAltVolume, pszEnd);
}
}
}
else if (PathIsUNC(szVolume))
{
LPTSTR pSlash = StrChr(&szVolume[2], TEXT('\\'));
if (pSlash)
cchServer = min(cchServer, (ULONG)(pSlash - szVolume) + 1);
lstrcpyn(pszServer, szVolume, cchServer);
}
}
LocalFreeString(&pszUNC);
TraceLeaveVoid();
}