// Copyright (C) 2000 Microsoft Corporation
//
// Non-domain Naming Context checking code
//
// 13 July 2000 sburns, from code supplied by jeffparh



#include "headers.hxx"
#include "state.hpp"
#include "resource.h"
#include "NonDomainNc.hpp"



#ifdef LOGGING_BUILD
   #define LOG_LDAP(msg, ldap) LOG(msg); LOG(String::format(L"LDAP error %1!ld!", (ldap)))
#else
   #define LOG_LDAP(msg, ldap)
#endif



HRESULT
LdapToHresult(int ldapError)
{
   // CODEWORK: I'm told that ldap_get_option for LDAP_OPT_SERVER_ERROR or
   // LDAP_OPT_SERVER_EXT_ERROR (or perhaps LDAP_OPT_ERROR_STRING?) will give
   // an error result with "higher fidelity"
   
   return Win32ToHresult(::LdapMapErrorToWin32(ldapError));
}



// provided by jeffparh

DWORD
IsLastReplicaOfNC(
    IN  LDAP *  hld,
    IN  LPWSTR  pszConfigNC,
    IN  LPWSTR  pszNC,
    IN  LPWSTR  pszNtdsDsaDN,
    OUT BOOL *  pfIsLastReplica
    )
/*++

Routine Description:

    Determine whether any DSAs other than that with DN pszNtdsDsaDN hold
    replicas of a particular NC.

Arguments:

    hld (IN) - LDAP handle to execute search with.
    
    pszConfigNC (IN) - DN of the config NC.  Used as a base for the search.
    
    pszNC (IN) - NC for which to check for other replicas.
    
    pszNtdsDsaDN (IN) - DN of the DSA object known to currently have a replica
        of the NC.  We are specifically looking for replicas *other than* this
        one.
        
    pfIsLastReplica (OUT) - On successful return, TRUE iff no DSAs hold replicas
        of pszNC other than that with DN pszNtdsDsaDN.

Return Values:

    Win error.

--*/
{
   LOG_FUNCTION2(IsLastReplicaOfNC, pszNC ? pszNC : L"(null)");
   ASSERT(hld);
   ASSERT(pszConfigNC);
   ASSERT(pszNC);
   ASSERT(pszNtdsDsaDN);
   ASSERT(pfIsLastReplica);

   if (
         !hld
      || !pszConfigNC
      || !pszNC
      || !pszNtdsDsaDN
      || !pfIsLastReplica)
   {
      return ERROR_INVALID_PARAMETER;
   }

    // Just checking for existence -- don't really want any attributes
    // returned.

    static LPWSTR rgpszDsaAttrsToRead[] = {
        L"__invalid_attribute_name__",
        NULL
    };

    static WCHAR szFilterFormat[]
        = L"(&(objectCategory=ntdsDsa)(hasMasterNCs=%ls)(!(distinguishedName=%ls)))";

   *pfIsLastReplica = TRUE;
   
    int                 ldStatus = 0;
    DWORD               err = 0;
    LDAPMessage *       pDsaResults = NULL;
    LDAPMessage *       pDsaEntry = NULL;
    size_t              cchFilter;
    PWSTR               pszFilter;
    LDAP_TIMEVAL        lTimeout = {3*60, 0};   // three minutes

   do
   {
        cchFilter = sizeof(szFilterFormat) / sizeof(*szFilterFormat)
                    + wcslen(pszNtdsDsaDN)
                    + wcslen(pszNC);

        pszFilter = (PWSTR) new BYTE[sizeof(WCHAR) * cchFilter];

        swprintf(pszFilter, szFilterFormat, pszNC, pszNtdsDsaDN);

        // Search config NC for any ntdsDsa object that hosts this NC other
        // than that with dn pszNtdsDsaDN.  Note that we cap the search at one
        // returned object -- we're not really trying to enumerate, just
        // checking for existence.

        ldStatus = ldap_search_ext_sW(hld, pszConfigNC, LDAP_SCOPE_SUBTREE,
                                      pszFilter, rgpszDsaAttrsToRead, 0,
                                      NULL, NULL, &lTimeout, 1, &pDsaResults);
        if (pDsaResults)
        {
            // Ignore any error (such as LDAP_SIZELIMIT_EXCEEDED) when the
            // search returns results.

            ldStatus = 0;
            
            pDsaEntry = ldap_first_entry(hld, pDsaResults);
            
            *pfIsLastReplica = (NULL == pDsaEntry);
        } else if (ldStatus)
        {
            // Search failed and returned no results.

            LOG_LDAP(L"Config NC search failed", ldStatus);
            break;
        } else
        {
            // No error, no results.  This shouldn't happen.

            LOG("ldap_search_ext_sW returned no results and no error!");
            ASSERT(false);
        }
   }
   while (0);

   if (NULL != pDsaResults) {
      ldap_msgfree(pDsaResults);
   }

   if (pszFilter)
   {
      delete[] pszFilter;
   }

   if (!err && ldStatus) {
     err = LdapMapErrorToWin32(ldStatus);
   }
    
   return err;
}



