// Authors; // Jeff Saathoff (jeffreys) // // Notes; // Context Menu and Property Sheet shell extensions #include "pch.h" #include "options.h" // ..\viewer\options.h #include "firstpin.h" #include "msgbox.h" #include "strings.h" #include "nopin.h" #include #define CSC_PROP_NO_CSC 0x00000001L #define CSC_PROP_MULTISEL 0x00000002L #define CSC_PROP_PINNED 0x00000004L #define CSC_PROP_SYNCABLE 0x00000008L #define CSC_PROP_ADMIN_PINNED 0x00000010L #define CSC_PROP_INHERIT_PIN 0x00000020L #define CSC_PROP_DCON_MODE 0x00000040L // Thread data for unpinning files typedef struct { CscFilenameList *pNamelist; DWORD dwUpdateFlags; HWND hwndOwner; BOOL bOffline; } CSC_UNPIN_DATA; /////////////////////////////////////////////////////////////////////////////// // // // Shell extension object implementation // // // /////////////////////////////////////////////////////////////////////////////// STDAPI CCscShellExt::CreateInstance(REFIID riid, LPVOID *ppv) { HRESULT hr; CCscShellExt *pThis = new CCscShellExt; if (pThis) { hr = pThis->QueryInterface(riid, ppv); pThis->Release(); // release initial ref } else hr = E_OUTOFMEMORY; return hr; } /////////////////////////////////////////////////////////////////////////////// // // // Shell extension object implementation (IUnknown) // // // /////////////////////////////////////////////////////////////////////////////// STDMETHODIMP CCscShellExt::QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { QITABENT(CCscShellExt, IShellExtInit), QITABENT(CCscShellExt, IContextMenu), QITABENT(CCscShellExt, IShellIconOverlayIdentifier), { 0 }, }; return QISearch(this, qit, riid, ppv); } STDMETHODIMP_(ULONG) CCscShellExt::AddRef() { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) CCscShellExt::Release() { ASSERT( 0 != m_cRef ); ULONG cRef = InterlockedDecrement(&m_cRef); if ( 0 == cRef ) { delete this; } return cRef; } // IShellExtInit STDMETHODIMP CCscShellExt::Initialize(LPCITEMIDLIST /*pidlFolder*/, IDataObject *pdobj, HKEY /*hKeyProgID*/) { IUnknown_Set((IUnknown **)&m_lpdobj, pdobj); return S_OK; } // IContextMenu // // 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 // iMenu - 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. // STDMETHODIMP CCscShellExt::QueryContextMenu(HMENU hMenu, UINT iMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags) { HRESULT hr = ResultFromShort(0); UINT idCmd = idCmdFirst; TCHAR szMenu[MAX_PATH]; MENUITEMINFO mii; CConfig& config = CConfig::GetSingleton(); if ((uFlags & (CMF_DEFAULTONLY | CMF_VERBSONLY)) || !m_lpdobj) return hr; TraceEnter(TRACE_SHELLEX, "CCscShellExt::QueryContextMenu"); TraceAssert(IsCSCEnabled()); // // Check the pin status and CSC-ability of the current selection // m_dwUIStatus = 0; if (FAILED(CheckFileStatus(m_lpdobj, &m_dwUIStatus))) m_dwUIStatus = CSC_PROP_NO_CSC; if (m_dwUIStatus & CSC_PROP_NO_CSC) TraceLeaveResult(hr); // // Add a menu separator // mii.cbSize = sizeof(mii); mii.fMask = MIIM_TYPE; mii.fType = MFT_SEPARATOR; InsertMenuItem(hMenu, iMenu++, TRUE, &mii); if (!config.NoMakeAvailableOffline()) { if (SUCCEEDED(hr = CanAllFilesBePinned(m_lpdobj))) { if (S_OK == hr) { mii.fState = MFS_ENABLED; // All files in selection can be pinned. } else { mii.fState = MFS_DISABLED; // 1+ files in selection cannot be pinned. } // // Add the "Make Available Offline" menu item // LoadString(g_hInstance, IDS_MENU_PIN, szMenu, ARRAYSIZE(szMenu)); mii.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID; mii.fType = MFT_STRING; if (m_dwUIStatus & (CSC_PROP_ADMIN_PINNED | CSC_PROP_PINNED)) { mii.fState = MFS_CHECKED; if (m_dwUIStatus & (CSC_PROP_ADMIN_PINNED | CSC_PROP_INHERIT_PIN)) mii.fState |= MFS_DISABLED; } mii.wID = idCmd++; mii.dwTypeData = szMenu; InsertMenuItem(hMenu, iMenu++, TRUE, &mii); } } if (m_dwUIStatus & (CSC_PROP_SYNCABLE | CSC_PROP_PINNED | CSC_PROP_ADMIN_PINNED)) { // // Add the "Synchronize" menu item // LoadString(g_hInstance, IDS_MENU_SYNCHRONIZE, szMenu, ARRAYSIZE(szMenu)); mii.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID; mii.fType = MFT_STRING; mii.fState = MFS_ENABLED; mii.wID = idCmd++; mii.dwTypeData = szMenu; InsertMenuItem(hMenu, iMenu++, TRUE, &mii); } // // Return the number of menu items we added. // hr = ResultFromShort(idCmd - idCmdFirst); 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 CCscShellExt::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi) { HRESULT hr = S_OK; UINT iCmd = 0; CscFilenameList *pfnl = NULL; // Namelist object. BOOL fPin; BOOL bSubFolders = FALSE; DWORD dwUpdateFlags = 0; TraceEnter(TRACE_SHELLEX, "CCscShellExt::InvokeCommand"); TraceAssert(IsCSCEnabled()); TraceAssert(!(m_dwUIStatus & CSC_PROP_NO_CSC)); if (HIWORD(lpcmi->lpVerb)) { if (!lstrcmpiA(lpcmi->lpVerb, STR_PIN_VERB)) { iCmd = 0; m_dwUIStatus &= ~CSC_PROP_PINNED; } else if (!lstrcmpiA(lpcmi->lpVerb, STR_UNPIN_VERB)) { iCmd = 0; m_dwUIStatus |= CSC_PROP_PINNED; } else if (!lstrcmpiA(lpcmi->lpVerb, STR_SYNC_VERB)) { iCmd = 1; } else { Trace((TEXT("Unknown command \"%S\""), lpcmi->lpVerb)); ExitGracefully(hr, E_INVALIDARG, "Invalid command"); } } else { iCmd = LOWORD(lpcmi->lpVerb); // If we didn't add the "Make Available Offline" verb, adjust the index if (CConfig::GetSingleton().NoMakeAvailableOffline()) iCmd++; } if (iCmd >= 2) ExitGracefully(hr, E_INVALIDARG, "Invalid command"); pfnl = new CscFilenameList; if (!pfnl) ExitGracefully(hr, E_OUTOFMEMORY, "Unable to create CscFilenameList object"); hr = BuildFileList(m_lpdobj, lpcmi->hwnd, pfnl, &bSubFolders); FailGracefully(hr, "Unable to build file list"); switch (iCmd) { case 0: // "Make available offline" menu choice - Pin files if (!FirstPinWizardCompleted()) { // // User has never seen the "first pin" wizard. // if (S_FALSE == ShowFirstPinWizard(lpcmi->hwnd)) { // // User cancelled wizard. Abort pinning operation. // ExitGracefully(hr, S_OK, "User cancelled first-pin wizard"); } } fPin = !(m_dwUIStatus & CSC_PROP_PINNED); if (!fPin && (m_dwUIStatus & CSC_PROP_DCON_MODE)) { // Unpin while disconnected causes things to disappear. // Warn the user. if (IDCANCEL == CscMessageBox(lpcmi->hwnd, MB_OKCANCEL | MB_ICONWARNING, g_hInstance, IDS_CONFIRM_UNPIN_OFFLINE)) { ExitGracefully(hr, E_FAIL, "User cancelled disconnected unpin operation"); } } // If there is a directory in the list AND we're pinning AND // the "AlwaysPinSubFolders" policy is NOT set, ask the user // whether to go deep or not. // If the policy IS set we automatically do a recursive pin. if (bSubFolders && (!fPin || !CConfig::GetSingleton().AlwaysPinSubFolders())) { switch (DialogBox(g_hInstance, MAKEINTRESOURCE(fPin ? IDD_CONFIRM_PIN : IDD_CONFIRM_UNPIN), lpcmi->hwnd, _ConfirmPinDlgProc)) { case IDYES: // nothing break; case IDNO: bSubFolders = FALSE; // no subfolders break; case IDCANCEL: ExitGracefully(hr, E_FAIL, "User cancelled (un)pin operation"); break; } } if (bSubFolders) dwUpdateFlags |= CSC_UPDATE_PIN_RECURSE; if (fPin) { // Set the flags for pin + quick sync dwUpdateFlags |= CSC_UPDATE_SELECTION | CSC_UPDATE_STARTNOW | CSC_UPDATE_PINFILES | CSC_UPDATE_FILL_QUICK; } else { HANDLE hThread; DWORD dwThreadID; CSC_UNPIN_DATA *pUnpinData = (CSC_UNPIN_DATA *)LocalAlloc(LPTR, sizeof(*pUnpinData)); // // No sync is required to unpin files, so let's do it in this // process rather than starting SyncMgr. However, let's do // it in the background in case there's a lot to unpin. // if (pUnpinData) { pUnpinData->pNamelist = pfnl; pUnpinData->dwUpdateFlags = dwUpdateFlags; pUnpinData->hwndOwner = lpcmi->hwnd; pUnpinData->bOffline = !!(m_dwUIStatus & CSC_PROP_DCON_MODE); hThread = CreateThread(NULL, 0, _UnpinFilesThread, pUnpinData, 0, &dwThreadID); if (hThread) { // The thread will delete pUnpinData and pUnpinData->pNamelist pfnl = NULL; // We give the async thread a little time to complete, during which we // put up the busy cursor. This is solely to let the user see that // some work is being done... HCURSOR hCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); WaitForSingleObject(hThread, 750); CloseHandle(hThread); SetCursor(hCur); } else { LocalFree(pUnpinData); } } // Clear the flags to prevent sync below dwUpdateFlags = 0; } break; case 1: // Synchronize // Set the flags for a full sync dwUpdateFlags = CSC_UPDATE_SELECTION | CSC_UPDATE_STARTNOW | CSC_UPDATE_REINT | CSC_UPDATE_FILL_ALL | CSC_UPDATE_SHOWUI_ALWAYS | CSC_UPDATE_NOTIFY_DONE; break; } // // Update the files we are pinning or synchronizing. // Setting the "ignore access" flag will cause us to ignore the // user/guest/other access info and sync all selected files. We want // this behavior as the operation was initiated by a user's explicit // selection of files/folders in explorer. // if (dwUpdateFlags && pfnl->GetFileCount()) { if (!::IsSyncInProgress()) { hr = CscUpdateCache(dwUpdateFlags | CSC_UPDATE_IGNORE_ACCESS, pfnl); } else { // // A sync is in progress. Tell user why they can't currently // pin or sync. // const UINT rgidsMsg[] = { IDS_CANTPIN_SYNCINPROGRESS, IDS_CANTSYNC_SYNCINPROGRESS }; CscMessageBox(lpcmi->hwnd, MB_OK | MB_ICONINFORMATION, g_hInstance, rgidsMsg[iCmd]); } } exit_gracefully: delete pfnl; 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 CCscShellExt::GetCommandString(UINT_PTR iCmd, UINT uFlags, LPUINT /*reserved*/, LPSTR pszString, UINT cchMax) { HRESULT hr = E_UNEXPECTED; if (uFlags == GCS_VALIDATE) hr = S_FALSE; if (iCmd > 1) return hr; hr = S_OK; if (uFlags == GCS_HELPTEXT) { if (0 == LoadString(g_hInstance, iCmd ? IDS_HELP_UPDATE_SEL : IDS_HELP_PIN, (LPTSTR)pszString, cchMax)) { hr = ResultFromLastError(); } } else if (uFlags == GCS_VERB) { hr = StringCchCopy((LPTSTR)pszString, cchMax, iCmd ? TEXT(STR_SYNC_VERB) : ((m_dwUIStatus & CSC_PROP_PINNED) ? TEXT(STR_UNPIN_VERB) : TEXT(STR_PIN_VERB))); } else if (uFlags != GCS_VALIDATE) { // Must be some other flag that we don't handle hr = E_NOTIMPL; } return hr; } /////////////////////////////////////////////////////////////////////////////// // // // Shell extension object implementation (IShellIconOverlayIdentifier) // // // /////////////////////////////////////////////////////////////////////////////// STDMETHODIMP CCscShellExt::IsMemberOf(LPCWSTR pwszPath, DWORD dwAttrib) { HRESULT hr = S_FALSE; // assume not pinned DWORD dwHintFlags; DWORD dwErr; LPTSTR pszUNC = NULL; LPTSTR pszSlash; // // Make sure we have a UNC path // GetRemotePath(pwszPath, &pszUNC); if (!pszUNC) return S_FALSE; // // Ask CSC if this is a pinned file // dwHintFlags = 0; if (CSCQueryFileStatus(pszUNC, NULL, NULL, &dwHintFlags)) { if (dwHintFlags & (FLAG_CSC_HINT_PIN_USER | FLAG_CSC_HINT_PIN_ADMIN)) hr = S_OK; } else { dwErr = GetLastError(); if (ERROR_FILE_NOT_FOUND != dwErr) { // // Need to check for 0 to accomodate GetLastError // returning 0 on CSCQueryFileStatus failure. // I'll talk to Shishir about getting this fixed. // [brianau - 5/13/99] // // Most of these were fixed for Windows 2000. If we // hit the assertion below we should file a bug and // get Shishir to fix it. // [jeffreys - 1/24/2000] // if (0 == dwErr) { ASSERTMSG(FALSE, "CSCQueryFileStatus failed with error = 0"); dwErr = ERROR_GEN_FAILURE; } hr = HRESULT_FROM_WIN32(dwErr); } } DWORD dwAttribTest = FILE_ATTRIBUTE_ENCRYPTED; if (!CConfig::GetSingleton().AlwaysPinSubFolders()) dwAttribTest |= FILE_ATTRIBUTE_DIRECTORY; if (S_FALSE == hr && !(dwAttrib & dwAttribTest)) { // // Check to see if pinning is disallowed by system policy. // hr = m_NoPinList.IsPinAllowed(pszUNC); if (S_OK == hr) { hr = S_FALSE; // Reset // // If we get here, then either CSCQueryFileStatus succeeded but the file // isn't pinned, or the file isn't in the cache (ERROR_FILE_NOT_FOUND). // Also, policy allows pinning of this file/folder. // // Check whether the parent folder has the pin-inherit-user or // admin-pin flag and pin this file if necessary. // // Note that we don't pin encrypted files here. // Also note that pinning of folder is policy-dependent. The default // behavior is to NOT pin folders (only files). If the // "AlwaysPinSubFolders" policy is set, we will pin folders. // pszSlash = PathFindFileName(pszUNC); if (pszSlash && pszUNC != pszSlash) { --pszSlash; *pszSlash = TEXT('\0'); // truncate the path // Check the parent status if (CSCQueryFileStatus(pszUNC, NULL, NULL, &dwHintFlags) && (dwHintFlags & (FLAG_CSC_HINT_PIN_USER | FLAG_CSC_HINT_PIN_ADMIN))) { // The parent is pinned, so pin this file with the same flags if (dwHintFlags & FLAG_CSC_HINT_PIN_USER) dwHintFlags |= FLAG_CSC_HINT_PIN_INHERIT_USER; // Restore the rest of the path *pszSlash = TEXT('\\'); // // To avoid a nasty race condition between purging and auto-pinning we need // to disable auto-pinning when a purge is in progress. The race condition // can occur if a shell folder for the files being purged is open. We purge // a file and send out a change notify. The shell updates the icon overlay // and calls our overlay handler to remove the overlay. Our handler notices // that the parent folder is pinned so we re-pin the file which places it // back in the cache. Ugh... [brianau - 11/01/99] // // p.s.: Note that this check calls WaitForSingleObject so we only // do it AFTER we're sure that we want to pin the file. We don't // want to do the "wait" and THEN decide the file should not be // pinned because it's not a UNC path or it's a directory. // if (!IsPurgeInProgress()) { if (CSCPinFile(pszUNC, dwHintFlags, NULL, NULL, NULL)) hr = S_OK; } } } } } LocalFreeString(&pszUNC); return hr; } STDMETHODIMP CCscShellExt::GetOverlayInfo (LPWSTR pwszIconFile, int cchMax, int * pIndex, DWORD * pdwFlags) { // Use positive #'s for indexes, negative for ID's (NT only) *pIndex = -IDI_PIN_OVERLAY; *pdwFlags = (ISIOI_ICONFILE | ISIOI_ICONINDEX); return StringCchCopy(pwszIconFile, cchMax, c_szDllName); } STDMETHODIMP CCscShellExt::GetPriority (int * pIPriority) { *pIPriority = 1; return S_OK; } /////////////////////////////////////////////////////////////////////////////// // // // CCscShellExt implementation // // // /////////////////////////////////////////////////////////////////////////////// BOOL ShareIsCacheable(LPCTSTR pszUNC, BOOL bPathIsFile, LPTSTR *ppszConnectionName, PDWORD pdwShareStatus) { TCHAR szShare[MAX_PATH]; DWORD dwShareStatus = FLAG_CSC_SHARE_STATUS_NO_CACHING; *ppszConnectionName = NULL; // CSCQueryFileStatus can fail for multiple reasons, one of which is that // there is no database entry and no existing SMB connection to the share. // To handle the no-connection part, we try to connect to the share and // retry CSCQueryFileStatus. // // However, there may be a non-SMB connnection which the SMB RDR doesn't // know about, so we have to check for a connection first. If there is a // non-SMB connection and we connect again, we would end up disconnecting // the pre-existing connection later, since we think we made the connection. // // If there is a non-SMB connection, then caching is not possible. // // Note that we can get here without a connection in at least 3 ways: // 1. When exploring on \\server and the context menu is for \\server\share. // 2. When checking a link target, which may live on a different server // than what we're exploring. // 3. When checking a folder which is a DFS junction (we need to connect // to the 'child' share). // Use a deep path to get correct results in DFS scenarios, but strip the // filename if it's not a directory. if (SUCCEEDED(StringCchCopy(szShare, ARRAYSIZE(szShare), pszUNC))) { if (bPathIsFile) { PathRemoveFileSpec(szShare); } // CSCQueryShareStatus is currently unable to return permissions in // some cases (e.g. DFS), so don't use the permission parameters. if (!CSCQueryShareStatus(szShare, &dwShareStatus, NULL, NULL, NULL, NULL)) { if (!ShareIsConnected(szShare) && ConnectShare(szShare, ppszConnectionName)) { if (!CSCQueryShareStatus(szShare, &dwShareStatus, NULL, NULL, NULL, NULL)) { dwShareStatus = FLAG_CSC_SHARE_STATUS_NO_CACHING; // We're going to return FALSE; kill the connection if (*ppszConnectionName) { WNetCancelConnection2(*ppszConnectionName, 0, FALSE); LocalFreeString(ppszConnectionName); } } } else { dwShareStatus = FLAG_CSC_SHARE_STATUS_NO_CACHING; } } } *pdwShareStatus = dwShareStatus; return !((dwShareStatus & FLAG_CSC_SHARE_STATUS_CACHING_MASK) == FLAG_CSC_SHARE_STATUS_NO_CACHING); } BOOL IsSameServer(LPCTSTR pszUNC, LPCTSTR pszServer) { ULONG nLen; LPTSTR pszSlash; pszUNC += 2; // Skip leading backslashes pszSlash = StrChr(pszUNC, TEXT('\\')); if (pszSlash) nLen = (ULONG)(pszSlash - pszUNC); else nLen = lstrlen(pszUNC); return (CSTR_EQUAL == CompareString(LOCALE_SYSTEM_DEFAULT, NORM_IGNORECASE, pszUNC, nLen, pszServer, -1)); } STDMETHODIMP CCscShellExt::CheckOneFileStatus(LPCTSTR pszItem, DWORD dwAttr, // SFGAO_* flags BOOL bShareChecked, LPDWORD pdwStatus) // CSC_PROP_* flags { HRESULT hr = S_OK; LPTSTR pszConnectionName = NULL; DWORD dwHintFlags = 0; TraceEnter(TRACE_SHELLEX, "CCscShellExt::CheckOneFileStatus"); TraceAssert(pszItem && *pszItem); TraceAssert(pdwStatus); if (!PathIsUNC(pszItem)) ExitGracefully(hr, HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME), "Not a network path"); // If server is local machine, fail. Don't allow someone to // cache a local path via a net share. if (IsSameServer(pszItem, m_szLocalMachine)) ExitGracefully(hr, HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME), "Locally redirected path"); // Check whether the share is cacheable // To handle DFS correctly, we need to re-check share status for folders, // since they may be DFS junction points and have different cache settings. if (!bShareChecked || (dwAttr & SFGAO_FOLDER)) { DWORD dwShareStatus = 0; if (!ShareIsCacheable(pszItem, !(dwAttr & SFGAO_FOLDER), &pszConnectionName, &dwShareStatus)) ExitGracefully(hr, E_FAIL, "Share not cacheable"); if (dwShareStatus & FLAG_CSC_SHARE_STATUS_DISCONNECTED_OP) *pdwStatus |= CSC_PROP_DCON_MODE; } // Check the file status if (!CSCQueryFileStatus(pszItem, NULL, NULL, &dwHintFlags)) { DWORD dwErr = GetLastError(); if (dwErr != ERROR_FILE_NOT_FOUND) { if (NO_ERROR == dwErr) dwErr = ERROR_GEN_FAILURE; ExitGracefully(hr, HRESULT_FROM_WIN32(dwErr), "CSCQueryFileStatus failed"); } } else { if (dwAttr & SFGAO_FOLDER) { // CSCQueryFileStatus succeeded, so this folder is in the cache. // Enable the sync menu. if (PathIsRoot(pszItem)) { // Special note for "\\server\share" items: CSCQueryFileStatus // can succeed even if nothing on the share is cached. Only // enable CSC_PROP_SYNCABLE if something on this share is cached. CSCSHARESTATS shareStats; CSCGETSTATSINFO si = { SSEF_NONE, // No exclusions SSUF_TOTAL, // Interested in total only. false, // No access info reqd (faster). false }; _GetShareStatisticsForUser(pszItem, &si, &shareStats); if (shareStats.cTotal) *pdwStatus |= CSC_PROP_SYNCABLE; } else { *pdwStatus |= CSC_PROP_SYNCABLE; } } const bool bPinSubFolders = CConfig::GetSingleton().AlwaysPinSubFolders(); if (!(*pdwStatus & CSC_PROP_INHERIT_PIN) && (!(dwAttr & SFGAO_FOLDER) || bPinSubFolders)) { TCHAR szParent[MAX_PATH]; DWORD dwParentHints = 0; // It's a file OR it's a folder and the "AlwaysPinSubFolders" // policy is set.. Check whether the parent is pinned. if (SUCCEEDED(StringCchCopy(szParent, ARRAYSIZE(szParent), pszItem)) && PathRemoveFileSpec(szParent) && CSCQueryFileStatus(szParent, NULL, NULL, &dwParentHints) && (dwParentHints & FLAG_CSC_HINT_PIN_USER)) { *pdwStatus |= CSC_PROP_INHERIT_PIN; } } } // If it's not pinned, turn off pinned flag if (0 == (dwHintFlags & FLAG_CSC_HINT_PIN_USER)) *pdwStatus &= ~CSC_PROP_PINNED; // If it's not admin pinned, turn off admin pinned flag if (0 == (dwHintFlags & FLAG_CSC_HINT_PIN_ADMIN)) *pdwStatus &= ~CSC_PROP_ADMIN_PINNED; exit_gracefully: if (pszConnectionName) { WNetCancelConnection2(pszConnectionName, 0, FALSE); LocalFreeString(&pszConnectionName); } TraceLeaveResult(hr); } BOOL _PathIsUNCServer(LPCTSTR pszPath) { int i; if (!pszPath) return FALSE; for (i = 0; *pszPath; pszPath++ ) { if (pszPath[0]==TEXT('\\') && pszPath[1]) // don't count a trailing slash { i++; } } return (i == 2); } STDMETHODIMP CCscShellExt::CheckFileStatus(IDataObject *pdobj, DWORD *pdwStatus) // CSC_PROP_* flags { LPTSTR pszConnectionName = NULL; UINT i; BOOL bShareOK = FALSE; TCHAR szItem[MAX_PATH]; CIDArray ida; TraceEnter(TRACE_SHELLEX, "CCscShellExt::CheckFileStatus"); TraceAssert(pdobj != NULL); TraceAssert(IsCSCEnabled()); if (pdwStatus) *pdwStatus = 0; // Assume that everything is both user and system pinned. If anything // is not pinned, clear the appropriate flag and treat the entire // selection as non-pinned. DWORD dwStatus = CSC_PROP_PINNED | CSC_PROP_ADMIN_PINNED; HRESULT hr = ida.Initialize(pdobj); FailGracefully(hr, "Can't get ID List format from data object"); if (ida.Count() > 1) dwStatus |= CSC_PROP_MULTISEL; // Check the parent path hr = ida.GetFolderPath(szItem, ARRAYSIZE(szItem)); FailGracefully(hr, "No parent path"); if (ida.Count() > 1 && PathIsUNC(szItem) && !_PathIsUNCServer(szItem)) { DWORD dwShareStatus = 0; if (!ShareIsCacheable(szItem, FALSE, &pszConnectionName, &dwShareStatus)) ExitGracefully(hr, E_FAIL, "Share not cacheable"); if (dwShareStatus & FLAG_CSC_SHARE_STATUS_DISCONNECTED_OP) dwStatus |= CSC_PROP_DCON_MODE; // No need to check share status again inside CheckOneFileStatus bShareOK = TRUE; } // Loop over each selected item for (i = 0; i < ida.Count(); i++) { // Get the attributes DWORD dwAttr = SFGAO_FILESYSTEM | SFGAO_LINK | SFGAO_FOLDER; hr = ida.GetItemPath(i, szItem, ARRAYSIZE(szItem), &dwAttr); FailGracefully(hr, "Unable to get item attributes"); if (!(dwAttr & SFGAO_FILESYSTEM)) ExitGracefully(hr, E_FAIL, "Not a filesystem object"); // Is it a shortcut? if (dwAttr & SFGAO_LINK) { LPTSTR pszTarget = NULL; // Check the target GetLinkTarget(szItem, &pszTarget); if (pszTarget) { hr = CheckOneFileStatus(pszTarget, 0, FALSE, &dwStatus); LocalFreeString(&pszTarget); if (SUCCEEDED(hr) && !PathIsUNC(szItem)) { // The link is local, but the target is remote, so don't // bother checking status of the link itself. Just go // with the target status and move on to the next item. continue; } } } hr = CheckOneFileStatus(szItem, dwAttr, bShareOK, &dwStatus); FailGracefully(hr, "File not cacheable"); } exit_gracefully: if (pszConnectionName) { WNetCancelConnection2(pszConnectionName, 0, FALSE); LocalFreeString(&pszConnectionName); } if (SUCCEEDED(hr) && pdwStatus != NULL) *pdwStatus = dwStatus; TraceLeaveResult(hr); } // // Determines if a folder has subfolders. // Returns: // S_OK = Has subfolders. // S_FALSE = No subfolders. // E_OUTOFMEMORY = Insufficient memory. // HRESULT CCscShellExt::FolderHasSubFolders( LPCTSTR pszPath, CscFilenameList *pfnl ) { if (NULL == pszPath || TEXT('\0') == *pszPath) return E_INVALIDARG; HRESULT hr = S_FALSE; size_t cchBuffer = lstrlen(pszPath) + 1 + MAX_PATH; LPTSTR pszTemp = (LPTSTR)LocalAlloc(LPTR, cchBuffer * sizeof(TCHAR)); if (NULL != pszTemp) { LPTSTR pszEnd; // We allocated more than enough to hold pszPath + "\\*", so // this should never fail. StringCchCopyEx(pszTemp, cchBuffer, pszPath, &pszEnd, &cchBuffer, 0); ASSERT(pszEnd > pszTemp && *(pszEnd-1) != TEXT('\\')); StringCchCopy(pszEnd, cchBuffer, TEXT("\\*")); pszEnd++; // move past '\\' cchBuffer--; WIN32_FIND_DATA fd; HANDLE hFind = FindFirstFile(pszTemp, &fd); if (INVALID_HANDLE_VALUE != hFind) { do { if ((FILE_ATTRIBUTE_DIRECTORY & fd.dwFileAttributes) && !PathIsDotOrDotDot(fd.cFileName)) { if (IsHiddenSystem(fd.dwFileAttributes)) { // This subfolder is "super hidden". Build the full path // and silently add it to the file list, but don't set the // result to S_OK (we don't want superhidden subfolders to // cause prompts). if (SUCCEEDED(StringCchCopy(pszEnd, cchBuffer, fd.cFileName))) { pfnl->AddFile(pszTemp, true); } } else hr = S_OK; // don't break, there may be superhidden folders } } while(FindNextFile(hFind, &fd)); FindClose(hFind); } else { hr = HRESULT_FROM_WIN32(GetLastError()); } LocalFree(pszTemp); } else { hr = E_OUTOFMEMORY; } return hr; } STDMETHODIMP CCscShellExt::BuildFileList(IDataObject *pdobj, HWND hwndOwner, CscFilenameList *pfnl, LPBOOL pbSubFolders) { UINT i; TCHAR szItem[MAX_PATH]; CIDArray ida; BOOL bDirectory; TraceEnter(TRACE_SHELLEX, "CCscShellExt::BuildFileList"); TraceAssert(pdobj != NULL); TraceAssert(pfnl != NULL); HCURSOR hCur = SetCursor(LoadCursor(NULL, IDC_WAIT)); HRESULT hr = ida.Initialize(pdobj); FailGracefully(hr, "Can't get ID List format from data object"); // Loop over each selected item for (i = 0; i < ida.Count(); i++) { // Get the attributes DWORD dwAttr = SFGAO_FILESYSTEM | SFGAO_LINK | SFGAO_FOLDER; hr = ida.GetItemPath(i, szItem, ARRAYSIZE(szItem), &dwAttr); FailGracefully(hr, "Unable to get item attributes"); if (!(dwAttr & SFGAO_FILESYSTEM)) continue; // Is it a shortcut? if (dwAttr & SFGAO_LINK) { LPTSTR pszTarget = NULL; // Check the target GetLinkTarget(szItem, &pszTarget); if (pszTarget) { // Add the target to the file list if (!pfnl->FileExists(pszTarget, false)) pfnl->AddFile(pszTarget, false); LocalFreeString(&pszTarget); } } bDirectory = (dwAttr & SFGAO_FOLDER); if (pbSubFolders && bDirectory && !*pbSubFolders) *pbSubFolders = (S_OK == FolderHasSubFolders(szItem, pfnl)); // Add the item to the file list pfnl->AddFile(szItem, !!bDirectory); // If it's an html file, look for a directory of the same name // and add it to the file list if necessary. // // We're supposed to look for a localized version of "Files" // tacked on to the root name. For example, given "foo.htm" we // should look for a directory named "foo Files" where the "Files" // part comes from a list of localized strings provided by Office. // We don't bother and just look for a directory named "foo". // if (!bDirectory && PathIsHTMLFile(szItem)) { // Truncate the path LPTSTR pszExtn = PathFindExtension(szItem); if (pszExtn) *pszExtn = NULL; // Check for existence dwAttr = GetFileAttributes(szItem); if ((DWORD)-1 != dwAttr && (dwAttr & FILE_ATTRIBUTE_DIRECTORY)) pfnl->AddFile(szItem, true); } } exit_gracefully: SetCursor(hCur); TraceLeaveResult(hr); } #define _WNET_ENUM_BUFFER_SIZE 4000 BOOL ShareIsConnected(LPCTSTR pszUNC) { HANDLE hEnum; PVOID pBuffer; BOOL fShareIsConnected = FALSE; pBuffer = (PVOID)LocalAlloc(LMEM_FIXED, _WNET_ENUM_BUFFER_SIZE); if (NULL != pBuffer) { // // Enumerate all connected disk resources // if (NO_ERROR == WNetOpenEnum(RESOURCE_CONNECTED, RESOURCETYPE_DISK, 0, NULL, &hEnum)) { // // Look at each connected share. If we find the share we're looking for, // we know it's connected so we can quit looking. // while (!fShareIsConnected) { LPNETRESOURCE pnr; DWORD cEnum = (DWORD)-1; DWORD dwBufferSize = _WNET_ENUM_BUFFER_SIZE; if (NO_ERROR != WNetEnumResource(hEnum, &cEnum, pBuffer, &dwBufferSize)) break; for (pnr = (LPNETRESOURCE)pBuffer; cEnum > 0; cEnum--, pnr++) { if (NULL != pnr->lpRemoteName && 0 == lstrcmpi(pnr->lpRemoteName, pszUNC)) { // Found it fShareIsConnected = TRUE; break; } } } WNetCloseEnum(hEnum); } LocalFree(pBuffer); } return fShareIsConnected; } BOOL ConnectShare(LPCTSTR pszUNC, LPTSTR *ppszAccessName) { NETRESOURCE nr; DWORD dwResult; DWORD dwErr; TCHAR szAccessName[MAX_PATH]; DWORD cchAccessName = ARRAYSIZE(szAccessName); TraceEnter(TRACE_SHELLEX, "CCscShellExt::ConnectShare"); TraceAssert(pszUNC && *pszUNC); nr.dwType = RESOURCETYPE_DISK; nr.lpLocalName = NULL; nr.lpRemoteName = (LPTSTR)pszUNC; nr.lpProvider = NULL; szAccessName[0] = TEXT('\0'); dwErr = WNetUseConnection(NULL, &nr, NULL, NULL, 0, szAccessName, &cchAccessName, &dwResult); Trace((TEXT("Connecting %s (%d)"), pszUNC, dwErr)); if (ppszAccessName && NOERROR == dwErr) { LocalAllocString(ppszAccessName, szAccessName); } TraceLeaveValue(NOERROR == dwErr); } DWORD WINAPI CCscShellExt::_UnpinFilesThread(LPVOID pvThreadData) { CSC_UNPIN_DATA *pUnpinData = reinterpret_cast(pvThreadData); if (pUnpinData) { HINSTANCE hInstThisDll = LoadLibrary(c_szDllName); if (hInstThisDll) { CscUnpinFileList(pUnpinData->pNamelist, (pUnpinData->dwUpdateFlags & CSC_UPDATE_PIN_RECURSE), pUnpinData->bOffline, NULL, NULL, 0); } delete pUnpinData->pNamelist; LocalFree(pUnpinData); if (hInstThisDll) { FreeLibraryAndExitThread(hInstThisDll, 0); } } return 0; } INT_PTR CALLBACK CCscShellExt::_ConfirmPinDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { INT_PTR bResult = TRUE; switch (uMsg) { case WM_INITDIALOG: CheckRadioButton(hDlg, IDC_PIN_NO_RECURSE, IDC_PIN_RECURSE, IDC_PIN_RECURSE); break; case WM_COMMAND: switch (LOWORD(wParam)) { case IDCANCEL: EndDialog(hDlg, IDCANCEL); break; case IDOK: // Return IDYES to indicate that the operation should be recursive. // Return IDNO to indicate no recursion. EndDialog(hDlg, BST_CHECKED == IsDlgButtonChecked(hDlg, IDC_PIN_RECURSE) ? IDYES : IDNO); break; } break; default: bResult = FALSE; // message not handled } return bResult; } // // Given an IDataObject ptr representing a selection of files from // within the shell, this function determins if pinning of any of // the files and directories is disallowed via system policy. // // Returns: S_OK - All files in data object can be pinned. // S_FALSE - At least one file in data object cannot be pinned. // HRESULT CCscShellExt::CanAllFilesBePinned( IDataObject *pdtobj ) { TraceEnter(TRACE_SHELLEX, "CCscShellExt::CanAllFilesBePinned"); // // Quick check to see if ANY pin restrictions are in place. // HRESULT hr = m_NoPinList.IsAnyPinDisallowed(); if (S_OK == hr) { // // Yes, at least one restriction was read from registry. // CscFilenameList fnl; hr = BuildFileList(m_lpdobj, GetDesktopWindow(), &fnl, NULL); if (SUCCEEDED(hr)) { // // Iterate over all UNC paths in the data object // until we either exhaust the list or find one for which // pinning is disallowed. // CscFilenameList::ShareIter si = fnl.CreateShareIterator(); CscFilenameList::HSHARE hShare; while(si.Next(&hShare)) { TCHAR szUncPath[MAX_PATH]; hr = StringCchCopy(szUncPath, ARRAYSIZE(szUncPath), fnl.GetShareName(hShare)); if (FAILED(hr) || !PathAddBackslash(szUncPath)) { TraceLeaveResult(HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE)); } const int cchShare = lstrlen(szUncPath); CscFilenameList::FileIter fi = fnl.CreateFileIterator(hShare); LPCTSTR pszFile; while(NULL != (pszFile = fi.Next())) { // // Assemble the full UNC path string. // If the item is a directory, will need to truncate the trailing // "\*" characters. // hr = StringCchCopy(szUncPath + cchShare, ARRAYSIZE(szUncPath) - cchShare, pszFile); if (FAILED(hr)) { TraceLeaveResult(HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE)); } LPTSTR pszEnd = szUncPath + lstrlen(szUncPath) - 1; while(pszEnd > szUncPath && (TEXT('\\') == *pszEnd || TEXT('*') == *pszEnd)) { *pszEnd-- = TEXT('\0'); } if (S_FALSE == m_NoPinList.IsPinAllowed(szUncPath)) { Trace((TEXT("Policy prevents pinning of \"%s\""), szUncPath)); TraceLeaveResult(S_FALSE); } } } } } TraceLeaveResult(SUCCEEDED(hr) ? S_OK : hr); } // // Support for recursively unpinning a tree with progress updates // typedef struct _UNPIN_FILES_DATA { BOOL bSubfolders; BOOL bOffline; PFN_UNPINPROGRESSPROC pfnProgressCB; LPARAM lpContext; } UNPIN_FILES_DATA, *PUNPIN_FILES_DATA; DWORD WINAPI _UnpinCallback(LPCTSTR pszItem, ENUM_REASON eReason, DWORD /*dwStatus*/, DWORD dwHintFlags, DWORD dwPinCount, LPWIN32_FIND_DATA pFind32, LPARAM lpContext) { PUNPIN_FILES_DATA pufd = reinterpret_cast(lpContext); // Skip folders if we aren't recursing if (eReason == ENUM_REASON_FOLDER_BEGIN && !pufd->bSubfolders) return CSCPROC_RETURN_SKIP; // Update progress if (pufd->pfnProgressCB) { DWORD dwResult = (*pufd->pfnProgressCB)(pszItem, pufd->lpContext); if (CSCPROC_RETURN_CONTINUE != dwResult) return dwResult; } // Unpin the item if it's pinned. For folders, // do this before recursing. if ((eReason == ENUM_REASON_FILE || eReason == ENUM_REASON_FOLDER_BEGIN) && (dwHintFlags & FLAG_CSC_HINT_PIN_USER)) { if (CSCUnpinFile(pszItem, FLAG_CSC_HINT_PIN_USER | FLAG_CSC_HINT_PIN_INHERIT_USER, NULL, NULL, &dwHintFlags)) { ShellChangeNotify(pszItem, pFind32, FALSE); } } // Delete items that are no longer pinned. For folders, // do this after recursing. if (eReason == ENUM_REASON_FILE || eReason == ENUM_REASON_FOLDER_END) { if (!dwHintFlags && !dwPinCount) { if (NOERROR == CscDelete(pszItem) && pufd->bOffline) { // Removing from the cache while in offline mode means // it's no longer available, so remove it from view. ShellChangeNotify(pszItem, pFind32, FALSE, (pFind32->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? SHCNE_RMDIR : SHCNE_DELETE); } } } return CSCPROC_RETURN_CONTINUE; } DWORD _UnpinOneShare(CscFilenameList *pfnl, CscFilenameList::HSHARE hShare, PUNPIN_FILES_DATA pufd) { DWORD dwResult = CSCPROC_RETURN_CONTINUE; LPCTSTR pszFile; LPCTSTR pszShare = pfnl->GetShareName(hShare); CscFilenameList::FileIter fi = pfnl->CreateFileIterator(hShare); // Iterate over the filenames associated with the share. while (pszFile = fi.Next()) { TCHAR szFullPath[MAX_PATH]; WIN32_FIND_DATA fd; DWORD dwPinCount = 0; DWORD dwHintFlags = 0; ZeroMemory(&fd, sizeof(fd)); fd.dwFileAttributes = FILE_ATTRIBUTE_NORMAL; // Build the full path if (!PathCombine(szFullPath, pszShare, pszFile)) { // fail instead? continue; } // Directories have a trailing "\*" if (StrChr(pszFile, TEXT('*'))) { // It's a directory. Trim off the "\*" PathRemoveFileSpec(szFullPath); fd.dwFileAttributes |= FILE_ATTRIBUTE_DIRECTORY; } // Update progress if (pufd->pfnProgressCB) { dwResult = (*pufd->pfnProgressCB)(szFullPath, pufd->lpContext); switch (dwResult) { case CSCPROC_RETURN_SKIP: continue; case CSCPROC_RETURN_ABORT: break; } } // Unpin it if (CSCUnpinFile(szFullPath, FLAG_CSC_HINT_PIN_USER | FLAG_CSC_HINT_PIN_INHERIT_USER, NULL, &dwPinCount, &dwHintFlags)) { StringCchCopy(fd.cFileName, ARRAYSIZE(fd.cFileName), PathFindFileName(szFullPath)); ShellChangeNotify(szFullPath, &fd, FALSE); } // If it's a directory, unpin its contents if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { _CSCEnumDatabase(szFullPath, pufd->bSubfolders, _UnpinCallback, (LPARAM)pufd); } // Is it still pinned? if (!dwHintFlags && !dwPinCount) { // Remove it from the cache (folders may still contain children // so we expect this to fail sometimes). if (NOERROR == CscDelete(szFullPath) && pufd->bOffline) { // Removing from the cache while in offline mode means // it's no longer available, so remove it from view. ShellChangeNotify(szFullPath, &fd, FALSE, (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? SHCNE_RMDIR : SHCNE_DELETE); } } } return dwResult; } void CscUnpinFileList(CscFilenameList *pfnl, BOOL bSubfolders, BOOL bOffline, LPCTSTR pszShare, PFN_UNPINPROGRESSPROC pfnProgressCB, LPARAM lpContext) { UNPIN_FILES_DATA ufd; DWORD dwResult = CSCPROC_RETURN_CONTINUE; CscFilenameList::HSHARE hShare; if (NULL == pfnl || !pfnl->IsValid() || 0 == pfnl->GetFileCount()) return; ufd.bSubfolders = bSubfolders; ufd.bOffline = bOffline; ufd.pfnProgressCB = pfnProgressCB; ufd.lpContext = lpContext; if (pszShare) // enumerate this share only { if (pfnl->GetShareHandle(pszShare, &hShare)) _UnpinOneShare(pfnl, hShare, &ufd); } else // enumerate everything in the list { CscFilenameList::ShareIter si = pfnl->CreateShareIterator(); while (si.Next(&hShare) && dwResult != CSCPROC_RETURN_ABORT) { dwResult = _UnpinOneShare(pfnl, hShare, &ufd); } } // Flush the shell notify queue ShellChangeNotify(NULL, TRUE); }