/*++

   Copyright    (c)    1994-2001    Microsoft Corporation

   Module  Name :
        usersess.cpp

   Abstract:
        FTP User Sessions Dialog

   Author:
        Ronald Meijer (ronaldm)
        Sergei Antonov (sergeia)

   Project:
        Internet Services Manager

   Revision History:

--*/

//
// Include Files
//
#include "stdafx.h"
#include "resource.h"
#include "common.h"
#include "inetprop.h"
#include "ftpsht.h"
#include "fservic.h"
#include "usersess.h"

#include <lmerr.h>

HRESULT ImpersonateUser(LPCTSTR  pszUserName,
                        LPCTSTR  pszDomain,
                        LPCTSTR  pszPassword,
                        HANDLE  *pCurImpToken,
                        HANDLE  *pLoggedOnUserToken);
HRESULT UnImpersonateUser(HANDLE    hSavedImpToken,
                          HANDLE    hLoggedOnUser);


#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif



//
// Registry key name for this dialog
//
const TCHAR g_szRegKey[] = _T("User Sessions");

//
// User Sessions Listbox Column Definitions
//
static const ODL_COLUMN_DEF_EX BASED_CODE g_aColumns[] =
{
// ==================================================================================================
// Weight      Label          Sort Helper Function
// ==================================================================================================
    { 2, IDS_CONNECTED_USERS, (CObjectPlus::PCOBJPLUS_ORDER_FUNC)&CFtpUserInfo::OrderByName        },
    { 1, IDS_FROM,            (CObjectPlus::PCOBJPLUS_ORDER_FUNC)&CFtpUserInfo::OrderByHostAddress },
    { 1, IDS_TIME,            (CObjectPlus::PCOBJPLUS_ORDER_FUNC)&CFtpUserInfo::OrderByTime        },
};


#define NUM_COLUMNS (sizeof(g_aColumns) / sizeof(g_aColumns[0]))



CFtpUserInfo::CFtpUserInfo(LPIIS_USER_INFO_1 lpUserInfo)
/*++

Routine Description:

    Constructor

Arguments:

    LPIIS_USER_INFO_1 lpUserInfo : User info structure

Return Value:

    N/A

--*/
    : m_idUser(lpUserInfo->idUser),
      m_strUser(lpUserInfo->pszUser),
      m_fAnonymous(lpUserInfo->fAnonymous),
      //                    Network Byte Order
      //                              ||
      //                              \/
      m_iaHost(lpUserInfo->inetHost, TRUE),
      m_tConnect(lpUserInfo->tConnect)
{
}



int
CFtpUserInfo::OrderByName(
    const CObjectPlus * pobFtpUser
    ) const
/*++

Routine Description:

    Sorting helper function to sort by user name.  The CObjectPlus pointer 
    really refers to another CFtpUserInfo object

Arguments:

    LPIIS_USER_INFO_1 lpUserInfo : User info structure

Return Value:

    Sort return code (-1, 0, +1)

--*/
{
    const CFtpUserInfo * pob = (CFtpUserInfo *)pobFtpUser;
    ASSERT(pob != NULL);

    return ::lstrcmpi(QueryUserName(), pob->QueryUserName());
}



int
CFtpUserInfo::OrderByTime(
    const CObjectPlus * pobFtpUser
    ) const
/*++

Routine Description:

    Sorting helper function to sort by user connect time.  The CObjectPlus 
    pointer really refers to another CFtpUserInfo object

Arguments:

    LPIIS_USER_INFO_1 lpUserInfo : User info structure

Return Value:

    Sort return code (-1, 0, +1)

--*/
{
    const CFtpUserInfo * pob = (CFtpUserInfo *)pobFtpUser;
    ASSERT(pob != NULL);

    return QueryConnectTime() > pob->QueryConnectTime()
        ? +1
        : QueryConnectTime() == pob->QueryConnectTime()
            ? 0
            : -1;
}



int
CFtpUserInfo::OrderByHostAddress(
    const CObjectPlus * pobFtpUser
    ) const