// S_OK if this machine (the localhost) is the last replica of at least one
// non-domain NC, S_FALSE if not, or error otherwise.  If S_OK, then the
// StringList will contain the DNs of the non domain NCs for which this
// machine is the last replica.
//
// based on code from jeffparh
//
// hld (IN) - LDAP handle bound to DSA to evaluate.
//
// result (OUT) - string list to receive DNs of the non-domain NCs.

HRESULT
IsLastNdncReplica(LDAP* hld, StringList& result)
{
   LOG_FUNCTION(IsLastNdncReplica);
   ASSERT(hld);
   ASSERT(result.empty());

   HRESULT      hr          = S_FALSE;
   LDAPMessage* rootResults = 0;      
   PWSTR*       configNc    = 0;      
   PWSTR*       schemaNc    = 0;      
   PWSTR*       domainNc    = 0;      
   PWSTR*       masterNcs   = 0;      
   PWSTR*       ntdsDsaDn   = 0;      

   do
   {
      // Gather basic rootDSE info.

      static PWSTR ROOT_ATTRS_TO_READ[] =
      {
         LDAP_OPATT_NAMING_CONTEXTS_W,
         LDAP_OPATT_DEFAULT_NAMING_CONTEXT_W,
         LDAP_OPATT_CONFIG_NAMING_CONTEXT_W,
         LDAP_OPATT_SCHEMA_NAMING_CONTEXT_W,
         LDAP_OPATT_DS_SERVICE_NAME_W,
         0
      };

      LOG(L"Calling ldap_search_s");

      int ldStatus =
         ldap_search_sW(
            hld,
            0,
            LDAP_SCOPE_BASE,
            L"(objectClass=*)",
            ROOT_ATTRS_TO_READ,
            0,
            &rootResults);
      if (ldStatus)
      {
         LOG_LDAP(L"RootDSE search failed", ldStatus);
         hr = LdapToHresult(ldStatus);
         break;
      }

      configNc  = ldap_get_valuesW(hld, rootResults, LDAP_OPATT_CONFIG_NAMING_CONTEXT_W); 
      schemaNc  = ldap_get_valuesW(hld, rootResults, LDAP_OPATT_SCHEMA_NAMING_CONTEXT_W); 
      domainNc  = ldap_get_valuesW(hld, rootResults, LDAP_OPATT_DEFAULT_NAMING_CONTEXT_W);
      masterNcs = ldap_get_valuesW(hld, rootResults, LDAP_OPATT_NAMING_CONTEXTS_W);       
      ntdsDsaDn = ldap_get_valuesW(hld, rootResults, LDAP_OPATT_DS_SERVICE_NAME_W);       

      if (
            (0 == configNc)
         || (0 == schemaNc)
         || (0 == domainNc)
         || (0 == masterNcs)
         || (0 == ntdsDsaDn))
      {
         LOG(L"Can't find key rootDSE attributes!");

         hr = Win32ToHresult(ERROR_DS_UNAVAILABLE);
         break;
      }

      // There is only one value for each of these attributes...

      ASSERT(1 == ldap_count_valuesW(configNc));
      ASSERT(1 == ldap_count_valuesW(schemaNc));
      ASSERT(1 == ldap_count_valuesW(domainNc));
      ASSERT(1 == ldap_count_valuesW(ntdsDsaDn));

      DWORD masterNcCount = ldap_count_valuesW(masterNcs);
      
      LOG(String::format(L"masterNcCount = %1!d!", masterNcCount));
         
      // '3' => 1 nc for config, 1 nc for schema, 1 nc for this DC's own
      // domain.

      if (masterNcCount <= 3)
      {
         // DSA holds no master NCs other than config, schema, and its own
         // domain.  Thus, it is not the last replica of any NDNC.

         LOG(L"This dsa holds no master NCs other than config, schema, and domain");
         
         ASSERT(3 == masterNcCount);
         ASSERT(0 == ldStatus);
         ASSERT(hr == S_FALSE);

         break;
      }

      // Loop through non-config/schema/domain NCs to determine those for
      // which the DSA is the last replica.

      for (int i = 0; 0 != masterNcs[i]; ++i)
      {
         PWSTR nc = masterNcs[i];

         LOG(L"Evaluating " + String(nc));

         if (
                (0 != wcscmp(nc, *configNc))
             && (0 != wcscmp(nc, *schemaNc))
             && (0 != wcscmp(nc, *domainNc)))
         {
            // A non-config/schema/domain NC.

            LOG(L"Calling IsLastReplicaOfNC on " + String(nc));

            BOOL isLastReplica = FALSE;
            DWORD err =
               IsLastReplicaOfNC(
                  hld,
                  *configNc,
                  nc,
                  *ntdsDsaDn,
                  &isLastReplica);
            if (err)
            {
               LOG(L"IsLastReplicaOfNC() failed");

               hr = Win32ToHresult(err);
               break;
            }

            if (isLastReplica)
            {
               // This DSA is indeed the last replica of this particular
               // NC.  Return the DN of this NC to our caller.

               LOG(L"last replica of " + String(nc));

               result.push_back(nc);
            }
            else
            {
               LOG(L"not last replica of " + String(nc));
            }
         }
      }

      // If we broke out of the prior loop with an error, jump out to the
      // cleanup section.

      BREAK_ON_FAILED_HRESULT(hr);

      hr = result.size() > 0 ? S_OK : S_FALSE;
   }
   while (0);

   if (rootResults)
   {
      ldap_msgfree(rootResults);
   }
   
   if (0 != configNc)
   {
      ldap_value_freeW(configNc);
   }

   if (0 != schemaNc)
   {
      ldap_value_freeW(schemaNc);
   }

   if (0 != domainNc)
   {
      ldap_value_freeW(domainNc);
   }

   if (0 != masterNcs)
   {
      ldap_value_freeW(masterNcs);
   }

   if (0 != ntdsDsaDn)
   {
      ldap_value_freeW(ntdsDsaDn);
   }

#ifdef LOGGING_BUILD
   LOG_HRESULT(hr);

   for (
      StringList::iterator i = result.begin();
      i != result.end();
      ++i)
   {
      LOG(*i);
   }
#endif

   return hr;
}



