// // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // // Copyright (c) Microsoft Corporation. All rights reserved. // // #ifndef WIN32_NO_STATUS #include #define WIN32_NO_STATUS #endif #include #include "CSampleCredential.h" #include "guid.h" CSampleCredential::CSampleCredential(): _cRef(1), _pCredProvCredentialEvents(nullptr), _pszUserSid(nullptr), _pszQualifiedUserName(nullptr), _fIsLocalUser(false), _fChecked(false), _fShowControls(false), _dwComboIndex(0) { DllAddRef(); ZeroMemory(_rgCredProvFieldDescriptors, sizeof(_rgCredProvFieldDescriptors)); ZeroMemory(_rgFieldStatePairs, sizeof(_rgFieldStatePairs)); ZeroMemory(_rgFieldStrings, sizeof(_rgFieldStrings)); } CSampleCredential::~CSampleCredential() { if (_rgFieldStrings[SFI_PASSWORD]) { size_t lenPassword = wcslen(_rgFieldStrings[SFI_PASSWORD]); SecureZeroMemory(_rgFieldStrings[SFI_PASSWORD], lenPassword * sizeof(*_rgFieldStrings[SFI_PASSWORD])); } for (int i = 0; i < ARRAYSIZE(_rgFieldStrings); i++) { CoTaskMemFree(_rgFieldStrings[i]); CoTaskMemFree(_rgCredProvFieldDescriptors[i].pszLabel); } CoTaskMemFree(_pszUserSid); CoTaskMemFree(_pszQualifiedUserName); DllRelease(); } // Initializes one credential with the field information passed in. // Set the value of the SFI_LARGE_TEXT field to pwzUsername. HRESULT CSampleCredential::Initialize(CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, _In_ CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR const *rgcpfd, _In_ FIELD_STATE_PAIR const *rgfsp, _In_ ICredentialProviderUser *pcpUser) { HRESULT hr = S_OK; _cpus = cpus; GUID guidProvider; pcpUser->GetProviderID(&guidProvider); _fIsLocalUser = (guidProvider == Identity_LocalUserProvider); // Copy the field descriptors for each field. This is useful if you want to vary the field // descriptors based on what Usage scenario the credential was created for. for (DWORD i = 0; SUCCEEDED(hr) && i < ARRAYSIZE(_rgCredProvFieldDescriptors); i++) { _rgFieldStatePairs[i] = rgfsp[i]; hr = FieldDescriptorCopy(rgcpfd[i], &_rgCredProvFieldDescriptors[i]); } // Initialize the String value of all the fields. if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Sample Credential", &_rgFieldStrings[SFI_LABEL]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Sample Credential Provider", &_rgFieldStrings[SFI_LARGE_TEXT]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Edit Text", &_rgFieldStrings[SFI_EDIT_TEXT]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"", &_rgFieldStrings[SFI_PASSWORD]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Submit", &_rgFieldStrings[SFI_SUBMIT_BUTTON]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Checkbox", &_rgFieldStrings[SFI_CHECKBOX]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Combobox", &_rgFieldStrings[SFI_COMBOBOX]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Launch helper window", &_rgFieldStrings[SFI_LAUNCHWINDOW_LINK]); } if (SUCCEEDED(hr)) { hr = SHStrDupW(L"Hide additional controls", &_rgFieldStrings[SFI_HIDECONTROLS_LINK]); } if (SUCCEEDED(hr)) { hr = pcpUser->GetStringValue(PKEY_Identity_QualifiedUserName, &_pszQualifiedUserName); } if (SUCCEEDED(hr)) { PWSTR pszUserName; pcpUser->GetStringValue(PKEY_Identity_UserName, &pszUserName); if (pszUserName != nullptr) { wchar_t szString[256]; StringCchPrintf(szString, ARRAYSIZE(szString), L"User Name: %s", pszUserName); hr = SHStrDupW(szString, &_rgFieldStrings[SFI_FULLNAME_TEXT]); CoTaskMemFree(pszUserName); } else { hr = SHStrDupW(L"User Name is NULL", &_rgFieldStrings[SFI_FULLNAME_TEXT]); } } if (SUCCEEDED(hr)) { PWSTR pszDisplayName; pcpUser->GetStringValue(PKEY_Identity_DisplayName, &pszDisplayName); if (pszDisplayName != nullptr) { wchar_t szString[256]; StringCchPrintf(szString, ARRAYSIZE(szString), L"Display Name: %s", pszDisplayName); hr = SHStrDupW(szString, &_rgFieldStrings[SFI_DISPLAYNAME_TEXT]); CoTaskMemFree(pszDisplayName); } else { hr = SHStrDupW(L"Display Name is NULL", &_rgFieldStrings[SFI_DISPLAYNAME_TEXT]); } } if (SUCCEEDED(hr)) { PWSTR pszLogonStatus; pcpUser->GetStringValue(PKEY_Identity_LogonStatusString, &pszLogonStatus); if (pszLogonStatus != nullptr) { wchar_t szString[256]; StringCchPrintf(szString, ARRAYSIZE(szString), L"Logon Status: %s", pszLogonStatus); hr = SHStrDupW(szString, &_rgFieldStrings[SFI_LOGONSTATUS_TEXT]); CoTaskMemFree(pszLogonStatus); } else { hr = SHStrDupW(L"Logon Status is NULL", &_rgFieldStrings[SFI_LOGONSTATUS_TEXT]); } } if (SUCCEEDED(hr)) { hr = pcpUser->GetSid(&_pszUserSid); } return hr; } // LogonUI calls this in order to give us a callback in case we need to notify it of anything. HRESULT CSampleCredential::Advise(_In_ ICredentialProviderCredentialEvents *pcpce) { if (_pCredProvCredentialEvents != nullptr) { _pCredProvCredentialEvents->Release(); } return pcpce->QueryInterface(IID_PPV_ARGS(&_pCredProvCredentialEvents)); } // LogonUI calls this to tell us to release the callback. HRESULT CSampleCredential::UnAdvise() { if (_pCredProvCredentialEvents) { _pCredProvCredentialEvents->Release(); } _pCredProvCredentialEvents = nullptr; return S_OK; } // LogonUI calls this function when our tile is selected (zoomed) // If you simply want fields to show/hide based on the selected state, // there's no need to do anything here - you can set that up in the // field definitions. But if you want to do something // more complicated, like change the contents of a field when the tile is // selected, you would do it here. HRESULT CSampleCredential::SetSelected(_Out_ BOOL *pbAutoLogon) { *pbAutoLogon = FALSE; return S_OK; } // Similarly to SetSelected, LogonUI calls this when your tile was selected // and now no longer is. The most common thing to do here (which we do below) // is to clear out the password field. HRESULT CSampleCredential::SetDeselected() { HRESULT hr = S_OK; if (_rgFieldStrings[SFI_PASSWORD]) { size_t lenPassword = wcslen(_rgFieldStrings[SFI_PASSWORD]); SecureZeroMemory(_rgFieldStrings[SFI_PASSWORD], lenPassword * sizeof(*_rgFieldStrings[SFI_PASSWORD])); CoTaskMemFree(_rgFieldStrings[SFI_PASSWORD]); hr = SHStrDupW(L"", &_rgFieldStrings[SFI_PASSWORD]); if (SUCCEEDED(hr) && _pCredProvCredentialEvents) { _pCredProvCredentialEvents->SetFieldString(this, SFI_PASSWORD, _rgFieldStrings[SFI_PASSWORD]); } } return hr; } // Get info for a particular field of a tile. Called by logonUI to get information // to display the tile. HRESULT CSampleCredential::GetFieldState(DWORD dwFieldID, _Out_ CREDENTIAL_PROVIDER_FIELD_STATE *pcpfs, _Out_ CREDENTIAL_PROVIDER_FIELD_INTERACTIVE_STATE *pcpfis) { HRESULT hr; // Validate our parameters. if ((dwFieldID < ARRAYSIZE(_rgFieldStatePairs))) { *pcpfs = _rgFieldStatePairs[dwFieldID].cpfs; *pcpfis = _rgFieldStatePairs[dwFieldID].cpfis; hr = S_OK; } else { hr = E_INVALIDARG; } return hr; } // Sets ppwsz to the string value of the field at the index dwFieldID HRESULT CSampleCredential::GetStringValue(DWORD dwFieldID, _Outptr_result_nullonfailure_ PWSTR *ppwsz) { HRESULT hr; *ppwsz = nullptr; // Check to make sure dwFieldID is a legitimate index if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors)) { // Make a copy of the string and return that. The caller // is responsible for freeing it. hr = SHStrDupW(_rgFieldStrings[dwFieldID], ppwsz); } else { hr = E_INVALIDARG; } return hr; } // Get the image to show in the user tile HRESULT CSampleCredential::GetBitmapValue(DWORD dwFieldID, _Outptr_result_nullonfailure_ HBITMAP *phbmp) { HRESULT hr; *phbmp = nullptr; if ((SFI_TILEIMAGE == dwFieldID)) { HBITMAP hbmp = LoadBitmap(HINST_THISDLL, MAKEINTRESOURCE(IDB_TILE_IMAGE)); if (hbmp != nullptr) { hr = S_OK; *phbmp = hbmp; } else { hr = HRESULT_FROM_WIN32(GetLastError()); } } else { hr = E_INVALIDARG; } return hr; } // Sets pdwAdjacentTo to the index of the field the submit button should be // adjacent to. We recommend that the submit button is placed next to the last // field which the user is required to enter information in. Optional fields // should be below the submit button. HRESULT CSampleCredential::GetSubmitButtonValue(DWORD dwFieldID, _Out_ DWORD *pdwAdjacentTo) { HRESULT hr; if (SFI_SUBMIT_BUTTON == dwFieldID) { // pdwAdjacentTo is a pointer to the fieldID you want the submit button to // appear next to. *pdwAdjacentTo = SFI_PASSWORD; hr = S_OK; } else { hr = E_INVALIDARG; } return hr; } // Sets the value of a field which can accept a string as a value. // This is called on each keystroke when a user types into an edit field HRESULT CSampleCredential::SetStringValue(DWORD dwFieldID, _In_ PCWSTR pwz) { HRESULT hr; // Validate parameters. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_EDIT_TEXT == _rgCredProvFieldDescriptors[dwFieldID].cpft || CPFT_PASSWORD_TEXT == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { PWSTR *ppwszStored = &_rgFieldStrings[dwFieldID]; CoTaskMemFree(*ppwszStored); hr = SHStrDupW(pwz, ppwszStored); } else { hr = E_INVALIDARG; } return hr; } // Returns whether a checkbox is checked or not as well as its label. HRESULT CSampleCredential::GetCheckboxValue(DWORD dwFieldID, _Out_ BOOL *pbChecked, _Outptr_result_nullonfailure_ PWSTR *ppwszLabel) { HRESULT hr; *ppwszLabel = nullptr; // Validate parameters. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_CHECKBOX == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { *pbChecked = _fChecked; hr = SHStrDupW(_rgFieldStrings[SFI_CHECKBOX], ppwszLabel); } else { hr = E_INVALIDARG; } return hr; } // Sets whether the specified checkbox is checked or not. HRESULT CSampleCredential::SetCheckboxValue(DWORD dwFieldID, BOOL bChecked) { HRESULT hr; // Validate parameters. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_CHECKBOX == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { _fChecked = bChecked; hr = S_OK; } else { hr = E_INVALIDARG; } return hr; } // Returns the number of items to be included in the combobox (pcItems), as well as the // currently selected item (pdwSelectedItem). HRESULT CSampleCredential::GetComboBoxValueCount(DWORD dwFieldID, _Out_ DWORD *pcItems, _Deref_out_range_(<, *pcItems) _Out_ DWORD *pdwSelectedItem) { HRESULT hr; *pcItems = 0; *pdwSelectedItem = 0; // Validate parameters. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_COMBOBOX == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { *pcItems = ARRAYSIZE(s_rgComboBoxStrings); *pdwSelectedItem = 0; hr = S_OK; } else { hr = E_INVALIDARG; } return hr; } // Called iteratively to fill the combobox with the string (ppwszItem) at index dwItem. HRESULT CSampleCredential::GetComboBoxValueAt(DWORD dwFieldID, DWORD dwItem, _Outptr_result_nullonfailure_ PWSTR *ppwszItem) { HRESULT hr; *ppwszItem = nullptr; // Validate parameters. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_COMBOBOX == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { hr = SHStrDupW(s_rgComboBoxStrings[dwItem], ppwszItem); } else { hr = E_INVALIDARG; } return hr; } // Called when the user changes the selected item in the combobox. HRESULT CSampleCredential::SetComboBoxSelectedValue(DWORD dwFieldID, DWORD dwSelectedItem) { HRESULT hr; // Validate parameters. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_COMBOBOX == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { _dwComboIndex = dwSelectedItem; hr = S_OK; } else { hr = E_INVALIDARG; } return hr; } // Called when the user clicks a command link. HRESULT CSampleCredential::CommandLinkClicked(DWORD dwFieldID) { HRESULT hr = S_OK; CREDENTIAL_PROVIDER_FIELD_STATE cpfsShow = CPFS_HIDDEN; // Validate parameter. if (dwFieldID < ARRAYSIZE(_rgCredProvFieldDescriptors) && (CPFT_COMMAND_LINK == _rgCredProvFieldDescriptors[dwFieldID].cpft)) { HWND hwndOwner = nullptr; switch (dwFieldID) { case SFI_LAUNCHWINDOW_LINK: if (_pCredProvCredentialEvents) { _pCredProvCredentialEvents->OnCreatingWindow(&hwndOwner); } // Pop a messagebox indicating the click. ::MessageBox(hwndOwner, L"Command link clicked", L"Click!", 0); break; case SFI_HIDECONTROLS_LINK: _pCredProvCredentialEvents->BeginFieldUpdates(); cpfsShow = _fShowControls ? CPFS_DISPLAY_IN_SELECTED_TILE : CPFS_HIDDEN; _pCredProvCredentialEvents->SetFieldState(nullptr, SFI_FULLNAME_TEXT, cpfsShow); _pCredProvCredentialEvents->SetFieldState(nullptr, SFI_DISPLAYNAME_TEXT, cpfsShow); _pCredProvCredentialEvents->SetFieldState(nullptr, SFI_LOGONSTATUS_TEXT, cpfsShow); _pCredProvCredentialEvents->SetFieldState(nullptr, SFI_CHECKBOX, cpfsShow); _pCredProvCredentialEvents->SetFieldState(nullptr, SFI_EDIT_TEXT, cpfsShow); _pCredProvCredentialEvents->SetFieldState(nullptr, SFI_COMBOBOX, cpfsShow); _pCredProvCredentialEvents->SetFieldString(nullptr, SFI_HIDECONTROLS_LINK, _fShowControls? L"Hide additional controls" : L"Show additional controls"); _pCredProvCredentialEvents->EndFieldUpdates(); _fShowControls = !_fShowControls; break; default: hr = E_INVALIDARG; } } else { hr = E_INVALIDARG; } return hr; } // Collect the username and password into a serialized credential for the correct usage scenario // (logon/unlock is what's demonstrated in this sample). LogonUI then passes these credentials // back to the system to log on. HRESULT CSampleCredential::GetSerialization(_Out_ CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE *pcpgsr, _Out_ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs, _Outptr_result_maybenull_ PWSTR *ppwszOptionalStatusText, _Out_ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon) { HRESULT hr = E_UNEXPECTED; *pcpgsr = CPGSR_NO_CREDENTIAL_NOT_FINISHED; *ppwszOptionalStatusText = nullptr; *pcpsiOptionalStatusIcon = CPSI_NONE; ZeroMemory(pcpcs, sizeof(*pcpcs)); // For local user, the domain and user name can be split from _pszQualifiedUserName (domain\username). // CredPackAuthenticationBuffer() cannot be used because it won't work with unlock scenario. if (_fIsLocalUser) { PWSTR pwzProtectedPassword; hr = ProtectIfNecessaryAndCopyPassword(_rgFieldStrings[SFI_PASSWORD], _cpus, &pwzProtectedPassword); if (SUCCEEDED(hr)) { PWSTR pszDomain; PWSTR pszUsername; hr = SplitDomainAndUsername(_pszQualifiedUserName, &pszDomain, &pszUsername); if (SUCCEEDED(hr)) { KERB_INTERACTIVE_UNLOCK_LOGON kiul; hr = KerbInteractiveUnlockLogonInit(pszDomain, pszUsername, pwzProtectedPassword, _cpus, &kiul); if (SUCCEEDED(hr)) { // We use KERB_INTERACTIVE_UNLOCK_LOGON in both unlock and logon scenarios. It contains a // KERB_INTERACTIVE_LOGON to hold the creds plus a LUID that is filled in for us by Winlogon // as necessary. hr = KerbInteractiveUnlockLogonPack(kiul, &pcpcs->rgbSerialization, &pcpcs->cbSerialization); if (SUCCEEDED(hr)) { ULONG ulAuthPackage; hr = RetrieveNegotiateAuthPackage(&ulAuthPackage); if (SUCCEEDED(hr)) { pcpcs->ulAuthenticationPackage = ulAuthPackage; pcpcs->clsidCredentialProvider = CLSID_CSample; // At this point the credential has created the serialized credential used for logon // By setting this to CPGSR_RETURN_CREDENTIAL_FINISHED we are letting logonUI know // that we have all the information we need and it should attempt to submit the // serialized credential. *pcpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED; } } } CoTaskMemFree(pszDomain); CoTaskMemFree(pszUsername); } CoTaskMemFree(pwzProtectedPassword); } } else { DWORD dwAuthFlags = CRED_PACK_PROTECTED_CREDENTIALS | CRED_PACK_ID_PROVIDER_CREDENTIALS; // First get the size of the authentication buffer to allocate if (!CredPackAuthenticationBuffer(dwAuthFlags, _pszQualifiedUserName, const_cast(_rgFieldStrings[SFI_PASSWORD]), nullptr, &pcpcs->cbSerialization) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { pcpcs->rgbSerialization = static_cast(CoTaskMemAlloc(pcpcs->cbSerialization)); if (pcpcs->rgbSerialization != nullptr) { hr = S_OK; // Retrieve the authentication buffer if (CredPackAuthenticationBuffer(dwAuthFlags, _pszQualifiedUserName, const_cast(_rgFieldStrings[SFI_PASSWORD]), pcpcs->rgbSerialization, &pcpcs->cbSerialization)) { ULONG ulAuthPackage; hr = RetrieveNegotiateAuthPackage(&ulAuthPackage); if (SUCCEEDED(hr)) { pcpcs->ulAuthenticationPackage = ulAuthPackage; pcpcs->clsidCredentialProvider = CLSID_CSample; // At this point the credential has created the serialized credential used for logon // By setting this to CPGSR_RETURN_CREDENTIAL_FINISHED we are letting logonUI know // that we have all the information we need and it should attempt to submit the // serialized credential. *pcpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED; } } else { hr = HRESULT_FROM_WIN32(GetLastError()); if (SUCCEEDED(hr)) { hr = E_FAIL; } } if (FAILED(hr)) { CoTaskMemFree(pcpcs->rgbSerialization); } } else { hr = E_OUTOFMEMORY; } } } return hr; } struct REPORT_RESULT_STATUS_INFO { NTSTATUS ntsStatus; NTSTATUS ntsSubstatus; PWSTR pwzMessage; CREDENTIAL_PROVIDER_STATUS_ICON cpsi; }; static const REPORT_RESULT_STATUS_INFO s_rgLogonStatusInfo[] = { { STATUS_LOGON_FAILURE, STATUS_SUCCESS, L"Incorrect password or username.", CPSI_ERROR, }, { STATUS_ACCOUNT_RESTRICTION, STATUS_ACCOUNT_DISABLED, L"The account is disabled.", CPSI_WARNING }, }; // ReportResult is completely optional. Its purpose is to allow a credential to customize the string // and the icon displayed in the case of a logon failure. For example, we have chosen to // customize the error shown in the case of bad username/password and in the case of the account // being disabled. HRESULT CSampleCredential::ReportResult(NTSTATUS ntsStatus, NTSTATUS ntsSubstatus, _Outptr_result_maybenull_ PWSTR *ppwszOptionalStatusText, _Out_ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon) { *ppwszOptionalStatusText = nullptr; *pcpsiOptionalStatusIcon = CPSI_NONE; DWORD dwStatusInfo = (DWORD)-1; // Look for a match on status and substatus. for (DWORD i = 0; i < ARRAYSIZE(s_rgLogonStatusInfo); i++) { if (s_rgLogonStatusInfo[i].ntsStatus == ntsStatus && s_rgLogonStatusInfo[i].ntsSubstatus == ntsSubstatus) { dwStatusInfo = i; break; } } if ((DWORD)-1 != dwStatusInfo) { if (SUCCEEDED(SHStrDupW(s_rgLogonStatusInfo[dwStatusInfo].pwzMessage, ppwszOptionalStatusText))) { *pcpsiOptionalStatusIcon = s_rgLogonStatusInfo[dwStatusInfo].cpsi; } } // If we failed the logon, try to erase the password field. if (FAILED(HRESULT_FROM_NT(ntsStatus))) { if (_pCredProvCredentialEvents) { _pCredProvCredentialEvents->SetFieldString(this, SFI_PASSWORD, L""); } } // Since nullptr is a valid value for *ppwszOptionalStatusText and *pcpsiOptionalStatusIcon // this function can't fail. return S_OK; } // Gets the SID of the user corresponding to the credential. HRESULT CSampleCredential::GetUserSid(_Outptr_result_nullonfailure_ PWSTR *ppszSid) { *ppszSid = nullptr; HRESULT hr = E_UNEXPECTED; if (_pszUserSid != nullptr) { hr = SHStrDupW(_pszUserSid, ppszSid); } // Return S_FALSE with a null SID in ppszSid for the // credential to be associated with an empty user tile. return hr; } // GetFieldOptions to enable the password reveal button and touch keyboard auto-invoke in the password field. HRESULT CSampleCredential::GetFieldOptions(DWORD dwFieldID, _Out_ CREDENTIAL_PROVIDER_CREDENTIAL_FIELD_OPTIONS *pcpcfo) { *pcpcfo = CPCFO_NONE; if (dwFieldID == SFI_PASSWORD) { *pcpcfo = CPCFO_ENABLE_PASSWORD_REVEAL; } else if (dwFieldID == SFI_TILEIMAGE) { *pcpcfo = CPCFO_ENABLE_TOUCH_KEYBOARD_AUTO_INVOKE; } return S_OK; }