/*++

Routine Description:

    Sorting helper function to sort by host address.  The CObjectPlus 
    pointer really refers to another CFtpUserInfo object

Arguments:

    LPIIS_USER_INFO_1 lpUserInfo : User info structure

Return Value:

    Sort return code (-1, 0, +1)

--*/
{
    const CFtpUserInfo * pob = (CFtpUserInfo *)pobFtpUser;
    ASSERT(pob != NULL);

    return QueryHostAddress().CompareItem(pob->QueryHostAddress());
}



IMPLEMENT_DYNAMIC(CFtpUsersListBox, CHeaderListBox);



//
// User listbox bitmaps
//
enum
{
    BMP_USER = 0,
    BMP_ANONYMOUS,

    //
    // Don't move this one
    //
    BMP_TOTAL
};

const int CFtpUsersListBox::nBitmaps = BMP_TOTAL;



CFtpUsersListBox::CFtpUsersListBox()
    : m_strTimeSep(_T(":")),
      CHeaderListBox(HLS_DEFAULT, g_szRegKey)
{
    //
    // Get intl time seperator
    //
    VERIFY(::GetLocaleInfo(
        ::GetUserDefaultLCID(), LOCALE_STIME, 
        m_strTimeSep.GetBuffer(10), 10));
}



void
CFtpUsersListBox::DrawItemEx(
    IN CRMCListBoxDrawStruct & ds
    )
/*++

Routine Description:

    Draw item.  This is called from the CRMCListBox base class

Arguments:

    CRMCListBoxDrawStruct & ds : Drawing structure

Return Value:

    None

--*/
{
    CFtpUserInfo * pFTPUser = (CFtpUserInfo *)ds.m_ItemData;
    ASSERT(pFTPUser != NULL);

    //
    // Display a user bitmap
    //
    DrawBitmap(ds, 0, pFTPUser->QueryAnonymous() ? BMP_ANONYMOUS : BMP_USER);
    ColumnText(ds, 0, TRUE, pFTPUser->QueryUserName());
    ColumnText(ds, 1, FALSE, pFTPUser->QueryHostAddress());

    DWORD dwTime = pFTPUser->QueryConnectTime();
    DWORD dwHours = dwTime / (60L * 60L);
    DWORD dwMinutes = (dwTime / 60L) % 60L;
    DWORD dwSeconds = dwTime % 60L;

    CString strTime;

    strTime.Format(
        _T("%d%s%02d%s%02d"),
        dwHours, (LPCTSTR)m_strTimeSep,
        dwMinutes, (LPCTSTR)m_strTimeSep,
        dwSeconds);

    ColumnText(ds, 2, FALSE, strTime);
}



/* virtual */
BOOL 
CFtpUsersListBox::Initialize()
/*++

Routine Description:

    Initialize the listbox.  Insert the columns as requested, and lay 
    them out appropriately

Arguments:

    None

Return Value:

    TRUE for succesful initialisation, FALSE otherwise

--*/
{
    if (!CHeaderListBox::Initialize())
    {
        return FALSE;
    }

    //
    // Build all columns
    //
    HINSTANCE hInst = AfxGetResourceHandle();
    for (int nCol = 0; nCol < NUM_COLUMNS; ++nCol)
    {
        InsertColumn(
            nCol, 
            g_aColumns[nCol].cd.nWeight, 
            g_aColumns[nCol].cd.nLabelID,
            hInst
            );
    }

    //
    // Try to set the widths from the stored registry value,
    // otherwise distribute according to column weights specified
    //
//    if (!SetWidthsFromReg())
//    {
        DistributeColumns();
//    }

    return TRUE;
}



CUserSessionsDlg::CUserSessionsDlg(
    LPCTSTR lpstrServerName,
    DWORD dwInstance,
    LPCTSTR pAdminName,
    LPCTSTR pAdminPassword,
    CWnd * pParent
    )