// S_OK if this machine (the localhost) is the last replica of at least one
// non-domain NC, S_FALSE if not, or error otherwise.
//
// result - If S_OK is returned, receives the DNs of the non domain NCs for
// which this machine is the last replica.  Should be empty on entry.

HRESULT
IsLastNonDomainNamingContextReplica(StringList& result)
{
   LOG_FUNCTION(IsLastNonDomainNamingContextReplica);
   ASSERT(result.empty());

   result.clear();

   HRESULT hr  = S_FALSE;
   LDAP*   hld = 0;   

   do
   {
      // Connect to target DSA.

      LOG(L"Calling ldap_open");

      hld = ldap_openW(L"localhost", LDAP_PORT);
      if (!hld)
      {
         LOG("Cannot open LDAP connection to localhost");
         hr = Win32ToHresult(ERROR_DS_UNAVAILABLE);
         break;
      }

      // Bind using logged-in user's credentials.

      int ldStatus = ldap_bind_s(hld, 0, 0, LDAP_AUTH_NEGOTIATE);
      if (ldStatus)
      {
         LOG_LDAP(L"LDAP bind failed", ldStatus);

         hr = LdapToHresult(ldStatus);
         break;
      }

      // go do the real work
          
      hr = IsLastNdncReplica(hld, result);
   }
   while (0);

   if (hld)
   {
      ldap_unbind(hld);
   }

   LOG_HRESULT(hr);

   return hr;
}



static const DWORD HELP_MAP[] =
{
   0, 0
};



