/*
* Process Hacker Extra Plugins -
*   Plugin Manager
*
* Copyright (C) 2016 dmex
*
* This file is part of Process Hacker.
*
* Process Hacker 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 3 of the License, or
* (at your option) any later version.
*
* Process Hacker 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 Process Hacker.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "main.h"
#include "miniz\miniz.h"

ULONGLONG ParseVersionString(
    _In_ PPH_STRING Version
    )
{
    PH_STRINGREF remainingPart, majorPart, minorPart, revisionPart, reservedPart;
    ULONG64 majorInteger = 0, minorInteger = 0, revisionInteger = 0, reservedInteger = 0;

    PhInitializeStringRef(&remainingPart, PhGetString(Version));
    PhSplitStringRefAtChar(&remainingPart, '.', &majorPart, &remainingPart);
    PhSplitStringRefAtChar(&remainingPart, '.', &minorPart, &remainingPart);
    PhSplitStringRefAtChar(&remainingPart, '.', &revisionPart, &remainingPart);
    PhSplitStringRefAtChar(&remainingPart, '.', &reservedPart, &remainingPart);

    PhStringToInteger64(&majorPart, 10, &majorInteger);
    PhStringToInteger64(&minorPart, 10, &minorInteger);
    PhStringToInteger64(&revisionPart, 10, &revisionInteger);
    PhStringToInteger64(&reservedPart, 10, &reservedInteger);

    return MAKE_VERSION_ULONGLONG(majorInteger, minorInteger, reservedInteger, revisionInteger);
}

json_object_ptr json_get_object(
    _In_ json_object_ptr Object, 
    _In_ PCSTR Key
    )
{
    json_object_ptr returnObj;

    if (json_object_object_get_ex(Object, Key, &returnObj))
    {
        return returnObj;
    }

    return NULL;
}

static BOOLEAN ReadRequestString(
    _In_ HINTERNET Handle,
    _Out_ _Deref_post_z_cap_(*DataLength) PSTR *Data,
    _Out_ ULONG *DataLength
    )
{
    BYTE buffer[PAGE_SIZE];
    PSTR data;
    ULONG allocatedLength;
    ULONG dataLength;
    ULONG returnLength;

    allocatedLength = sizeof(buffer);
    data = (PSTR)PhAllocate(allocatedLength);
    dataLength = 0;

    // Zero the buffer
    memset(buffer, 0, PAGE_SIZE);
    memset(data, 0, allocatedLength);

    while (WinHttpReadData(Handle, buffer, PAGE_SIZE, &returnLength))
    {
        if (returnLength == 0)
            break;

        if (allocatedLength < dataLength + returnLength)
        {
            allocatedLength *= 2;
            data = (PSTR)PhReAllocate(data, allocatedLength);
        }

        // Copy the returned buffer into our pointer
        memcpy(data + dataLength, buffer, returnLength);
        // Zero the returned buffer for the next loop
        //memset(buffer, 0, returnLength);

        dataLength += returnLength;
    }

    if (allocatedLength < dataLength + 1)
    {
        allocatedLength++;
        data = (PSTR)PhReAllocate(data, allocatedLength);
    }

    // Ensure that the buffer is null-terminated.
    data[dataLength] = 0;

    *DataLength = dataLength;
    *Data = data;

    return TRUE;
}

VOID EnumerateLoadedPlugins(
    _In_ PWCT_CONTEXT Context
    )
{
    PPH_AVL_LINKS root;
    PPH_AVL_LINKS links;

    root = PluginInstance->Links.Parent;

    while (root)
    {
        if (!root->Parent)
            break;

        root = root->Parent;
    }

    for (
        links = PhMinimumElementAvlTree((PPH_AVL_TREE)root); 
        links; 
        links = PhSuccessorElementAvlTree(links)
        )
    {
        HANDLE handle;
        IO_STATUS_BLOCK isb;
        FILE_BASIC_INFORMATION basic;
        PPHAPP_PLUGIN pluginInstance;
        PH_STRINGREF pluginBaseName;

        pluginInstance = CONTAINING_RECORD(links, PHAPP_PLUGIN, Links);

        PhInitializeStringRefLongHint(&pluginBaseName, PhGetPluginBaseName(pluginInstance));

        if (PhIsPluginDisabled(&pluginBaseName))
        {
            continue;
        }

        memset(&basic, 0, sizeof(FILE_BASIC_INFORMATION));

        if (NT_SUCCESS(PhCreateFileWin32(
            &handle,
            PhGetString(pluginInstance->FileName),
            FILE_GENERIC_READ,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN,
            FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT
            )))
        {
            NtQueryInformationFile(
                handle,
                &isb,
                &basic,
                sizeof(FILE_BASIC_INFORMATION),
                FileBasicInformation
                );

            NtClose(handle);
        }

        ULONG versionSize;
        PVOID versionInfo;
        PUSHORT languageInfo;
        UINT language;
        UINT bufferSize = 0;
        PWSTR buffer = NULL;
        PPH_STRING internalName = NULL;
        PPH_STRING version = NULL;

        versionSize = GetFileVersionInfoSize(PhGetString(pluginInstance->FileName), NULL);
        versionInfo = PhAllocate(versionSize);
        memset(versionInfo, 0, versionSize);

        if (GetFileVersionInfo(PhGetString(pluginInstance->FileName), 0, versionSize, versionInfo))
        {
            if (VerQueryValue(versionInfo, L"\\", &buffer, &bufferSize))
            {
                VS_FIXEDFILEINFO* info = (VS_FIXEDFILEINFO*)buffer;

                if (info->dwSignature == 0xfeef04bd)
                {
                    version = PhFormatString(
                        L"%lu.%lu.%lu.%lu",
                        (info->dwFileVersionMS >> 16) & 0xffff,
                        (info->dwFileVersionMS >> 0) & 0xffff,
                        (info->dwFileVersionLS >> 16) & 0xffff,
                        (info->dwFileVersionLS >> 0) & 0xffff
                        );
                }
            }

            if (VerQueryValue(versionInfo, L"\\VarFileInfo\\Translation", &languageInfo, &language))
            {
                PPH_STRING internalNameString = PhFormatString(
                    L"\\StringFileInfo\\%04x%04x\\InternalName", 
                    languageInfo[0],
                    languageInfo[1]
                    );

                if (VerQueryValue(versionInfo, PhGetStringOrEmpty(internalNameString), &buffer, &bufferSize))
                {
                    internalName = PhCreateStringEx(buffer, bufferSize * sizeof(WCHAR));
                }

                PhDereferenceObject(internalNameString);
            }
        }

        PPLUGIN_NODE entry = PhCreateAlloc(sizeof(PLUGIN_NODE));
        memset(entry, 0, sizeof(PLUGIN_NODE));

        entry->State = PLUGIN_STATE_LOCAL;
        entry->PluginInstance = pluginInstance;
        entry->InternalName = internalName;
        entry->Version = PhSubstring(version, 0, version->Length / sizeof(WCHAR) - 4);
        entry->Name = PhCreateString(pluginInstance->Information.DisplayName);
        entry->Author = PhCreateString(pluginInstance->Information.Author);
        entry->Description = PhCreateString(pluginInstance->Information.Description);
        entry->FilePath = PhCreateString2(&pluginInstance->FileName->sr);
        entry->FileName = PhGetBaseName(entry->FilePath);
    
        SYSTEMTIME utcTime, localTime;
        PhLargeIntegerToSystemTime(&utcTime, &basic.LastWriteTime);
        SystemTimeToTzSpecificLocalTime(NULL, &utcTime, &localTime);
        entry->UpdatedTime = PhFormatDateTime(&localTime);

        PhLargeIntegerToSystemTime(&utcTime, &basic.CreationTime);
        SystemTimeToTzSpecificLocalTime(NULL, &utcTime, &localTime);
        entry->AddedTime = PhFormatDateTime(&localTime);

        entry->PluginOptions = pluginInstance->Information.HasOptions;

        PostMessage(Context->DialogHandle, ID_UPDATE_ADD, 0, (LPARAM)entry);
    }
}

NTSTATUS QueryPluginsCallbackThread(
    _In_ PVOID Parameter
    )
{
    HINTERNET httpSessionHandle = NULL;
    HINTERNET httpConnectionHandle = NULL;
    HINTERNET httpRequestHandle = NULL;
    WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig = { 0 };
    ULONG xmlStringBufferLength = 0;
    PSTR xmlStringBuffer = NULL;
    json_object_ptr rootJsonObject;
    PWCT_CONTEXT context = Parameter;

    WinHttpGetIEProxyConfigForCurrentUser(&proxyConfig);

    if (!(httpSessionHandle = WinHttpOpen(
        L"ExtraPlugins_1.0",
        proxyConfig.lpszProxy ? WINHTTP_ACCESS_TYPE_NAMED_PROXY : WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
        proxyConfig.lpszProxy,
        proxyConfig.lpszProxyBypass,
        0
        )))
    {
        goto CleanupExit;
    }

    if (!(httpConnectionHandle = WinHttpConnect(
        httpSessionHandle,
        L"wj32.org",
        INTERNET_DEFAULT_HTTP_PORT,
        0
        )))
    {
        goto CleanupExit;
    }

    if (!(httpRequestHandle = WinHttpOpenRequest(
        httpConnectionHandle,
        NULL,
        L"/processhacker/plugins/list.php",
        NULL,
        WINHTTP_NO_REFERER,
        WINHTTP_DEFAULT_ACCEPT_TYPES,
        WINHTTP_FLAG_REFRESH
        )))
    {
        goto CleanupExit;
    }

    if (!WinHttpSendRequest(
        httpRequestHandle,
        WINHTTP_NO_ADDITIONAL_HEADERS,
        0,
        WINHTTP_NO_REQUEST_DATA,
        0,
        WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH,
        0
        ))
    {
        goto CleanupExit;
    }

    if (!WinHttpReceiveResponse(httpRequestHandle, NULL))
        goto CleanupExit;

    if (!ReadRequestString(httpRequestHandle, &xmlStringBuffer, &xmlStringBufferLength))
        goto CleanupExit;

    if (!(rootJsonObject = json_tokener_parse(xmlStringBuffer)))
        goto CleanupExit;

    for (INT i = 0; i < json_object_array_length(rootJsonObject); i++)
    {
        json_object_ptr jvalue;
        PPLUGIN_NODE entry;
        SYSTEMTIME time = { 0 };
        SYSTEMTIME localTime = { 0 };
        PH_STRINGREF pluginBaseName;
        PPH_STRING pluginDllPath;

        entry = PhCreateAlloc(sizeof(PLUGIN_NODE));
        memset(entry, 0, sizeof(PLUGIN_NODE));

        jvalue = json_object_array_get_idx(rootJsonObject, i);
        entry->Id = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_id")));
        entry->Visible = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_visible")));
        entry->InternalName = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_internal_name")));
        entry->Name = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_name")));
        entry->Version = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_version")));
        entry->Author = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_author")));
        entry->Description = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_description")));
        entry->IconUrl = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_icon")));
        entry->Requirements = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_requirements")));
        entry->FeedbackUrl = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_feedback")));
        entry->Screenshots = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_screenshots")));
        entry->AddedTime = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_datetime_added")));
        entry->UpdatedTime = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_datetime_updated")));
        entry->Download_count = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_download_count")));
        entry->Download_link_32 = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_download_link_32")));
        entry->Download_link_64 = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_download_link_64")));
        entry->SHA2_32 = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_hash_32")));
        entry->SHA2_64 = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_hash_64")));
        entry->HASH_32 = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_signed_32")));
        entry->HASH_64 = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_signed_64")));
        entry->FileName = PhConvertUtf8ToUtf16(json_object_get_string(json_get_object(jvalue, "plugin_filename")));

        swscanf(
            PhGetString(entry->UpdatedTime),
            L"%hu-%hu-%hu %hu:%hu:%hu",
            &time.wYear,
            &time.wMonth,
            &time.wDay,
            &time.wHour,
            &time.wMinute,
            &time.wSecond
            );

        if (SystemTimeToTzSpecificLocalTime(NULL, &time, &localTime))
        {
            entry->UpdatedTime = PhFormatDateTime(&localTime);
        }

        pluginDllPath = PhConcatStrings(3, PhGetString(PhGetApplicationDirectory()), L"Plugins\\", PhGetString(entry->FileName));
        PhInitializeStringRefLongHint(&pluginBaseName, PhGetString(entry->FileName));
   
        if (PhIsPluginDisabled(&pluginBaseName))
            goto CleanupExit;

        if (RtlDoesFileExists_U(PhGetString(pluginDllPath)))
        {
            ULONG versionSize;
            PVOID versionInfo;
            PUSHORT languageInfo;
            UINT language;
            UINT bufferSize = 0;
            PWSTR buffer = NULL;
            PPH_STRING internalName = NULL;
            PPH_STRING version = NULL;

            entry->FilePath = PhCreateString2(&pluginDllPath->sr);//entry->PluginInstance->FileName->sr);

            versionSize = GetFileVersionInfoSize(PhGetString(entry->FilePath), NULL);
            versionInfo = PhAllocate(versionSize);
            memset(versionInfo, 0, versionSize);

            if (GetFileVersionInfo(PhGetString(entry->FilePath), 0, versionSize, versionInfo))
            {
                if (VerQueryValue(versionInfo, L"\\", &buffer, &bufferSize))
                {
                    VS_FIXEDFILEINFO* info = (VS_FIXEDFILEINFO*)buffer;

                    if (info->dwSignature == 0xfeef04bd)
                    {
                        version = PhFormatString(
                            L"%lu.%lu.%lu.%lu",
                            (info->dwFileVersionMS >> 16) & 0xffff,
                            (info->dwFileVersionMS >> 0) & 0xffff,
                            (info->dwFileVersionLS >> 16) & 0xffff,
                            (info->dwFileVersionLS >> 0) & 0xffff
                        );
                    }
                }

                if (VerQueryValue(versionInfo, L"\\VarFileInfo\\Translation", &languageInfo, &language))
                {
                    PPH_STRING internalNameString = PhFormatString(
                        L"\\StringFileInfo\\%04x%04x\\InternalName",
                        languageInfo[0],
                        languageInfo[1]
                    );

                    if (VerQueryValue(versionInfo, PhGetStringOrEmpty(internalNameString), &buffer, &bufferSize))
                    {
                        internalName = PhCreateStringEx(buffer, bufferSize * sizeof(WCHAR));
                    }

                    PhDereferenceObject(internalNameString);
                }
            }

            PhFree(versionInfo);

            if (entry->PluginInstance = (PPHAPP_PLUGIN)PhFindPlugin(PhGetString(entry->InternalName)))
            {
                ULONGLONG currentVersion = ParseVersionString(version);
                ULONGLONG latestVersion = ParseVersionString(entry->Version);

                entry->PluginOptions = entry->PluginInstance->Information.HasOptions;

                if (currentVersion < latestVersion)
                {
                    // User is running an older version
                    entry->State = PLUGIN_STATE_UPDATE;
                    PostMessage(context->DialogHandle, ID_UPDATE_ADD, 0, (LPARAM)entry);
                }
            }
            else
            {
                // Plugin waiting to load?
                entry->State = PLUGIN_STATE_RESTART;
                PostMessage(context->DialogHandle, ID_UPDATE_ADD, 0, (LPARAM)entry);
            }
        }
        else
        {
            // New plugin available for download
            entry->State = PLUGIN_STATE_REMOTE;
            PostMessage(context->DialogHandle, ID_UPDATE_ADD, 0, (LPARAM)entry);
        }
    }

CleanupExit:

    if (httpRequestHandle)
        WinHttpCloseHandle(httpRequestHandle);

    if (httpConnectionHandle)
        WinHttpCloseHandle(httpConnectionHandle);

    if (httpSessionHandle)
        WinHttpCloseHandle(httpSessionHandle);

    if (xmlStringBuffer)
        PhFree(xmlStringBuffer);

    PostMessage(context->DialogHandle, ID_UPDATE_COUNT, 0, 0);

    return STATUS_SUCCESS;
}

NTSTATUS SetupExtractBuild(
    _In_ PVOID Parameter
    )
{
    static PH_STRINGREF pluginsDirectory = PH_STRINGREF_INIT(L"plugins\\");

    mz_uint64 totalLength = 0;
    mz_uint64 total_size = 0;
    mz_uint64 downloadedBytes = 0;
    PPH_STRING totalSizeStr = NULL;
    mz_bool status = MZ_FALSE;
    mz_zip_archive zip_archive;
    PPH_UPDATER_CONTEXT context = (PPH_UPDATER_CONTEXT)Parameter;

    memset(&zip_archive, 0, sizeof(zip_archive));

    // TODO: Move existing folder items.

    if (!(status = mz_zip_reader_init_file(&zip_archive, PhGetStringOrEmpty(context->SetupFilePath), 0)))
    {
        goto error;
    }

    for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip_archive); i++)
    {
        mz_zip_archive_file_stat file_stat;

        if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat))
        {
            //mz_zip_reader_end(&zip_archive);
            break;
        }

        total_size += file_stat.m_uncomp_size;
     }

    totalSizeStr = PhFormatSize(total_size, -1);

    for (ULONG i = 0; i < mz_zip_reader_get_num_files(&zip_archive); i++)
    {
        mz_zip_archive_file_stat stat;

        if (!mz_zip_reader_file_stat(&zip_archive, i, &stat))
        {
            continue;
        }

        if (mz_zip_reader_is_file_a_directory(&zip_archive, i))
        {
            PPH_STRING directory;
            PPH_STRING fileName;
            PPH_STRING fullSetupPath;
            PPH_STRING extractPath;
            ULONG indexOfFileName = -1;

            fileName = PhConvertUtf8ToUtf16(stat.m_filename);
            directory = PhGetApplicationDirectory();
            extractPath = PhConcatStringRef3(&directory->sr, &pluginsDirectory, &fileName->sr);
            fullSetupPath = PhGetFullPath(PhGetStringOrEmpty(extractPath), &indexOfFileName);

            SHCreateDirectoryEx(NULL, PhGetStringOrEmpty(fullSetupPath), NULL);
        }
        else
        {
            PPH_STRING directory;
            PPH_STRING fileName;
            PPH_STRING fullSetupPath;
            PPH_STRING extractPath;
            PPH_STRING directoryPath;
            //PPH_STRING baseNameString;
            PPH_STRING fileNameString;
            ULONG indexOfFileName = -1;

            fileName = PhConvertUtf8ToUtf16(stat.m_filename);
            directory = PhGetApplicationDirectory();
            extractPath = PhConcatStringRef3(&directory->sr, &pluginsDirectory, &fileName->sr);
            fullSetupPath = PhGetFullPath(PhGetStringOrEmpty(extractPath), &indexOfFileName);
            //baseNameString = PhGetBaseName(fullSetupPath);
            //fullSetupPath = PhGetFullPath(PhGetStringOrEmpty(fileName), &indexOfFileName);
            fileNameString = PhConcatStrings(2, fullSetupPath->Buffer, L".bak");

            if (indexOfFileName != -1)
            {
                if (directoryPath = PhSubstring(fullSetupPath, 0, indexOfFileName))
                {
                    SHCreateDirectoryEx(NULL, PhGetStringOrEmpty(directoryPath), NULL);
                    PhDereferenceObject(directoryPath);
                }
            }

            if (RtlDoesFileExists_U(PhGetStringOrEmpty(fullSetupPath)))
            {
                MoveFileEx(PhGetString(fullSetupPath), PhGetString(fileNameString), MOVEFILE_REPLACE_EXISTING);
            }

            if (!mz_zip_reader_extract_to_file(&zip_archive, i, PhGetString(fullSetupPath), 0))
            {
                goto error;
            }

            PhDereferenceObject(fullSetupPath);
            PhDereferenceObject(extractPath);
        }
    }

    mz_zip_reader_end(&zip_archive);
    return STATUS_SUCCESS;

error:
    mz_zip_reader_end(&zip_archive);
    return STATUS_FAIL_CHECK;
}