/*++

Routine Description:

    Constructor for FTP user sessions dialog

Arguments:

    LPCTSTR lpstrServerName : Server name to connect to
    CWnd * pParent          : Pointer to parent window

Return Value:

    N/A

--*/
    : m_list_Users(),
      m_ListBoxRes(IDB_USERS, m_list_Users.nBitmaps),
      m_oblFtpUsers(),
      m_strServerName(lpstrServerName),
      m_strAdminName(pAdminName),
      m_strAdminPassword(pAdminPassword),
      m_nSortColumn(0),
      m_dwInstance(dwInstance),
      m_hImpToken(INVALID_HANDLE_VALUE),
      m_hLogToken(INVALID_HANDLE_VALUE),
      CDialog(CUserSessionsDlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(CUserSessionsDlg)
    //}}AFX_DATA_INIT

    m_list_Users.AttachResources(&m_ListBoxRes);
    VERIFY(m_strTotalConnected.LoadString(IDS_USERS_TOTAL));
}



void 
CUserSessionsDlg::DoDataExchange(CDataExchange * pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CUserSessionsDlg)
    DDX_Control(pDX, IDC_STATIC_NUM_CONNECTED, m_static_Total);
    DDX_Control(pDX, IDC_BUTTON_DISCONNECT_ALL, m_button_DisconnectAll);
    DDX_Control(pDX, IDC_BUTTON_DISCONNECT, m_button_Disconnect);
    //}}AFX_DATA_MAP

    DDX_Control(pDX, IDC_LIST_USERS, m_list_Users);
}



//
// Message Map
//
BEGIN_MESSAGE_MAP(CUserSessionsDlg, CDialog)
    //{{AFX_MSG_MAP(CUserSessionsDlg)
    ON_BN_CLICKED(IDC_BUTTON_DISCONNECT, OnButtonDisconnect)
    ON_BN_CLICKED(IDC_BUTTON_DISCONNECT_ALL, OnButtonDisconnectAll)
    ON_BN_CLICKED(IDC_BUTTON_REFRESH, OnButtonRefresh)
    ON_LBN_SELCHANGE(IDC_LIST_USERS, OnSelchangeListUsers)
    //}}AFX_MSG_MAP

    ON_NOTIFY_RANGE(HDN_ITEMCLICK, 0, 0xFFFF, OnHeaderItemClick)

END_MESSAGE_MAP()



DWORD
CUserSessionsDlg::SortUsersList()
/*++

Routine Description:

    Sort the list of ftp users on the current sorting key

Arguments:

    None

Return Value:

    ERROR return code

--*/
{
    ASSERT(m_nSortColumn >= 0 && m_nSortColumn < NUM_COLUMNS);

    BeginWaitCursor();              
    DWORD err = m_oblFtpUsers.Sort(
        (CObjectPlus::PCOBJPLUS_ORDER_FUNC)g_aColumns[m_nSortColumn].pSortFn);
    EndWaitCursor();

    return err;
}



HRESULT
CUserSessionsDlg::BuildUserList()
/*++

Routine Description:

    Call the FtpEnum api and build the list of currently connected users.

Arguments:

    None

Return Value:

    ERROR return code

--*/
{
    CError err;
    LPIIS_USER_INFO_1 lpUserInfo = NULL;
    DWORD dwCount = 0L;

    m_oblFtpUsers.RemoveAll();

    BeginWaitCursor();

    err = ::IISEnumerateUsers(
        (LPTSTR)(LPCTSTR)m_strServerName,
        1,
        INET_FTP_SVC_ID,
        m_dwInstance,
        &dwCount,
        (LPBYTE *)&lpUserInfo
        );

    EndWaitCursor();

    TRACEEOLID("IISEnumerateUsers returned " << err);

    if (err.Failed())
    {
        return err;
    }

    try
    {
        for (DWORD i = 0; i < dwCount; ++i)
        {
            m_oblFtpUsers.AddTail(new CFtpUserInfo(lpUserInfo++));
        }
    }
    catch(CMemoryException * e)
    {
        err = ERROR_NOT_ENOUGH_MEMORY;
        e->Delete();
    }

    SortUsersList();

    return err;
}