NonDomainNcErrorDialog::NonDomainNcErrorDialog(StringList& ndncList_)
   :
   Dialog(IDD_NON_DOMAIN_NC_ERROR, HELP_MAP),
   ndncList(ndncList_),
   warnIcon(0)
{
   LOG_CTOR(NonDomainNcErrorDialog);

   ASSERT(ndncList.size());
}



NonDomainNcErrorDialog::~NonDomainNcErrorDialog()
{
   LOG_DTOR(NonDomainNcErrorDialog);

   if (warnIcon)
   {
      Win::DestroyIcon(warnIcon);
   }
}



void
NonDomainNcErrorDialog::OnInit()
{
   LOG_FUNCTION(NonDomainNcErrorDialog::OnInit);

   // set the warning icon NTRAID#NTBUG9-239678-2000/11/28-sburns
   
   HRESULT hr = Win::LoadImage(IDI_WARN, warnIcon);
   ASSERT(SUCCEEDED(hr));

   Win::SendMessage(
      Win::GetDlgItem(hwnd, IDC_WARNING_ICON),
      STM_SETICON,
      reinterpret_cast<WPARAM>(warnIcon),
      0);

   PopulateListView();
}



// Unwraps the safearray of variants inside a variant, extracts the strings
// inside them, and catenates the strings together with semicolons in between.
// Return empty string on error.
// 
// variant - in, the variant containing a safearray of variants of bstr.

String
GetNdncDescriptionHelper(VARIANT* variant)
{
   LOG_FUNCTION(GetNdncDescriptionHelper);
   ASSERT(variant);
   ASSERT(V_VT(variant) == (VT_ARRAY | VT_VARIANT));

   String result;

   SAFEARRAY* psa = V_ARRAY(variant);

   do
   {
      ASSERT(psa);
      ASSERT(psa != (SAFEARRAY*)-1);

      if (!psa or psa == (SAFEARRAY*)-1)
      {
         LOG(L"variant not safe array");
         break;
      }

      if (::SafeArrayGetDim(psa) != 1)
      {
         LOG(L"safe array: wrong number of dimensions");
         break;
      }

      VARTYPE vt = VT_EMPTY;
      HRESULT hr = ::SafeArrayGetVartype(psa, &vt);
      if (FAILED(hr) || vt != VT_VARIANT)
      {
         LOG(L"safe array: wrong element type");
         break;
      }

      long lower = 0;
      long upper = 0;
     
      hr = ::SafeArrayGetLBound(psa, 1, &lower);
      BREAK_ON_FAILED_HRESULT2(hr, L"can't get lower bound");      

      hr = ::SafeArrayGetUBound(psa, 1, &upper);
      BREAK_ON_FAILED_HRESULT2(hr, L"can't get upper bound");      
      
      VARIANT varItem;
      ::VariantInit(&varItem);

      for (long i = lower; i <= upper; ++i)
      {
         hr = ::SafeArrayGetElement(psa, &i, &varItem);
         if (FAILED(hr))
         {
            LOG(String::format(L"index %1!d! failed", i));
            continue;
         }

         result += V_BSTR(&varItem);

         if (i < upper)
         {   
            result += L";";
         }

         ::VariantClear(&varItem);
      }
      
   }
   while (0);

   LOG(result);
   
   return result;   
}



// bind to an ndnc, read it's description(s), and return them catenated
// together.  Return empty string on error.
// 
// ndncDn - in, DN of the ndnc

String
GetNdncDescription(const String& ndncDn)
{
   LOG_FUNCTION2(GetNdncDescription, ndncDn);
   ASSERT(!ndncDn.empty());

   String result;

   do
   {
      String path = L"LDAP://" + ndncDn;

      SmartInterface<IADs> iads(0);
      IADs* dumb = 0;
      
      HRESULT hr =
         ::ADsGetObject(
            path.c_str(),
            __uuidof(iads),
            reinterpret_cast<void**>(&dumb));
      BREAK_ON_FAILED_HRESULT2(hr, L"ADsGetObject failed on " + path);

      iads.Acquire(dumb);

      // description is a multivalued attrbute for no apparent good reason.
      // so we need to walk an array of values.
      
      _variant_t variant;
      hr = iads->GetEx(AutoBstr(L"description"), &variant);
      BREAK_ON_FAILED_HRESULT2(hr, L"read description failed");

      result = GetNdncDescriptionHelper(&variant);
   }
   while (0);

   LOG(result);

   return result;
}



