1356 lines
32 KiB
C++
1356 lines
32 KiB
C++
/*++
|
|
|
|
Copyright (c) 1997 Microsoft Corporation
|
|
|
|
Abstract:
|
|
|
|
This module provides utilities useful for Directory Service interactions
|
|
|
|
Author:
|
|
|
|
Steve Wilson (NT) November 1997
|
|
|
|
Revision History:
|
|
|
|
--*/
|
|
|
|
#define INC_OLE2
|
|
|
|
#include "precomp.h"
|
|
#pragma hdrstop
|
|
|
|
#include "client.h"
|
|
|
|
#include "pubprn.hxx"
|
|
#include "varconv.hxx"
|
|
#include "property.hxx"
|
|
#include "dsutil.hxx"
|
|
#include "winsprlp.h"
|
|
#include "dnsapi.h"
|
|
|
|
|
|
#define DN_SPECIAL_CHARS L",=\r\n+<>#;\"\\"
|
|
#define DN_SPECIAL_FILTER_CHARS L"\\*()"
|
|
|
|
|
|
PWSTR
|
|
GetUNCName(
|
|
HANDLE hPrinter
|
|
)
|
|
{
|
|
PPRINTER_INFO_2 pInfo2 = NULL;
|
|
DWORD cbNeeded;
|
|
PWSTR pszUNCName = NULL;
|
|
|
|
|
|
if (!GetPrinter(hPrinter, 2, (PBYTE) pInfo2, 0, &cbNeeded)) {
|
|
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
|
goto error;
|
|
}
|
|
|
|
if (!(pInfo2 = (PPRINTER_INFO_2) AllocSplMem(cbNeeded)))
|
|
goto error;
|
|
|
|
if (!GetPrinter(hPrinter, 2, (PBYTE) pInfo2, cbNeeded, &cbNeeded))
|
|
goto error;
|
|
|
|
|
|
// pPrinterName is already in correct UNC format since we
|
|
// opened handle with UNC name: just copy it
|
|
|
|
cbNeeded = wcslen(pInfo2->pPrinterName) + 1;
|
|
cbNeeded *= sizeof(WCHAR);
|
|
|
|
if (!(pszUNCName = (PWSTR) AllocSplMem(cbNeeded)))
|
|
goto error;
|
|
|
|
wcscpy(pszUNCName, pInfo2->pPrinterName);
|
|
|
|
|
|
error:
|
|
|
|
if (pInfo2)
|
|
FreeSplMem(pInfo2);
|
|
|
|
return pszUNCName;
|
|
}
|
|
|
|
PWSTR
|
|
CreateEscapedDN(
|
|
PCWSTR pszIn
|
|
)
|
|
{
|
|
PWSTR psz, pszO;
|
|
PWSTR pszOut = NULL;
|
|
DWORD cb;
|
|
|
|
// Count special characters
|
|
for (cb = 0, psz = (PWSTR) pszIn ; psz = wcspbrk(psz, DN_SPECIAL_FILTER_CHARS) ; ++cb, ++psz)
|
|
;
|
|
|
|
// Add in length of input string
|
|
// 2 = (\5c) - '\'
|
|
cb = (wcslen(pszIn) + cb*2 + 1)*sizeof *pszIn;
|
|
|
|
// Allocate output buffer and replace special chars with \HexEquivalent
|
|
// Ex: replace \ with \5c , ( with \28
|
|
if (pszOut = (PWSTR) AllocSplMem(cb)) {
|
|
|
|
for(psz = (PWSTR) pszIn, pszO = pszOut ; *psz ; ++psz) {
|
|
if (wcschr(DN_SPECIAL_FILTER_CHARS, *psz)) {
|
|
pszO += wsprintf(pszO, L"\\%x", *psz);
|
|
} else {
|
|
*pszO++ = *psz;
|
|
}
|
|
}
|
|
*pszO = L'\0';
|
|
}
|
|
|
|
return pszOut;
|
|
}
|
|
|
|
DWORD
|
|
PrintQueueExists(
|
|
HWND hwnd,
|
|
HANDLE hPrinter,
|
|
PWSTR pszUNCName,
|
|
DWORD dwAction,
|
|
PWSTR pszTargetDN,
|
|
PWSTR *ppszObjectDN
|
|
)
|
|
{
|
|
HRESULT hr = S_OK;
|
|
DWORD dwRet = ERROR_SUCCESS;
|
|
WCHAR szSearchPattern[MAX_UNC_PRINTER_NAME + 50];
|
|
WCHAR szFullUNCName[MAX_UNC_PRINTER_NAME];
|
|
PWSTR pNames[2];
|
|
WCHAR szName[MAX_PATH + 1];
|
|
WCHAR szDuplicateFormat[1024];
|
|
PWSTR pszSearchRoot = NULL;
|
|
PWSTR pszDuplicate = NULL;
|
|
PWSTR pszUNCNameSearch = NULL;
|
|
IDirectorySearch *pDSSearch = NULL;
|
|
DS_NAME_RESULT *pDNR = NULL;
|
|
DOMAIN_CONTROLLER_INFO *pDCI = NULL;
|
|
HANDLE hDS = NULL;
|
|
ADS_SEARCH_HANDLE hSearchHandle = NULL;
|
|
ADS_SEARCH_COLUMN ADsPath;
|
|
ADS_SEARCH_COLUMN UNCName;
|
|
PWSTR pszAttributes[] = {L"ADsPath", L"UNCName"};
|
|
DWORD nSize;
|
|
BOOL bRet = FALSE;
|
|
BOOL bDeleteDuplicate;
|
|
PDSROLE_PRIMARY_DOMAIN_INFO_BASIC pDsRole = NULL;
|
|
|
|
dwRet = DsRoleGetPrimaryDomainInformation(NULL, DsRolePrimaryDomainInfoBasic, (PBYTE *) &pDsRole);
|
|
|
|
if (dwRet != ERROR_SUCCESS)
|
|
goto error;
|
|
|
|
wsprintf(szName, L"%ws\\", pDsRole->DomainNameFlat);
|
|
|
|
pNames[0] = szName;
|
|
pNames[1] = NULL;
|
|
|
|
dwRet = Bind2DS(&hDS, &pDCI, DS_GC_SERVER_REQUIRED);
|
|
if (dwRet != ERROR_SUCCESS)
|
|
goto error;
|
|
|
|
if (!(DsCrackNames(
|
|
hDS,
|
|
DS_NAME_NO_FLAGS,
|
|
DS_UNKNOWN_NAME,
|
|
DS_FQDN_1779_NAME,
|
|
1,
|
|
&pNames[0],
|
|
&pDNR) == ERROR_SUCCESS)) {
|
|
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
if (pDNR->rItems[0].status != DS_NAME_NO_ERROR) {
|
|
if (pDNR->rItems[0].status == DS_NAME_ERROR_RESOLVING)
|
|
dwRet = ERROR_PATH_NOT_FOUND;
|
|
else
|
|
dwRet = pDNR->rItems[0].status;
|
|
|
|
goto error;
|
|
}
|
|
|
|
// GC:// + pDCName + 1
|
|
nSize = (wcslen(pDNR->rItems[0].pName) + 6)*sizeof(WCHAR);
|
|
|
|
if (!(pszSearchRoot = (PWSTR) AllocSplMem(nSize))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
wsprintf(pszSearchRoot, L"GC://%ws", pDNR->rItems[0].pName);
|
|
|
|
|
|
hr = ADsGetObject( pszSearchRoot,
|
|
IID_IDirectorySearch,
|
|
(void **)&pDSSearch);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
if (!(pszUNCNameSearch = CreateEscapedDN(pszUNCName))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
wsprintf(szSearchPattern, L"(&(objectClass=printQueue)(uNCName=%ws))", pszUNCNameSearch);
|
|
|
|
hr = pDSSearch->ExecuteSearch(
|
|
szSearchPattern,
|
|
pszAttributes,
|
|
sizeof(pszAttributes)/sizeof *pszAttributes,
|
|
&hSearchHandle);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
|
|
hr = pDSSearch->GetNextRow(hSearchHandle);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
|
|
while (hr != S_ADS_NOMORE_ROWS) {
|
|
|
|
hr = pDSSearch->GetColumn(
|
|
hSearchHandle,
|
|
L"ADsPath",
|
|
&ADsPath
|
|
);
|
|
|
|
if (hr == S_OK) {
|
|
|
|
hr = pDSSearch->GetColumn(
|
|
hSearchHandle,
|
|
L"UNCName",
|
|
&UNCName
|
|
);
|
|
|
|
if (hr == S_OK) {
|
|
|
|
switch (dwAction) {
|
|
case PUBLISHPRINTER_QUERY:
|
|
|
|
if (!LoadString(hInst, IDS_DUPLICATE_PRINTQUEUE, szDuplicateFormat, COUNTOF(szDuplicateFormat))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
nSize = wcslen(szDuplicateFormat);
|
|
nSize += wcslen(ADsPath.pADsValues->DNString);
|
|
nSize += wcslen(pszTargetDN);
|
|
nSize = (nSize + 1)*sizeof(WCHAR);
|
|
if (!(pszDuplicate = (PWSTR) AllocSplMem(nSize))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
PWSTR pszCanonicalSource;
|
|
PWSTR pszCanonicalTarget;
|
|
|
|
pszCanonicalSource = pszCanonicalTarget = NULL;
|
|
|
|
FQDN2Canonical(ADsPath.pADsValues->DNString, &pszCanonicalSource);
|
|
FQDN2Canonical(pszTargetDN, &pszCanonicalTarget);
|
|
|
|
if (!pszCanonicalSource || !pszCanonicalTarget) {
|
|
wsprintf(pszDuplicate, szDuplicateFormat, ADsPath.pADsValues->DNString, pszTargetDN);
|
|
} else {
|
|
wsprintf(pszDuplicate, szDuplicateFormat, pszCanonicalSource, pszCanonicalTarget);
|
|
}
|
|
|
|
FreeSplStr(pszCanonicalSource);
|
|
FreeSplStr(pszCanonicalTarget);
|
|
|
|
if (!LoadString(hInst, IDS_DUPLICATE_PRINTQUEUE_TITLE, szDuplicateFormat, COUNTOF(szDuplicateFormat)))
|
|
goto error;
|
|
|
|
dwRet = MessageBox( hwnd,
|
|
pszDuplicate,
|
|
szDuplicateFormat,
|
|
MB_YESNO);
|
|
|
|
bDeleteDuplicate = (dwRet == IDYES);
|
|
dwRet = (dwRet == IDYES) ? ERROR_SUCCESS : ERROR_CANCELLED;
|
|
|
|
pszDuplicate = NULL;
|
|
break;
|
|
|
|
case PUBLISHPRINTER_DELETE_DUPLICATES:
|
|
bDeleteDuplicate = TRUE;
|
|
break;
|
|
|
|
case PUBLISHPRINTER_FAIL_ON_DUPLICATE:
|
|
bDeleteDuplicate = FALSE;
|
|
|
|
if (ppszObjectDN) {
|
|
if (!(*ppszObjectDN = AllocGlobalStr(ADsPath.pADsValues->DNString))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
dwRet = ERROR_FILE_EXISTS;
|
|
break;
|
|
|
|
case PUBLISHPRINTER_IGNORE_DUPLICATES:
|
|
bDeleteDuplicate = FALSE;
|
|
break;
|
|
|
|
default:
|
|
bDeleteDuplicate = FALSE;
|
|
dwRet = ERROR_INVALID_PARAMETER;
|
|
break;
|
|
}
|
|
|
|
if (bDeleteDuplicate) {
|
|
hr = DeleteDSObject(ADsPath.pADsValues->DNString);
|
|
|
|
if ( hr == ERROR_DS_NO_SUCH_OBJECT ) {
|
|
hr = S_OK;
|
|
}
|
|
}
|
|
|
|
pDSSearch->FreeColumn(&UNCName);
|
|
}
|
|
|
|
pDSSearch->FreeColumn(&ADsPath);
|
|
}
|
|
|
|
if (dwRet != ERROR_SUCCESS || hr != S_OK)
|
|
goto error;
|
|
|
|
hr = pDSSearch->GetNextRow(hSearchHandle);
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
|
|
|
|
hr = S_OK;
|
|
|
|
|
|
error:
|
|
|
|
if (hr != S_OK)
|
|
dwRet = ERROR_DS_UNAVAILABLE;
|
|
|
|
if (pDsRole)
|
|
DsRoleFreeMemory((PVOID) pDsRole);
|
|
|
|
if (pDNR)
|
|
DsFreeNameResult(pDNR);
|
|
|
|
if (hDS)
|
|
DsUnBind(&hDS);
|
|
|
|
if (pDCI)
|
|
NetApiBufferFree(pDCI);
|
|
|
|
if (hSearchHandle)
|
|
pDSSearch->CloseSearchHandle(hSearchHandle);
|
|
|
|
if (pszUNCNameSearch)
|
|
FreeSplMem(pszUNCNameSearch);
|
|
|
|
if (pDSSearch)
|
|
pDSSearch->Release();
|
|
|
|
if (pszSearchRoot)
|
|
FreeSplMem(pszSearchRoot);
|
|
|
|
if(pszDuplicate)
|
|
FreeSplMem(pszDuplicate);
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
|
|
|
|
DWORD
|
|
MovePrintQueue(
|
|
PCWSTR pszObjectGUID,
|
|
PCWSTR pszNewContainer, // Container path
|
|
PCWSTR pszNewCN // Object CN
|
|
)
|
|
{
|
|
PWSTR pszCurrentContainer = NULL;
|
|
PWSTR pszCurrentCN = NULL;
|
|
HRESULT hr;
|
|
IADsContainer *pADsContainer = NULL;
|
|
IDispatch *pNewObject = NULL;
|
|
|
|
|
|
// Get PublishPoint from GUID
|
|
hr = GetPublishPointFromGUID(pszObjectGUID, &pszCurrentContainer, &pszCurrentCN);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
if (pszCurrentContainer) {
|
|
// Get container
|
|
hr = ADsGetObject( pszCurrentContainer, IID_IADsContainer, (void **) &pADsContainer);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
|
|
// Move PrintQueue
|
|
if (wcscmp(pszCurrentContainer, pszNewContainer)) {
|
|
hr = pADsContainer->MoveHere((PWSTR) pszNewContainer, (PWSTR) pszNewCN, &pNewObject);
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
}
|
|
|
|
error:
|
|
|
|
if (pszCurrentContainer)
|
|
FreeSplMem(pszCurrentContainer);
|
|
|
|
if (pszCurrentCN)
|
|
FreeSplMem(pszCurrentCN);
|
|
|
|
if (pADsContainer)
|
|
pADsContainer->Release();
|
|
|
|
if (pNewObject)
|
|
pNewObject->Release();
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
HRESULT
|
|
GetPublishPointFromGUID(
|
|
PCWSTR pszObjectGUID,
|
|
PWSTR *ppszDN,
|
|
PWSTR *ppszCN
|
|
)
|
|
{
|
|
DWORD dwRet, nBytes, nChars;
|
|
PWSTR pNames[2];
|
|
DS_NAME_RESULT *pDNR = NULL;
|
|
DOMAIN_CONTROLLER_INFO *pDCI = NULL;
|
|
HANDLE hDS = NULL;
|
|
PWSTR psz;
|
|
HRESULT hr = S_OK;
|
|
|
|
|
|
dwRet = Bind2DS(&hDS, &pDCI, DS_DIRECTORY_SERVICE_PREFERRED);
|
|
if (dwRet != ERROR_SUCCESS)
|
|
goto error;
|
|
|
|
|
|
// Get Publish Point
|
|
|
|
if (ppszDN) {
|
|
|
|
pNames[0] = (PWSTR) pszObjectGUID;
|
|
pNames[1] = NULL;
|
|
|
|
if (!(DsCrackNames(
|
|
hDS,
|
|
DS_NAME_NO_FLAGS,
|
|
DS_UNKNOWN_NAME,
|
|
DS_FQDN_1779_NAME,
|
|
1,
|
|
&pNames[0],
|
|
&pDNR) == ERROR_SUCCESS)) {
|
|
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
if (pDNR->rItems[0].status != DS_NAME_NO_ERROR) {
|
|
if (pDNR->rItems[0].status == DS_NAME_ERROR_RESOLVING)
|
|
dwRet = ERROR_PATH_NOT_FOUND;
|
|
else
|
|
dwRet = pDNR->rItems[0].status;
|
|
|
|
goto error;
|
|
}
|
|
|
|
|
|
// Separate DN into CN & PublishPoint
|
|
// pDNR has form: CN=CommonName,DN...
|
|
|
|
hr = FQDN2CNDN(pDCI->DomainControllerName + 2, pDNR->rItems[0].pName, ppszCN, ppszDN);
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
|
|
|
|
error:
|
|
|
|
if (pDNR)
|
|
DsFreeNameResult(pDNR);
|
|
|
|
if (hDS)
|
|
DsUnBind(&hDS);
|
|
|
|
if (pDCI)
|
|
NetApiBufferFree(pDCI);
|
|
|
|
if (dwRet != ERROR_SUCCESS) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, dwRet);
|
|
}
|
|
|
|
if (FAILED(hr)) {
|
|
FreeSplMem(*ppszCN);
|
|
FreeSplMem(*ppszDN);
|
|
*ppszCN = *ppszDN = NULL;
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
DWORD
|
|
Bind2DS(
|
|
HANDLE *phDS,
|
|
DOMAIN_CONTROLLER_INFO **ppDCI,
|
|
ULONG Flags
|
|
)
|
|
{
|
|
DWORD dwRet;
|
|
|
|
dwRet = DsGetDcName(NULL, NULL, NULL, NULL, Flags, ppDCI);
|
|
if (dwRet == ERROR_SUCCESS) {
|
|
|
|
if ((*ppDCI)->Flags & DS_DS_FLAG) {
|
|
|
|
dwRet = DsBind (NULL, (*ppDCI)->DomainName, phDS);
|
|
if (dwRet != ERROR_SUCCESS) {
|
|
|
|
NetApiBufferFree(*ppDCI);
|
|
*ppDCI = NULL;
|
|
|
|
if (!(Flags & DS_FORCE_REDISCOVERY)) {
|
|
dwRet = Bind2DS(phDS, ppDCI, DS_FORCE_REDISCOVERY | Flags);
|
|
}
|
|
}
|
|
} else {
|
|
NetApiBufferFree(*ppDCI);
|
|
*ppDCI = NULL;
|
|
dwRet = ERROR_CANT_ACCESS_DOMAIN_INFO;
|
|
}
|
|
}
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
|
|
|
|
HRESULT
|
|
FQDN2CNDN(
|
|
PWSTR pszDCName,
|
|
PWSTR pszFQDN,
|
|
PWSTR *ppszCN,
|
|
PWSTR *ppszDN
|
|
)
|
|
{
|
|
IADs *pADs = NULL;
|
|
PWSTR pszCN = NULL;
|
|
PWSTR pszDN = NULL;
|
|
PWSTR pszLDAPPath = NULL;
|
|
HRESULT hr;
|
|
|
|
// Get LDAP path to object
|
|
hr = BuildLDAPPath(pszDCName, pszFQDN, &pszLDAPPath);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
// Get DN
|
|
hr = ADsGetObject(pszLDAPPath, IID_IADs, (void **) &pADs);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
hr = pADs->get_Parent(&pszDN);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
if (!(*ppszDN = AllocSplStr(pszDN))) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
|
|
|
|
// Get CN
|
|
hr = pADs->get_Name(&pszCN);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
if (!(*ppszCN = AllocSplStr(pszCN))) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
|
|
|
|
error:
|
|
|
|
if (pADs)
|
|
pADs->Release();
|
|
|
|
if (pszCN)
|
|
SysFreeString(pszCN);
|
|
|
|
if (pszDN)
|
|
SysFreeString(pszDN);
|
|
|
|
FreeSplStr(pszLDAPPath);
|
|
|
|
if (FAILED(hr)) {
|
|
FreeSplStr(*ppszCN);
|
|
FreeSplStr(*ppszDN);
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
HRESULT
|
|
BuildLDAPPath(
|
|
PWSTR pszDC,
|
|
PWSTR pszFQDN,
|
|
PWSTR *ppszLDAPPath
|
|
)
|
|
{
|
|
DWORD nBytes;
|
|
HRESULT hr;
|
|
|
|
// LDAP:// + pDCName + / + pName + 1
|
|
nBytes = (wcslen(pszDC) + wcslen(pszFQDN) + 9)*sizeof(WCHAR);
|
|
|
|
if (!(*ppszLDAPPath = (PWSTR) AllocSplMem(nBytes))) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
|
|
wsprintf(*ppszLDAPPath, L"LDAP://%ws/%ws", pszDC,pszFQDN);
|
|
|
|
error:
|
|
|
|
return hr;
|
|
}
|
|
|
|
DWORD
|
|
UNC2Printer(
|
|
PCWSTR pszUNC,
|
|
PWSTR *ppszPrinter
|
|
)
|
|
{
|
|
PWSTR psz;
|
|
|
|
if (!pszUNC || pszUNC[0] != L'\\' || pszUNC[1] != L'\\')
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
if(!(psz = wcsrchr(pszUNC + 2, L'\\')))
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
if (!(*ppszPrinter = (PWSTR) AllocSplStr(psz + 1)))
|
|
return GetLastError();
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
DWORD
|
|
UNC2Server(
|
|
PCWSTR pszUNC,
|
|
PWSTR *ppszServer
|
|
)
|
|
{
|
|
PWSTR psz;
|
|
DWORD cb;
|
|
DWORD nChars;
|
|
|
|
if (!pszUNC || pszUNC[0] != L'\\' || pszUNC[1] != L'\\')
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
if(!(psz = wcschr(pszUNC + 2, L'\\')))
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
cb = (DWORD) ((ULONG_PTR) psz - (ULONG_PTR) pszUNC + sizeof *psz);
|
|
|
|
if (!(*ppszServer = (PWSTR) AllocSplMem(cb)))
|
|
return GetLastError();
|
|
|
|
|
|
nChars = (DWORD) (psz - pszUNC);
|
|
wcsncpy(*ppszServer, pszUNC, nChars);
|
|
(*ppszServer)[nChars] = L'\0';
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
|
|
// Utility routine to report if a printer is color or monochrome
|
|
|
|
BOOL
|
|
ThisIsAColorPrinter(
|
|
LPCTSTR lpstrName
|
|
)
|
|
{
|
|
HANDLE hPrinter = NULL;
|
|
LPTSTR lpstrMe = const_cast <LPTSTR> (lpstrName);
|
|
BOOL bReturn = FALSE;
|
|
LPDEVMODE lpdm = NULL;
|
|
long lcbNeeded;
|
|
|
|
if (!OpenPrinter(lpstrMe, &hPrinter, NULL)) {
|
|
goto error;
|
|
}
|
|
|
|
|
|
// First, use DocumentProperties to find the correct DEVMODE size- we
|
|
// must use the DEVMODE to force color on, in case the user's defaults
|
|
// have turned it off...
|
|
|
|
lcbNeeded = DocumentProperties(NULL, hPrinter, lpstrMe, NULL, NULL, 0);
|
|
if (lcbNeeded <= 0) {
|
|
goto error;
|
|
}
|
|
|
|
lpdm = (LPDEVMODE) AllocSplMem(lcbNeeded);
|
|
if (lpdm) {
|
|
|
|
lpdm->dmSize = sizeof(DEVMODE);
|
|
lpdm->dmFields = DM_COLOR;
|
|
lpdm->dmColor = DMCOLOR_COLOR;
|
|
|
|
if (IDOK == DocumentProperties(NULL, hPrinter, lpstrMe, lpdm, lpdm,
|
|
DM_IN_BUFFER | DM_OUT_BUFFER)) {
|
|
|
|
// Finally, we can create the DC!
|
|
HDC hdcThis = CreateDC(NULL, lpstrName, NULL, lpdm);
|
|
|
|
if (hdcThis) {
|
|
bReturn = 2 < (unsigned) GetDeviceCaps(hdcThis, NUMCOLORS);
|
|
DeleteDC(hdcThis);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
error:
|
|
|
|
FreeSplMem(lpdm);
|
|
|
|
if (hPrinter)
|
|
ClosePrinter(hPrinter);
|
|
|
|
return bReturn;
|
|
}
|
|
|
|
|
|
|
|
|
|
HRESULT
|
|
DeleteDSObject(
|
|
PWSTR pszADsPath
|
|
)
|
|
{
|
|
BSTR bstrCommonName = NULL;
|
|
PWSTR pszParent = NULL;
|
|
PWSTR pszCN = NULL;
|
|
PWSTR pszLDAPPath = NULL;
|
|
IADs *pADs = NULL;
|
|
IADsContainer *pContainer = NULL;
|
|
DWORD cb;
|
|
HRESULT hr;
|
|
DWORD nSize;
|
|
|
|
if(pszADsPath && !_wcsnicmp(pszADsPath , L"GC://" , 5)){
|
|
|
|
//
|
|
// Build LDAP://..... path from GC://.... path
|
|
// 3 comes from len(LDAP) - len(GC) + len(string_terminator)
|
|
//
|
|
nSize = (wcslen(pszADsPath) + 3)* sizeof(WCHAR);
|
|
if (!(pszLDAPPath = (PWSTR) AllocSplMem(nSize))) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
|
|
goto error;
|
|
}
|
|
wsprintf(pszLDAPPath, L"LDAP%ws", pszADsPath + 2);
|
|
|
|
}
|
|
|
|
// Get PrintQueue object
|
|
hr = ADsGetObject(pszLDAPPath, IID_IADs, (void **) &pADs);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
|
|
// Get the CommonName & don't forget the "CN="
|
|
hr = get_BSTR_Property(pADs, L"cn", &bstrCommonName);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
cb = (SysStringLen(bstrCommonName) + 4)*sizeof(WCHAR);
|
|
|
|
if (!(pszCN = (PWSTR) AllocSplMem(cb))) {
|
|
hr = dw2hr(GetLastError());
|
|
BAIL_ON_FAILURE(hr);
|
|
}
|
|
|
|
wcscpy(pszCN, L"CN=");
|
|
wcscat(pszCN, bstrCommonName);
|
|
|
|
|
|
// Get the Parent ADsPath
|
|
hr = pADs->get_Parent(&pszParent);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
|
|
// Get the Parent object
|
|
hr = ADsGetObject( pszParent,
|
|
IID_IADsContainer,
|
|
(void **) &pContainer);
|
|
BAIL_ON_FAILURE(hr);
|
|
|
|
// Delete the printqueue
|
|
hr = pContainer->Delete(SPLDS_PRINTER_CLASS, pszCN);
|
|
|
|
|
|
error:
|
|
|
|
if (pADs)
|
|
pADs->Release();
|
|
|
|
if (pContainer)
|
|
pContainer->Release();
|
|
|
|
if (bstrCommonName)
|
|
SysFreeString(bstrCommonName);
|
|
|
|
if (pszParent)
|
|
SysFreeString(pszParent);
|
|
|
|
if (pszCN)
|
|
FreeSplMem(pszCN);
|
|
|
|
if(pszLDAPPath)
|
|
FreeSplMem(pszLDAPPath);
|
|
|
|
return hr;
|
|
}
|
|
|
|
DWORD
|
|
GetCommonName(
|
|
HANDLE hPrinter,
|
|
PWSTR *ppszCommonName
|
|
)
|
|
{
|
|
DWORD nBytes;
|
|
PWSTR psz;
|
|
PPRINTER_INFO_2 pInfo2 = NULL;
|
|
DWORD cbNeeded;
|
|
DWORD dwRet;
|
|
PWSTR pszServerName, pszPrinterName;
|
|
|
|
|
|
// Get Server & Share names
|
|
if (!GetPrinter(hPrinter, 2, (PBYTE) pInfo2, 0, &cbNeeded)) {
|
|
|
|
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
if (!(pInfo2 = (PPRINTER_INFO_2) AllocSplMem(cbNeeded))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
if (!GetPrinter(hPrinter, 2, (PBYTE) pInfo2, cbNeeded, &cbNeeded)) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
pszServerName = pInfo2->pServerName;
|
|
if (!pszServerName) {
|
|
DBGMSG(DBG_ERROR,("GetPrinter returned NULL ServerName"));
|
|
dwRet = ERROR_INVALID_DATA;
|
|
goto error;
|
|
}
|
|
|
|
pszPrinterName = pInfo2->pShareName ? pInfo2->pShareName : pInfo2->pPrinterName;
|
|
|
|
} else {
|
|
// We should never get here. If we do, something is wrong
|
|
// with the server and we have no meaningful error to report,
|
|
// so just claim invalid data.
|
|
DBGMSG(DBG_ERROR,("INVALID GetPrinter return"));
|
|
dwRet = ERROR_INVALID_DATA;
|
|
goto error;
|
|
}
|
|
|
|
// "CN=Server-Printer"
|
|
nBytes = (wcslen(pszPrinterName) + wcslen(pszServerName) + 5)*sizeof(WCHAR);
|
|
if (!(*ppszCommonName = psz = (PWSTR) AllocSplMem(nBytes))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
// CN=
|
|
wcscpy(psz, L"CN=");
|
|
|
|
// Server
|
|
for(psz += 3, pszServerName += 2 ; *pszServerName ; ++psz, ++pszServerName) {
|
|
*psz = wcschr(DN_SPECIAL_CHARS, *pszServerName) ? TEXT('_') : *pszServerName;
|
|
}
|
|
*psz = L'-';
|
|
|
|
// Printer
|
|
for(++psz; *pszPrinterName ; ++psz, ++pszPrinterName) {
|
|
*psz = wcschr(DN_SPECIAL_CHARS, *pszPrinterName) ? TEXT('_') : *pszPrinterName;
|
|
}
|
|
|
|
// NULL
|
|
*psz = *pszPrinterName;
|
|
|
|
// DS only allows 64 characters in CN attribute, so shorten this if needed
|
|
if (wcslen(pszPrinterName) > 62)
|
|
pszPrinterName[63] = NULL;
|
|
|
|
|
|
error:
|
|
|
|
FreeSplMem(pInfo2);
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
|
|
PWSTR
|
|
AllocGlobalStr(
|
|
PWSTR pszIn
|
|
)
|
|
{
|
|
DWORD cb;
|
|
PWSTR pszOut = NULL;
|
|
|
|
if (!pszIn)
|
|
return NULL;
|
|
|
|
cb = (wcslen(pszIn) + 1)*sizeof *pszIn;
|
|
|
|
if (pszOut = (PWSTR) GlobalAlloc(GMEM_FIXED, cb))
|
|
wcscpy(pszOut, pszIn);
|
|
|
|
return pszOut;
|
|
}
|
|
|
|
VOID
|
|
FreeGlobalStr(
|
|
PWSTR pszIn
|
|
)
|
|
{
|
|
if (pszIn)
|
|
GlobalFree(pszIn);
|
|
}
|
|
|
|
|
|
|
|
|
|
DWORD
|
|
GetADsPathFromGUID(
|
|
PCWSTR pszObjectGUID,
|
|
PWSTR *ppszDN
|
|
)
|
|
{
|
|
DWORD dwRet, nBytes, nChars;
|
|
PWSTR pNames[2];
|
|
DS_NAME_RESULT *pDNR = NULL;
|
|
DOMAIN_CONTROLLER_INFO *pDCI = NULL;
|
|
HANDLE hDS = NULL;
|
|
PWSTR psz;
|
|
|
|
|
|
dwRet = Bind2DS(&hDS, &pDCI, DS_DIRECTORY_SERVICE_PREFERRED);
|
|
if (dwRet != ERROR_SUCCESS)
|
|
goto error;
|
|
|
|
|
|
// Get Publish Point
|
|
|
|
if (ppszDN) {
|
|
|
|
pNames[0] = (PWSTR) pszObjectGUID;
|
|
pNames[1] = NULL;
|
|
|
|
if (!(DsCrackNames(
|
|
hDS,
|
|
DS_NAME_NO_FLAGS,
|
|
DS_UNKNOWN_NAME,
|
|
DS_FQDN_1779_NAME,
|
|
1,
|
|
&pNames[0],
|
|
&pDNR) == ERROR_SUCCESS)) {
|
|
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
if (pDNR->rItems[0].status != DS_NAME_NO_ERROR) {
|
|
if (pDNR->rItems[0].status == DS_NAME_ERROR_RESOLVING)
|
|
dwRet = ERROR_PATH_NOT_FOUND;
|
|
else
|
|
dwRet = pDNR->rItems[0].status;
|
|
|
|
goto error;
|
|
}
|
|
|
|
// LDAP:// + pDCName + / + pName + 1
|
|
nBytes = (wcslen(pDCI->DomainControllerName + 2) +
|
|
wcslen(pDNR->rItems[0].pName) + 9)*sizeof(WCHAR);
|
|
|
|
if (!(*ppszDN = (PWSTR) AllocSplMem(nBytes))) {
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
wsprintf(*ppszDN, L"LDAP://%ws/%ws", pDCI->DomainControllerName + 2,pDNR->rItems[0].pName);
|
|
}
|
|
|
|
|
|
error:
|
|
|
|
if (pDNR)
|
|
DsFreeNameResult(pDNR);
|
|
|
|
if (hDS)
|
|
DsUnBind(&hDS);
|
|
|
|
if (pDCI)
|
|
NetApiBufferFree(pDCI);
|
|
|
|
if (dwRet != ERROR_SUCCESS) {
|
|
FreeSplMem(*ppszDN);
|
|
*ppszDN = NULL;
|
|
}
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
|
|
|
|
PWSTR
|
|
GetDNWithServer(
|
|
PCWSTR pszDNIn
|
|
)
|
|
{
|
|
DOMAIN_CONTROLLER_INFO *pDCI = NULL;
|
|
DWORD nBytes;
|
|
PWSTR pszDNOut = NULL;
|
|
DWORD dwRet;
|
|
|
|
// Convert pszDNIn into a DN with the DC name, if it isn't already there
|
|
|
|
// Check for existing DC name or badly formed name
|
|
if (wcsncmp(pszDNIn, L"LDAP://", 7) || wcschr(pszDNIn + 7, L'/'))
|
|
goto error;
|
|
|
|
// Get DC name
|
|
dwRet = DsGetDcName(NULL, NULL, NULL, NULL, 0, &pDCI);
|
|
if (dwRet != ERROR_SUCCESS) {
|
|
goto error;
|
|
}
|
|
|
|
|
|
// Build name
|
|
// LDAP:// + pDCName + / + pName + 1
|
|
nBytes = (wcslen(pDCI->DomainControllerName + 2) +
|
|
wcslen(pszDNIn + 7) + 9)*sizeof(WCHAR);
|
|
|
|
if (!(pszDNOut = (PWSTR) AllocSplMem(nBytes)))
|
|
goto error;
|
|
|
|
wsprintf(pszDNOut, L"LDAP://%ws/%ws", pDCI->DomainControllerName + 2,pszDNIn + 7);
|
|
|
|
|
|
error:
|
|
|
|
if (pDCI)
|
|
NetApiBufferFree(pDCI);
|
|
|
|
return pszDNOut;
|
|
}
|
|
|
|
|
|
DWORD
|
|
hr2dw(
|
|
HRESULT hr
|
|
)
|
|
{
|
|
if (SUCCEEDED(hr))
|
|
return ERROR_SUCCESS;
|
|
|
|
if (HRESULT_FACILITY(hr) == FACILITY_WIN32)
|
|
return HRESULT_CODE(hr);
|
|
|
|
if (hr != HRESULT_FROM_WIN32(ERROR_OBJECT_ALREADY_EXISTS))
|
|
return ERROR_DS_UNAVAILABLE;
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
|
|
PWSTR
|
|
DelimString2MultiSz(
|
|
PWSTR pszIn,
|
|
WCHAR wcDelim
|
|
)
|
|
{
|
|
DWORD cb;
|
|
PWSTR pszOut = NULL;
|
|
|
|
// pszIn looks like L"xxx,xxxx,xxx,xxx"
|
|
// the output looks like L"xxx0xxxx0xxx0xxx00"
|
|
// Replace all wcDelim characters with NULLs & add a NULL
|
|
|
|
if (!pszIn || !*pszIn)
|
|
return NULL;
|
|
|
|
cb = (wcslen(pszIn) + 2)*sizeof *pszIn;
|
|
|
|
pszOut = (PWSTR) AllocSplMem(cb);
|
|
|
|
if (pszOut) {
|
|
|
|
DWORD i;
|
|
|
|
for (i = 0 ; pszIn[i] ; ++i) {
|
|
pszOut[i] = (pszIn[i] == wcDelim) ? L'\0' : pszIn[i];
|
|
}
|
|
pszOut[i] = pszOut[i + 1] = L'\0';
|
|
}
|
|
|
|
return pszOut;
|
|
}
|
|
|
|
|
|
HRESULT
|
|
GetPrinterInfo2(
|
|
HANDLE hPrinter,
|
|
PPRINTER_INFO_2 *ppInfo2
|
|
)
|
|
{
|
|
HRESULT hr = S_OK;
|
|
DWORD cbNeeded;
|
|
|
|
// Get PRINTER_INFO_2 properties
|
|
if (!GetPrinter(hPrinter, 2, (PBYTE) *ppInfo2, 0, &cbNeeded)) {
|
|
|
|
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
|
hr = dw2hr(GetLastError());
|
|
goto error;
|
|
}
|
|
|
|
if (!(*ppInfo2 = (PPRINTER_INFO_2) AllocSplMem(cbNeeded))) {
|
|
hr = dw2hr(GetLastError());
|
|
goto error;
|
|
}
|
|
|
|
if (!GetPrinter(hPrinter, 2, (PBYTE) *ppInfo2, cbNeeded, &cbNeeded)) {
|
|
hr = dw2hr(GetLastError());
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
error:
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
|
|
DWORD
|
|
FQDN2Canonical(
|
|
PWSTR pszIn,
|
|
PWSTR *ppszOut
|
|
)
|
|
{
|
|
DWORD dwRet = ERROR_SUCCESS;
|
|
DS_NAME_RESULT *pDNR = NULL;
|
|
PWSTR pNames[2];
|
|
|
|
|
|
*ppszOut = NULL;
|
|
|
|
if (wcslen(pszIn) < 8) {
|
|
dwRet = ERROR_INVALID_PARAMETER;
|
|
goto error;
|
|
}
|
|
|
|
pNames[0] = pszIn + 7; // Input string is LDAP://CN=... Strip off the LDAP:// portion
|
|
pNames[1] = NULL;
|
|
|
|
if (!(DsCrackNames(
|
|
INVALID_HANDLE_VALUE,
|
|
DS_NAME_FLAG_SYNTACTICAL_ONLY,
|
|
DS_FQDN_1779_NAME,
|
|
DS_CANONICAL_NAME,
|
|
1,
|
|
&pNames[0],
|
|
&pDNR) == ERROR_SUCCESS)) {
|
|
|
|
dwRet = GetLastError();
|
|
goto error;
|
|
}
|
|
|
|
if (pDNR->rItems[0].status != DS_NAME_NO_ERROR) {
|
|
if (pDNR->rItems[0].status == DS_NAME_ERROR_RESOLVING)
|
|
dwRet = ERROR_PATH_NOT_FOUND;
|
|
else
|
|
dwRet = pDNR->rItems[0].status;
|
|
|
|
goto error;
|
|
}
|
|
|
|
*ppszOut = AllocSplStr(pDNR->rItems[0].pName);
|
|
|
|
error:
|
|
|
|
if (pDNR)
|
|
DsFreeNameResult(pDNR);
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
|
|
BOOL
|
|
DevCapMultiSz(
|
|
PWSTR pszUNCName,
|
|
IADs *pPrintQueue,
|
|
WORD fwCapability,
|
|
DWORD dwElementBytes,
|
|
PWSTR pszAttributeName
|
|
)
|
|
{
|
|
DWORD dwResult, cbBytes;
|
|
PWSTR pszDevCapBuffer = NULL;
|
|
PWSTR pszRegData = NULL;
|
|
HRESULT hr;
|
|
|
|
|
|
_try {
|
|
dwResult = DeviceCapabilities( pszUNCName,
|
|
NULL,
|
|
fwCapability,
|
|
NULL,
|
|
NULL);
|
|
|
|
if (dwResult != GDI_ERROR) {
|
|
pszDevCapBuffer = (PWSTR) AllocSplMem(dwResult*dwElementBytes*sizeof(WCHAR));
|
|
|
|
if (pszDevCapBuffer) {
|
|
dwResult = DeviceCapabilities( pszUNCName,
|
|
NULL,
|
|
fwCapability,
|
|
pszDevCapBuffer,
|
|
NULL);
|
|
|
|
if (dwResult != GDI_ERROR) {
|
|
if (!(pszRegData = DevCapStrings2MultiSz(pszDevCapBuffer, dwResult, dwElementBytes, &cbBytes))) {
|
|
dwResult = GDI_ERROR;
|
|
}
|
|
}
|
|
} else {
|
|
dwResult = GDI_ERROR;
|
|
}
|
|
}
|
|
} _except(1) {
|
|
SetLastError(GetExceptionCode());
|
|
dwResult = GDI_ERROR;
|
|
}
|
|
|
|
if (dwResult != GDI_ERROR) {
|
|
|
|
hr = PublishDsData( pPrintQueue,
|
|
pszAttributeName,
|
|
REG_MULTI_SZ,
|
|
(PBYTE) pszRegData);
|
|
|
|
if (FAILED(hr)) {
|
|
SetLastError(HRESULT_CODE(hr));
|
|
dwResult = GDI_ERROR;
|
|
}
|
|
}
|
|
|
|
FreeSplStr(pszDevCapBuffer);
|
|
FreeSplStr(pszRegData);
|
|
|
|
return dwResult != GDI_ERROR;
|
|
}
|
|
|
|
|
|
PWSTR
|
|
DevCapStrings2MultiSz(
|
|
PWSTR pszDevCapString,
|
|
DWORD nDevCapStrings,
|
|
DWORD dwDevCapStringLength,
|
|
DWORD *pcbBytes
|
|
)
|
|
{
|
|
DWORD i, cbBytes, cbSize;
|
|
PWSTR pszMultiSz = NULL;
|
|
PWSTR pStr;
|
|
|
|
|
|
if (!pszDevCapString || !pcbBytes)
|
|
return NULL;
|
|
|
|
*pcbBytes = 0;
|
|
|
|
//
|
|
// Devcap buffers may not be NULL terminated
|
|
//
|
|
cbBytes = (nDevCapStrings*(dwDevCapStringLength + 1) + 1)*sizeof(WCHAR);
|
|
|
|
|
|
//
|
|
// Allocate and copy
|
|
//
|
|
if (pszMultiSz = (PWSTR) AllocSplMem(cbBytes)) {
|
|
for(i = 0, pStr = pszMultiSz, cbBytes = 0 ; i < nDevCapStrings ; ++i, pStr += cbSize, cbBytes +=cbSize ) {
|
|
wcsncpy(pStr, pszDevCapString + i*dwDevCapStringLength, dwDevCapStringLength);
|
|
cbSize = *pStr ? wcslen(pStr) + 1 : 0;
|
|
}
|
|
*pStr = L'\0';
|
|
*pcbBytes = (cbBytes + 1) * sizeof(WCHAR);
|
|
}
|
|
|
|
return pszMultiSz;
|
|
}
|
|
|
|
|
|
HRESULT
|
|
MachineIsInMyForest(
|
|
PWSTR pszMachineName
|
|
)
|
|
{
|
|
DWORD dwRet;
|
|
HRESULT hr;
|
|
PDSROLE_PRIMARY_DOMAIN_INFO_BASIC pMachineRole = NULL;
|
|
PDSROLE_PRIMARY_DOMAIN_INFO_BASIC pMyRole = NULL;
|
|
|
|
dwRet = DsRoleGetPrimaryDomainInformation( pszMachineName,
|
|
DsRolePrimaryDomainInfoBasic,
|
|
(PBYTE *) &pMachineRole);
|
|
if (dwRet != ERROR_SUCCESS) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, dwRet);
|
|
goto error;
|
|
}
|
|
|
|
dwRet = DsRoleGetPrimaryDomainInformation( NULL,
|
|
DsRolePrimaryDomainInfoBasic,
|
|
(PBYTE *) &pMyRole);
|
|
if (dwRet != ERROR_SUCCESS) {
|
|
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, dwRet);
|
|
goto error;
|
|
}
|
|
|
|
dwRet = DnsNameCompare_W(pMachineRole->DomainForestName, pMyRole->DomainForestName);
|
|
hr = MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_WIN32, dwRet);
|
|
|
|
|
|
error:
|
|
|
|
if (pMachineRole)
|
|
DsRoleFreeMemory((PVOID) pMachineRole);
|
|
|
|
if (pMyRole)
|
|
DsRoleFreeMemory((PVOID) pMyRole);
|
|
|
|
return hr;
|
|
}
|