HRESULT
CUserSessionsDlg::DisconnectUser(CFtpUserInfo * pUserInfo)
/*++

Routine Description:

    Disconnect a single user

Arguments:

    CFtpUserInfo * pUserInfo : User to disconnect

Return Value:

    ERROR return code

--*/
{
    CError err(::IISDisconnectUser(
        (LPTSTR)(LPCTSTR)m_strServerName,
        INET_FTP_SVC_ID, 
        m_dwInstance, 
        pUserInfo->QueryUserID()
        ));

    if (err.Win32Error() == NERR_UserNotFound)
    {
        //
        // As long as he's gone now, that's alright
        //
        err.Reset();
    }

    return err;
}



void 
CUserSessionsDlg::UpdateTotalCount()
/*++

Routine Description:

    Update the count of total users

Arguments:

    None

Return Value:

    None

--*/
{
    CString str;
    str.Format(m_strTotalConnected, m_oblFtpUsers.GetCount() );

    m_static_Total.SetWindowText(str);     
}



void
CUserSessionsDlg::FillListBox(CFtpUserInfo * pSelection)
/*++

Routine Description:

    Show the users in the listbox

Arguments:

    CFtpUserInfo * pSelection : Item to be selected or NULL

Return Value:

    None

--*/
{
    CObListIter obli(m_oblFtpUsers);
    const CFtpUserInfo * pUserEntry = NULL;

    m_list_Users.SetRedraw(FALSE);
    m_list_Users.ResetContent();
    int cItems = 0;

    for ( /**/ ; pUserEntry = (CFtpUserInfo *)obli.Next(); ++cItems)
    {
        m_list_Users.AddItem(pUserEntry);
    }

    if (pSelection)
    {
        //
        // Select the desired entry
        //
        m_list_Users.SelectItem(pSelection);
    }

    m_list_Users.SetRedraw(TRUE);

    //
    // Update the count text on the dialog
    //
    UpdateTotalCount();
}



HRESULT
CUserSessionsDlg::RefreshUsersList()
/*++

Routine Description:

    Rebuild the user list

Arguments:

    None

Return Value:

    Error return code

--*/
{
    CError err;

    //
    // Add some friendly error message overrides
    //
    err.AddOverride(EPT_S_NOT_REGISTERED,       IDS_ERR_RPC_NA);
    err.AddOverride(RPC_S_SERVER_UNAVAILABLE,   IDS_FTP_SERVICE_NOT_STARTED);
    err.AddOverride(RPC_S_UNKNOWN_IF,           IDS_FTP_SERVICE_NOT_STARTED);
    err.AddOverride(RPC_S_PROCNUM_OUT_OF_RANGE, IDS_ERR_INTERFACE);

	err = BuildUserList();

    if (!err.MessageBoxOnFailure())
    {
        FillListBox();
        SetControlStates();
    }

    return err;
}



void
CUserSessionsDlg::SetControlStates()
/*++

Routine Description:

    Set the connect/disconnect buttons depending on the selection state
    in the listbox.

Arguments:

    None

Return Value:

    None

--*/
{
    m_button_Disconnect.EnableWindow(m_list_Users.GetSelCount() > 0);
    m_button_DisconnectAll.EnableWindow(m_list_Users.GetCount() > 0);
}



//
// Message Handlers
//
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<



void 
CUserSessionsDlg::OnButtonDisconnect() 
/*++

Routine Description:

    'Disconnect User' button has been pressed.  Disconnect the currently
    selected user.

Arguments:

    None

Return Value:

    None

--*/
{
    //
    // Ask for confirmation
    //
    if (!NoYesMessageBox(IDS_CONFIRM_DISCONNECT_USER))
    {
        //
        // Changed his mind
        //
        return;
    }

    CError err;
    m_list_Users.SetRedraw(FALSE);
    CWaitCursor wait;
    
    CFtpUserInfo * pUserEntry;
    int nSel = 0;
    BOOL fProblems = FALSE;

    while((pUserEntry = GetNextSelectedItem(&nSel)) != NULL)
    {
        err = DisconnectUser(pUserEntry);
        if (err.Failed())
        {
            ++fProblems;

            if (err.MessageBoxFormat(
                IDS_DISCONNECT_ERR,
                MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2,
                NO_HELP_CONTEXT,
                (LPCTSTR)pUserEntry->QueryUserName()
                ) == IDYES)
            {
                //
                // Continue trying to delete
                //
                ++nSel;
                continue;
            }    
            else
            {
                break;
            }
        }
    
        m_oblFtpUsers.RemoveIndex(nSel);
        m_list_Users.DeleteString(nSel);

        //
        // Don't advance counter to account for offset
        //
    }

    m_list_Users.SetRedraw(TRUE);
    UpdateTotalCount();
    SetControlStates();

    if (!fProblems)
    {
        //
        // Ensure button not disabled
        //
        GetDlgItem(IDC_BUTTON_REFRESH)->SetFocus();
    }
}