// Build a list view with two columns, one for the DN of the ndncs for which
// this box is the last replica, another for the description(s) of those
// ndncs.

void
NonDomainNcErrorDialog::PopulateListView()
{
   LOG_FUNCTION(NonDomainNcErrorDialog::PopulateListView);

   HWND view = Win::GetDlgItem(hwnd, IDC_NDNC_LIST);

   // add a column to the list view for the DN
      
   LVCOLUMN column;
   ::ZeroMemory(&column, sizeof column);

   column.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
   column.fmt = LVCFMT_LEFT;

   int width = 0;
   String::load(IDS_NDNC_LIST_NAME_COLUMN_WIDTH).convert(width);
   column.cx = width;

   String label = String::load(IDS_NDNC_LIST_NAME_COLUMN);
   column.pszText = const_cast<wchar_t*>(label.c_str());

   Win::ListView_InsertColumn(view, 0, column);

   // add a column to the list view for description.

   String::load(IDS_NDNC_LIST_DESC_COLUMN_WIDTH).convert(width);
   column.cx = width;
   label = String::load(IDS_NDNC_LIST_DESC_COLUMN);
   column.pszText = const_cast<wchar_t*>(label.c_str());

   Win::ListView_InsertColumn(view, 1, column);
   
   // Load up the edit box with the DNs we aliased in the ctor.

   LVITEM item;
   ::ZeroMemory(&item, sizeof item);

   for (
      StringList::iterator i = ndncList.begin();
      i != ndncList.end();
      ++i)
   {
      item.mask     = LVIF_TEXT;
      item.pszText  = const_cast<wchar_t*>(i->c_str());
      item.iSubItem = 0;

      item.iItem = Win::ListView_InsertItem(view, item);

      // add the description sub-item to the list control

      String description = GetNdncDescription(*i);
      
      item.mask = LVIF_TEXT; 
      item.pszText = const_cast<wchar_t*>(description.c_str());
      item.iSubItem = 1;
      
      Win::ListView_SetItem(view, item);
   }
}



bool
NonDomainNcErrorDialog::OnCommand(
   HWND        /* windowFrom */ ,
   unsigned    controlIDFrom,
   unsigned    code)
{
//   LOG_FUNCTION(NonDomainNcErrorDialog::OnCommand);

   if (code == BN_CLICKED)
   {
      switch (controlIDFrom)
      {
         case IDOK:
         case IDCANCEL:
         {
            Win::EndDialog(hwnd, controlIDFrom);
            return true;
         }

         // NTRAID#NTBUG9-239678-2000/11/28-sburns
         
         case IDC_SHOW_HELP:
         {
            if (code == BN_CLICKED)
            {
               Win::HtmlHelp(
                  hwnd,
                  L"adconcepts.chm::/ADHelpDemoteWithNDNC.htm",
                  HH_DISPLAY_TOPIC,
                  0);
               return true;
            }
            break;
         }
         default:
         {
            // do nothing
         }
      }
   }

   return false;
}

      


bool
IsLastReplicaOfNonDomainNamingContexts()
{
   LOG_FUNCTION(IsLastReplicaOfNonDomainNamingContexts);

   bool result = false;

   do
   {
      State::RunContext context = State::GetInstance().GetRunContext();
      if (context != State::NT5_DC)
      {
         // not a DC, so can't be replica of any NCs

         LOG(L"not a DC");
         break;
      }

      // Find the list of non-domain NCs that this DC is the last replica
      // (if any).  If we find some, gripe at the user.

      StringList ndncList;
      HRESULT hr = IsLastNonDomainNamingContextReplica(ndncList);
      if (FAILED(hr))
      {
         popup.Error(
            Win::GetDesktopWindow(),
            hr,
            IDS_FAILED_TO_READ_NDNC_INFO);

         result = true;
         break;
      }

      if (hr == S_FALSE)
      {
         LOG(L"Not last replica of non-domain NCs");
         ASSERT(result == false);
         break;
      }

      result = true;
         
      // there should be at least one DN in the list.

      ASSERT(ndncList.size());

      NonDomainNcErrorDialog(ndncList).ModalExecute(Win::GetDesktopWindow());
   }
   while (0);

   LOG(result ? L"true" : L"false");

   return result;
}