// CommitMonitor - simple checker for new commits in svn repositories
// Copyright (C) 2007-2012 - Stefan Kueng
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#include "StdAfx.h"
#include "UrlInfo.h"
#include "AppUtils.h"
#include "MappedInFile.h"
#include "SimpleIni.h"
#include "Blowfish.h"
#include <Wincrypt.h>
#pragma comment(lib, "Crypt32.lib")
CUrlInfo::CUrlInfo(void) : lastchecked(0)
    , lastcheckedrev(0)
    , minutesinterval(90)
    , minminutesinterval(0)
    , fetchdiffs(false)
    , disallowdiffs(false)
    , parentpath(false)
    , monitored(true)
    , errNr(0)
    , maxentries(1000)
    , sccs(SCCS_SVN)
{
}
CUrlInfo::~CUrlInfo(void)
{
}
bool CUrlInfo::Save(FILE * hFile)
{
    if (!CSerializeUtils::SaveNumber(hFile, URLINFO_VERSION))
        return false;
    if (!CSerializeUtils::SaveString(hFile, username))
        return false;
    // encrypt the password
    DATA_BLOB blob, outblob;
    string encpwd = CUnicodeUtils::StdGetUTF8(password);
    encpwd = "encrypted_" + encpwd;
    blob.cbData = encpwd.size();
    blob.pbData = (BYTE*)encpwd.c_str();
    if (CryptProtectData(&blob, _T("CommitMonitorLogin"), NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &outblob))
    {
        if (!CSerializeUtils::SaveBuffer(hFile, outblob.pbData, outblob.cbData))
        {
            LocalFree(outblob.pbData);
            return false;
        }
        LocalFree(outblob.pbData);
    }
    if (!CSerializeUtils::SaveString(hFile, name))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, lastchecked))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, lastcheckedrev))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, lastcheckedrobots))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, minutesinterval))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, minminutesinterval))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, fetchdiffs))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, disallowdiffs))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, monitored))
        return false;
    if (!CSerializeUtils::SaveString(hFile, ignoreUsers))
        return false;
    if (!CSerializeUtils::SaveString(hFile, includeUsers))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, parentpath))
        return false;
    if (!CSerializeUtils::SaveString(hFile, error))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, errNr))
        return false;
    if (!CSerializeUtils::SaveString(hFile, callcommand))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, noexecuteignored))
        return false;
    if (!CSerializeUtils::SaveString(hFile, webviewer))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, maxentries))
        return false;
    // RA Sewell: Version 14
    if (!CSerializeUtils::SaveNumber(hFile, sccs))
        return false;
    if (!CSerializeUtils::SaveString(hFile, accurevRepo))
        return false;
    // prevent caching more than URLINFO_MAXENTRIES revisions - this is a commit monitor, not a full featured
    // log dialog!
    while (logentries.size() > (size_t)min(URLINFO_MAXENTRIES, maxentries))
        logentries.erase(logentries.begin());
    if (!CSerializeUtils::SaveNumber(hFile, CSerializeUtils::SerializeType_Map))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, logentries.size()))
        return false;
    for (map<svn_revnum_t,SCCSLogEntry>::iterator it = logentries.begin(); it != logentries.end(); ++it)
    {
        if (!CSerializeUtils::SaveNumber(hFile, it->first))
            return false;
        if (!it->second.Save(hFile))
            return false;
    }
    return true;
}
bool CUrlInfo::Load(const unsigned char *& buf)
{
    unsigned __int64 version = 0;
    if (!CSerializeUtils::LoadNumber(buf, version))
        return false;
    unsigned __int64 value = 0;
    if (!CSerializeUtils::LoadString(buf, username))
        return false;
    const unsigned char * buf2 = buf;
    BYTE * pbData = NULL;
    size_t len = 0;
    if (!CSerializeUtils::LoadBuffer(buf, pbData, len))
    {
        buf = buf2;
        if (!CSerializeUtils::LoadString(buf, password))
            return false;
    }
    // decrypt the password
    DATA_BLOB blob, outblob;
    blob.cbData = len;
    blob.pbData = pbData;
    if (CryptUnprotectData(&blob, NULL, NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &outblob))
    {
        string encpwd = string((const char*)outblob.pbData, outblob.cbData);
        if (_strnicmp(encpwd.c_str(), "encrypted_", 10) == 0)
        {
            encpwd = encpwd.substr(10);
            password = CUnicodeUtils::StdGetUnicode(encpwd);
        }
        LocalFree(outblob.pbData);
    }
    delete [] pbData;
    if (!CSerializeUtils::LoadString(buf, name))
        return false;
    if (!CSerializeUtils::LoadNumber(buf, value))
        return false;
    lastchecked = value;
    if (!CSerializeUtils::LoadNumber(buf, value))
        return false;
    lastcheckedrev = (svn_revnum_t)value;
    if (version > 1)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        lastcheckedrobots = (int)value;
    }
    if (!CSerializeUtils::LoadNumber(buf, value))
        return false;
    minutesinterval = (int)value;
    if (version > 1)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        minminutesinterval = (int)value;
    }
    if ((version < 4)||(version >= 12))
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        fetchdiffs = !!value;
    }
    if (version > 1)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        disallowdiffs = !!value;
    }
    if (version >= 9)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        monitored = !!value;
    }
    if (version >= 3)
    {
        if (version >= 5)
        {
            if (!CSerializeUtils::LoadString(buf, ignoreUsers))
                return false;
        }
        else
        {
            if (!CSerializeUtils::LoadNumber(buf, value))
                return false;
            if (value)
                ignoreUsers = username;
        }
    }
    if (version >= 13)
    {
        if (!CSerializeUtils::LoadString(buf, includeUsers))
            return false;
    }
    if (!CSerializeUtils::LoadNumber(buf, value))
        return false;
    parentpath = !!value;
    if (!CSerializeUtils::LoadString(buf, error))
        return false;
    if (version >= 10)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        errNr = (apr_status_t)value;
    }
    if (version >= 6)
    {
        if (!CSerializeUtils::LoadString(buf, callcommand))
            return false;
    }
    if (version >= 8)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        noexecuteignored = !!value;
    }
    else
        noexecuteignored = false;
    if (version >= 7)
    {
        if (!CSerializeUtils::LoadString(buf, webviewer))
            return false;
    }
    if (version >= 11)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        maxentries = (int)value;
    }
    else
        maxentries = URLINFO_MAXENTRIES;
    if (version >= 14)
    {
        if (!CSerializeUtils::LoadNumber(buf, value))
            return false;
        sccs = (SCCS_TYPE)value;
        if (!CSerializeUtils::LoadString(buf, accurevRepo))
            return false;
    }
    else
    {
        sccs = SCCS_SVN;
    }
    logentries.clear();
    if (!CSerializeUtils::LoadNumber(buf, value))
        return false;
    if (CSerializeUtils::SerializeType_Map == value)
    {
        if (CSerializeUtils::LoadNumber(buf, value))
        {
            // we had a bug where the size could be bigger than URLINFO_MAXENTRIES, but
            // only the first URLINFO_MAXENTRIES entries were actually saved.
            // instead of bailing out if the value is bigger than URLINFO_MAXENTRIES, we
            // adjust it to the max saved values instead.
            // in case the value is out of range for other reasons,
            // the further serialization should bail out soon enough.
            if (value > URLINFO_MAXENTRIES)
                value = URLINFO_MAXENTRIES;
            for (unsigned __int64 i=0; i<value; ++i)
            {
                unsigned __int64 key;
                SCCSLogEntry logentry;
                if (!CSerializeUtils::LoadNumber(buf, key))
                    return false;
                if (!logentry.Load(buf))
                    return false;
                logentries[(svn_revnum_t)key] = logentry;
            }
            return true;
        }
    }
    return false;
}
CUrlInfos::CUrlInfos(void)
{
}
CUrlInfos::~CUrlInfos(void)
{
}
bool CUrlInfos::Load()
{
    wstring urlfile = CAppUtils::GetDataDir() + _T("\\urls");
    wstring urlfilebak = CAppUtils::GetDataDir() + _T("\\urls_backup");
    if (!PathFileExists(urlfile.c_str()))
        return false;
    if (Load(urlfile.c_str()))
    {
        // urls file successfully loaded: create a backup copy
        CopyFile(urlfile.c_str(), urlfilebak.c_str(), FALSE);
        return true;
    }
    else
    {
        // loading the file failed. Check whether there's a backup
        // file available to load instead
        return Load(urlfilebak.c_str());
    }
}
void CUrlInfos::Save()
{
    bool bExit = false;
    const map<wstring,CUrlInfo> * pInfos = GetReadOnlyData();
    if (pInfos->size() == 0)
    {
        // empty project list: don't save it!
        // See issue #267 for why: http://code.google.com/p/commitmonitor/issues/detail?id=267
        bExit = true;
    }
    ReleaseReadOnlyData();
    if (bExit)
        return;
    wstring urlfile = CAppUtils::GetDataDir() + _T("\\urls");
    wstring urlfilenew = CAppUtils::GetDataDir() + _T("\\urls_new");
    if (Save(urlfilenew.c_str()))
    {
        DeleteFile(urlfile.c_str());
        MoveFile(urlfilenew.c_str(), urlfile.c_str());
    }
}
bool CUrlInfos::Save(LPCWSTR filename)
{
#ifdef _DEBUG
    DWORD dwStartTicks = GetTickCount();
#endif
    FILE * hFile = NULL;
    _tfopen_s(&hFile, filename, _T("w+b"));
    if (hFile == NULL)
        return false;
    char filebuffer[4096];
    setvbuf(hFile, filebuffer, _IOFBF, 4096);
    guard.AcquireReaderLock();
    bool bSuccess = Save(hFile);
    guard.ReleaseReaderLock();
    fclose(hFile);
    if (bSuccess)
    {
        // rename the file to the original requested name
        TRACE(_T("data saved\n"));
#ifdef _DEBUG
        TCHAR timerbuf[MAX_PATH] = {0};
        _stprintf_s(timerbuf, MAX_PATH, _T("time needed for saving all url info: %ld ms\n"), GetTickCount()-dwStartTicks);
        TRACE(timerbuf);
#endif
        return true;
    }
    return false;
}
bool CUrlInfos::Load(LPCWSTR filename)
{
    bool bRet = false;
#ifdef _DEBUG
    DWORD dwStartTicks = GetTickCount();
#endif
    CMappedInFile file(filename);
    guard.AcquireWriterLock();
    const unsigned char * buf = file.GetBuffer();
    if (buf)
        bRet = Load(buf);
    guard.ReleaseWriterLock();
    TRACE(_T("data loaded\n"));
#ifdef _DEBUG
    TCHAR timerbuf[MAX_PATH] = {0};
    _stprintf_s(timerbuf, MAX_PATH, _T("time needed for loading all url info: %ld ms\n"), GetTickCount()-dwStartTicks);
    TRACE(timerbuf);
#endif
    return bRet;
}
bool CUrlInfos::Save(FILE * hFile)
{
    if (!CSerializeUtils::SaveNumber(hFile, URLINFOS_VERSION))
        return false;
    // first save the size of the map
    if (!CSerializeUtils::SaveNumber(hFile, CSerializeUtils::SerializeType_Map))
        return false;
    if (!CSerializeUtils::SaveNumber(hFile, infos.size()))
        return false;
    for (map<wstring,CUrlInfo>::iterator it = infos.begin(); it != infos.end(); ++it)
    {
        if (!CSerializeUtils::SaveString(hFile, it->first))
            return false;
        if (!it->second.Save(hFile))
            return false;
    }
    return true;
}
bool CUrlInfos::Load(const unsigned char *& buf)
{
    unsigned __int64 version = 0;
    if (!CSerializeUtils::LoadNumber(buf, version))
        return false;
    infos.clear();
    unsigned __int64 value = 0;
    if (!CSerializeUtils::LoadNumber(buf, value))
        return false;
    if (CSerializeUtils::SerializeType_Map == value)
    {
        if (CSerializeUtils::LoadNumber(buf, value))
        {
            for (unsigned __int64 i=0; i<value; ++i)
            {
                wstring key;
                CUrlInfo info;
                if (!CSerializeUtils::LoadString(buf, key))
                    return false;
                if (!info.Load(buf))
                    return false;
                info.url = key;
                infos[key] = info;
            }
            return true;
        }
    }
    return false;
}
bool CUrlInfos::IsEmpty()
{
    bool bIsEmpty = true;
    guard.AcquireReaderLock();
    bIsEmpty = (infos.size() == 0);
    guard.ReleaseReaderLock();
    return bIsEmpty;
}
string CUrlInfos::CalcMD5(LPCWSTR s)
{
    HCRYPTPROV hCryptProv;
    HCRYPTHASH hHash = 0;
    BYTE bHash[0x7f];
    DWORD dwHashLen= 16; // The MD5 algorithm always returns 16 bytes.
    DWORD cbContent= wcslen(s)*sizeof(WCHAR);
    BYTE* pbContent= (BYTE*)s;
    string retHash;
    if (CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET))
    {
        if (CryptCreateHash(hCryptProv,
                            CALG_MD5,   // algorithm identifier definitions see: wincrypt.h
                            0, 0, &hHash))
        {
            if (CryptHashData(hHash, pbContent, cbContent, 0))
            {
                if (CryptGetHashParam(hHash, HP_HASHVAL, bHash, &dwHashLen, 0))
                {
                    // Make a string version of the numeric digest value
                    char tmpBuf[3];
                    for (int i = 0; i<16; i++)
                    {
                        sprintf_s(tmpBuf, 3, "%02x", bHash[i]);
                        retHash += tmpBuf;
                    }
                }
            }
        }
    }
    CryptDestroyHash(hHash);
    CryptReleaseContext(hCryptProv, 0);
    return retHash;
}
bool CUrlInfos::Export(LPCWSTR filename, LPCWSTR password)
{
    FILE * hFile = NULL;
    _tfopen_s(&hFile, filename, _T("w+b"));
    if (hFile == NULL)
        return false;
    string pwHash = CalcMD5(password);
    guard.AcquireReaderLock();
    CSimpleIni iniFile;
    CBlowFish blower((const unsigned char*)pwHash.c_str(), 16);
    WCHAR numberBuf[1024];
    for (map<wstring,CUrlInfo>::iterator it = infos.begin(); it != infos.end(); ++it)
    {
        iniFile.SetValue(it->first.c_str(), L"username", it->second.username.c_str());
        iniFile.SetValue(it->first.c_str(), L"url", it->second.url.c_str());
        iniFile.SetValue(it->first.c_str(), L"name", it->second.name.c_str());
        iniFile.SetValue(it->first.c_str(), L"ignoreUsers", it->second.ignoreUsers.c_str());
        iniFile.SetValue(it->first.c_str(), L"callcommand", it->second.callcommand.c_str());
        iniFile.SetValue(it->first.c_str(), L"webviewer", it->second.webviewer.c_str());
        swprintf_s(numberBuf, 1024, L"%ld", it->second.minutesinterval);
        iniFile.SetValue(it->first.c_str(), L"minutesinterval", numberBuf);
        swprintf_s(numberBuf, 1024, L"%ld", it->second.minminutesinterval);
        iniFile.SetValue(it->first.c_str(), L"minminutesinterval", numberBuf);
        swprintf_s(numberBuf, 1024, L"%ld", (int)it->second.disallowdiffs);
        iniFile.SetValue(it->first.c_str(), L"disallowdiffs", numberBuf);
        swprintf_s(numberBuf, 1024, L"%ld", it->second.maxentries);
        iniFile.SetValue(it->first.c_str(), L"maxentries", numberBuf);
        swprintf_s(numberBuf, 1024, L"%ld", it->second.noexecuteignored);
        iniFile.SetValue(it->first.c_str(), L"noexecuteignored", numberBuf);
        swprintf_s(numberBuf, 1024, L"%ld", it->second.monitored);
        iniFile.SetValue(it->first.c_str(), L"monitored", numberBuf);
        if (it->second.password.size())
        {
            // encrypt the password
            int bufSize = ((it->second.password.size() / 8) + 1) * 8;
            WCHAR * pwBuf = new WCHAR[bufSize + 1];
            SecureZeroMemory(pwBuf, bufSize*sizeof(WCHAR));
            wcscpy_s(pwBuf, bufSize, it->second.password.c_str());
            BYTE * pByteBuf = (BYTE*)pwBuf;
            blower.Encrypt(pByteBuf, bufSize*sizeof(WCHAR));
            WCHAR tmpBuf[3];
            wstring encryptedPassword;
            for (unsigned int i = 0; i < bufSize*sizeof(WCHAR); ++i)
            {
                swprintf_s(tmpBuf, 3, L"%02x", pByteBuf[i]);
                encryptedPassword += tmpBuf;
            }
            iniFile.SetValue(it->first.c_str(), L"password", encryptedPassword.c_str());
        }
    }
    SI_Error err = iniFile.SaveFile(hFile);
    fclose(hFile);
    guard.ReleaseReaderLock();
    return (err == SI_OK);
}
bool CUrlInfos::Import(LPCWSTR filename, LPCWSTR password)
{
    CSimpleIni iniFile;
    if (iniFile.LoadFile(filename) != SI_OK)
        return false;
    string pwHash = CalcMD5(password);
    guard.AcquireWriterLock();
    CBlowFish blower((const unsigned char*)pwHash.c_str(), 16);
    CSimpleIni::TNamesDepend sections;
    iniFile.GetAllSections(sections);
    for (CSimpleIni::TNamesDepend::iterator it = sections.begin(); it != sections.end(); ++it)
    {
        CUrlInfo info;
        info.username = wstring(iniFile.GetValue(*it, _T("username"), _T("")));
        info.url = wstring(iniFile.GetValue(*it, _T("url"), _T("")));
        info.name = wstring(iniFile.GetValue(*it, _T("name"), _T("")));
        info.ignoreUsers = wstring(iniFile.GetValue(*it, _T("ignoreUsers"), _T("")));
        info.callcommand = wstring(iniFile.GetValue(*it, _T("callcommand"), _T("")));
        info.webviewer = wstring(iniFile.GetValue(*it, _T("webviewer"), _T("")));
        info.minutesinterval = _wtol(iniFile.GetValue(*it, L"minutesinterval", L""));
        info.minminutesinterval = _wtol(iniFile.GetValue(*it, L"minminutesinterval", L""));
        info.disallowdiffs = !!_wtol(iniFile.GetValue(*it, L"disallowdiffs", L""));
        info.maxentries = _wtol(iniFile.GetValue(*it, L"maxentries", L""));
        info.noexecuteignored = !!_wtol(iniFile.GetValue(*it, L"noexecuteignored", L""));
        info.monitored = !!_wtol(iniFile.GetValue(*it, L"monitored", L"1"));
        wstring unencryptedPassword = wstring(iniFile.GetValue(*it, _T("password"), _T("")));
        if (unencryptedPassword.size())
        {
            // decrypt the password
            BYTE * pPwBuf = new BYTE[unencryptedPassword.size()/2];
            SecureZeroMemory(pPwBuf, unencryptedPassword.size()/2);
            const WCHAR * pUnencryptedString = unencryptedPassword.c_str();
            for (unsigned int i = 0; i < unencryptedPassword.size()/2; ++i)
            {
                WCHAR tmpBuf[3];
                wcsncpy_s(tmpBuf, 3, &pUnencryptedString[i*2], 2);
                WCHAR * stopString;
                pPwBuf[i] = (BYTE)wcstol(tmpBuf, &stopString, 16);
            }
            blower.Decrypt(pPwBuf, unencryptedPassword.size()/2);
            WCHAR * pDecryptedPW = (WCHAR*)pPwBuf;
            wstring plainPw = pDecryptedPW;
            info.password = plainPw;
        }
        if ((infos.size())&&(infos.find(info.url) != infos.end()))
        {
            CUrlInfo existingUrlInfo = infos.find(info.url)->second;
            existingUrlInfo.username = info.username;
            existingUrlInfo.url = info.url;
            existingUrlInfo.name = info.name;
            existingUrlInfo.ignoreUsers = info.ignoreUsers;
            existingUrlInfo.callcommand = info.callcommand;
            existingUrlInfo.webviewer = info.webviewer;
            existingUrlInfo.minutesinterval = info.minutesinterval;
            existingUrlInfo.minminutesinterval = info.minminutesinterval;
            existingUrlInfo.disallowdiffs = info.disallowdiffs;
            existingUrlInfo.maxentries = info.maxentries;
            existingUrlInfo.noexecuteignored = info.noexecuteignored;
            existingUrlInfo.monitored = info.monitored;
            infos[existingUrlInfo.url] = existingUrlInfo;
        }
        else
            infos[info.url] = info;
    }
    guard.ReleaseWriterLock();
    return true;
}
const map<wstring,CUrlInfo> * CUrlInfos::GetReadOnlyData()
{
    guard.AcquireReaderLock();
    return &infos;
}
map<wstring,CUrlInfo> * CUrlInfos::GetWriteData()
{
    guard.AcquireWriterLock();
    return &infos;
}
void CUrlInfos::ReleaseReadOnlyData()
{
    guard.ReleaseReaderLock();
}
void CUrlInfos::ReleaseWriteData()
{
    guard.ReleaseWriterLock();
}