void
CUserSessionsDlg::OnButtonDisconnectAll() 
/*++

Routine Description:

    'Disconnect All Users' button has been pressed.  Disconnect all users

Arguments:

    None

Return Value:

    None

--*/
{
    //
    // Ask for confirmation
    //
    if (!NoYesMessageBox(IDS_CONFIRM_DISCONNECT_ALL))
    {
        //
        // Changed his mind
        //
        return;
    }
    
    CObListIter obli(m_oblFtpUsers);
    CFtpUserInfo * pUserEntry;

    m_list_Users.SetRedraw(FALSE);
    CWaitCursor wait;
    int cItems = 0;

    CError err;
    int nSel = 0;
    BOOL fProblems = FALSE;

    for ( /**/; pUserEntry = (CFtpUserInfo *)obli.Next(); ++cItems)
    {
        err = DisconnectUser(pUserEntry);
        if (err.Failed())
        {
            ++fProblems;

            if (err.MessageBoxFormat(
                IDS_DISCONNECT_ERR,
                MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2,
                NO_HELP_CONTEXT,
                (LPCTSTR)pUserEntry->QueryUserName()
                ) == IDYES)
            {
                //
                // Continue trying to delete
                //
                ++nSel;
                continue;
            }    
            else
            {
                break;
            }
        }

        m_oblFtpUsers.RemoveIndex(nSel);
        m_list_Users.DeleteString(nSel);
    }

    m_list_Users.SetRedraw(TRUE);
    UpdateTotalCount();
    SetControlStates();

    if (!fProblems)
    {
        //
        // Ensure button not disabled
        //
        GetDlgItem(IDC_BUTTON_REFRESH)->SetFocus();
    }
}



void
CUserSessionsDlg::OnButtonRefresh() 
/*++

Routine Description:

    'Refresh' Button has been pressed.  Refresh the user list

Arguments:

    None

Return Value:

    None

--*/
{
    RefreshUsersList();
}



void 
CUserSessionsDlg::OnSelchangeListUsers() 
/*++

Routine Description:

    Respond to a change in selection in the user listbox

Arguments:

    None

Return Value:

    None

--*/
{
    SetControlStates();
}



void
CUserSessionsDlg::OnHeaderItemClick(
    IN  UINT nID,
    IN  NMHDR * pNMHDR,
    OUT LRESULT * plResult
    )
/*++

Routine Description:

    Header item has been clicked in the listbox.  Change the sort key
    as appropriate.

Arguments:

    None

Return Value:

    None

--*/
{
    HD_NOTIFY * pNotify = (HD_NOTIFY *)pNMHDR;
    TRACEEOLID("Header Button clicked.");

    //
    // Can't press a button out of range, surely...
    //
    ASSERT(pNotify->iItem < m_list_Users.QueryNumColumns());
    int nOldSortColumn = m_nSortColumn;
    m_nSortColumn = pNotify->iItem;

    if(m_nSortColumn != nOldSortColumn)
    {
        //
        // Rebuild the list
        //
        SortUsersList();
        CFtpUserInfo * pSelector = GetSelectedListItem();
        FillListBox(pSelector);
        SetControlStates();
    }

    //
    // Message Fully Handled
    //
    *plResult = 0;
}



BOOL 
CUserSessionsDlg::OnInitDialog() 
/*++

Routine Description:

    WM_INITDIALOG handler.  Initialize the dialog.

Arguments:

    None.

Return Value:

    TRUE if no focus is to be set automatically, FALSE if the focus
    is already set.

--*/
{
    CDialog::OnInitDialog();

    if (!m_strAdminName.IsEmpty())
    {
        CError err = ImpersonateUser(m_strAdminName, _T(""), m_strAdminPassword, &m_hImpToken, &m_hLogToken);
    }

    m_list_Users.Initialize();

    if (RefreshUsersList() != ERROR_SUCCESS)
    {
        EndDialog(IDCANCEL);
        return FALSE;
    }

    return TRUE;
}

void
CUserSessionsDlg::OnDestroy() 
{
    if (m_hImpToken != INVALID_HANDLE_VALUE || m_hLogToken != INVALID_HANDLE_VALUE)
    {
        UnImpersonateUser(m_hImpToken, m_hLogToken);
    }
}

HRESULT ImpersonateUser(LPCTSTR  pszUserName,
                        LPCTSTR  pszDomain,
                        LPCTSTR  pszPassword,
                        HANDLE  *pCurImpToken,
                        HANDLE  *pLoggedOnUserToken)
{
    HRESULT hr = S_OK;

    ASSERT(pCurImpToken);
    ASSERT(pLoggedOnUserToken);

    *pCurImpToken       = INVALID_HANDLE_VALUE;
    *pLoggedOnUserToken = INVALID_HANDLE_VALUE;

    // logon the user.  This creates an primary impersonation
    // token.  If this fails, an error will be returned.

    if (!LogonUser((LPTSTR)pszUserName,
                   (LPTSTR)pszDomain,
                   (LPTSTR)pszPassword,
                   LOGON32_LOGON_BATCH,
                   LOGON32_PROVIDER_DEFAULT,
                   pLoggedOnUserToken)) {

        hr = HRESULT_FROM_WIN32(GetLastError());
    }
    // get the current impersonation token.  If an error occurs,
    // that is OK.  This just means that no impersonation on the
    // thread is occurring, so there is no need to do the RevertToSelf.

    else if (OpenThreadToken( GetCurrentThread(),
                              TOKEN_READ | TOKEN_IMPERSONATE,
                              TRUE,           
                              pCurImpToken)) {

           RevertToSelf();
    }

    // if everything's been successful so far, than call 
    // ImpersonateLoggedOnUser with the token created above.

    if (SUCCEEDED(hr)) {

        if (!ImpersonateLoggedOnUser(*pLoggedOnUserToken)) {

            hr = HRESULT_FROM_WIN32(GetLastError());
        }
    }

    // if there were failures, clean up any tokens that we created
    // or hold.

    if (FAILED(hr)) {

        // cleanup the LogonUser token

        if (*pLoggedOnUserToken != INVALID_HANDLE_VALUE) {
            CloseHandle(*pLoggedOnUserToken);
            *pLoggedOnUserToken = INVALID_HANDLE_VALUE;
        }

        // cleanup the token from the OpenThreadToken call

        if (*pCurImpToken != INVALID_HANDLE_VALUE) {
            HANDLE  hThread = GetCurrentThread();
            SetThreadToken(&hThread,
                           *pCurImpToken);
            CloseHandle(*pCurImpToken);
            *pCurImpToken = INVALID_HANDLE_VALUE;
        }
    }

    return hr;
}

HRESULT UnImpersonateUser(HANDLE hSavedImpToken, HANDLE hLoggedOnUser)
{
    // if there is an hSavedImpToken, then call SetThreadToken to
    // restore it.

    if (hSavedImpToken != INVALID_HANDLE_VALUE) 
    {
        HANDLE  hThread = GetCurrentThread();
        SetThreadToken(&hThread, hSavedImpToken);
        CloseHandle(hSavedImpToken);
    }

    // cleanup the LogonUser token

    if (hLoggedOnUser != INVALID_HANDLE_VALUE) 
    {
        CloseHandle(hLoggedOnUser);
    }

    return S_OK;
}