/*
 *  This file is part of nzbget. See <https://nzbget.com>.
 *
 *  Copyright (C) 2007-2019 Andrey Prygunkov <hugbug@users.sourceforge.net>
 *  Copyright (C) 2023-2024 Denis <denis@nzbget.com>
 *
 *  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, see <https://www.gnu.org/licenses/>.
 */


#include "nzbget.h"
#include "XmlRpc.h"
#include "Log.h"
#include "Options.h"
#include "WorkState.h"
#include "Scanner.h"
#include "FeedCoordinator.h"
#include "ServerPool.h"
#include "Util.h"
#include "FileSystem.h"
#include "Maintenance.h"
#include "StatMeter.h"
#include "ArticleWriter.h"
#include "DiskState.h"
#include "ScriptConfig.h"
#include "QueueScript.h"
#include "CommandScript.h"
#include "UrlCoordinator.h"
#include "ExtensionManager.h"
#include "SystemInfo.h"
#include "Benchmark.h"
#include "NetworkSpeedTest.h"
#include "Xml.h"

extern void ExitProc();
extern void Reload();

class SafeXmlCommand: public XmlCommand
{
public:
	virtual bool IsSafeMethod() { return true; };
};

class ErrorXmlCommand: public XmlCommand
{
public:
	ErrorXmlCommand(int errCode, const char* errText) :
		m_errCode(errCode), m_errText(errText) {}
	virtual void Execute();
	virtual bool IsError() { return true; };

private:
	int m_errCode;
	const char* m_errText;
};

class PauseUnpauseXmlCommand: public XmlCommand
{
public:
	enum EPauseAction
	{
		paDownload,
		paPostProcess,
		paScan
	};

	PauseUnpauseXmlCommand(bool pause, EPauseAction pauseAction) :
		m_pause(pause), m_pauseAction(pauseAction) {}
	virtual void Execute();

private:
	bool m_pause;
	EPauseAction m_pauseAction;
};

class ScheduleResumeXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class ShutdownXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class ReloadXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class VersionXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class DumpDebugXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class SetDownloadRateXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class StatusXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class SysInfoXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class LogXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();

protected:
	int m_idFrom;
	int m_nrEntries;
	virtual GuardedMessageList GuardMessages();
};

class NzbInfoXmlCommand: public SafeXmlCommand
{
protected:
	void AppendNzbInfoFields(NzbInfo* nzbInfo);
	void AppendPostInfoFields(PostInfo* postInfo, int logEntries, bool postQueue);
};

class ListFilesXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class ListGroupsXmlCommand: public NzbInfoXmlCommand
{
public:
	virtual void Execute();
private:
	const char* DetectStatus(NzbInfo* nzbInfo);
};

class EditQueueXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class DownloadXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class PostQueueXmlCommand: public NzbInfoXmlCommand
{
public:
	virtual void Execute();
};

class WriteLogXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class ClearLogXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class ScanXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class HistoryXmlCommand: public NzbInfoXmlCommand
{
public:
	virtual void Execute();
private:
	const char* DetectStatus(HistoryInfo* historyInfo);
};

class UrlQueueXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class ConfigXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class LoadConfigXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class LoadExtensionsXmlCommand : public SafeXmlCommand
{
public:
	virtual void Execute();
};

class DownloadExtensionXmlCommand : public SafeXmlCommand
{
public:
	virtual void Execute();
};

class UpdateExtensionXmlCommand : public SafeXmlCommand
{
public:
	virtual void Execute();
};

class DeleteExtensionXmlCommand : public SafeXmlCommand
{
public:
	virtual void Execute();
};

class TestExtensionXmlCommand : public SafeXmlCommand
{
public:
	virtual void Execute();
};

class SaveConfigXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class ConfigTemplatesXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class ViewFeedXmlCommand: public XmlCommand
{
public:
	ViewFeedXmlCommand(bool preview) : m_preview(preview) {}
	virtual void Execute();
private:
	bool m_preview;
};

class FetchFeedXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class EditServerXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class ReadUrlXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class CheckUpdatesXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class StartUpdateXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class LogUpdateXmlCommand: public LogXmlCommand
{
protected:
	virtual GuardedMessageList GuardMessages();
};

class ServerVolumesXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class ResetServerVolumeXmlCommand: public XmlCommand
{
public:
	virtual void Execute();
};

class LoadLogXmlCommand: public LogXmlCommand
{
protected:
	virtual void Execute();
	virtual GuardedMessageList GuardMessages();
private:
	int m_nzbId;
	NzbInfo* m_nzbInfo;
	MessageList m_messages;
	std::unique_ptr<GuardedDownloadQueue> m_downloadQueue;
};

class TestServerXmlCommand: public XmlCommand
{
public:
	virtual void Execute();

private:
	CString m_errText;

	class TestConnection : public NntpConnection
	{
	public:
		TestConnection(NewsServer* newsServer, TestServerXmlCommand* owner):
			NntpConnection(newsServer), m_owner(owner) {}
	protected:
		TestServerXmlCommand* m_owner;
		virtual void PrintError(const char* errMsg) { m_owner->PrintError(errMsg); }
	};

	void PrintError(const char* errMsg);
};

class TestServerSpeedXmlCommand: public SafeXmlCommand
{
public:
	virtual void Execute();
};

class TestDiskSpeedXmlCommand final : public SafeXmlCommand
{
public:
	void Execute() override;

	std::string ToJsonStr(uint64_t size, double time)
	{
		Json::JsonObject json;

		json["SizeMB"] = size / 1024ull / 1024ull;
		json["DurationMS"] = time;

		return Json::Serialize(json);
	}

	std::string ToXmlStr(uint64_t size, double time)
	{
		xmlNodePtr rootNode = xmlNewNode(nullptr, BAD_CAST "value");
		xmlNodePtr structNode = xmlNewNode(nullptr, BAD_CAST "struct");

		std::string sizeMB = std::to_string(size / 1024ull / 1024ull);
		std::string durationMS = std::to_string(time);

		Xml::AddNewNode(structNode, "SizeMB", "i4", sizeMB.c_str());
		Xml::AddNewNode(structNode, "DurationMS", "double", durationMS.c_str());

		xmlAddChild(rootNode, structNode);
		
		std::string result = Xml::Serialize(rootNode);

		xmlFreeNode(rootNode);

		return result;
	}
};

class TestNetworkSpeedXmlCommand final : public SafeXmlCommand
{
public:
	void Execute() override;

	std::string ToJsonStr(double speedMbps)
	{
		Json::JsonObject json;

		json["SpeedMbps"] = speedMbps;

		return Json::Serialize(json);
	}

	std::string ToXmlStr(double speedMbps)
	{
		xmlNodePtr rootNode = xmlNewNode(nullptr, BAD_CAST "value");
		xmlNodePtr structNode = xmlNewNode(nullptr, BAD_CAST "struct");

		std::string speedMbpsStr = std::to_string(speedMbps);

		Xml::AddNewNode(structNode, "SpeedMbps", "double", speedMbpsStr.c_str());

		xmlAddChild(rootNode, structNode);
		
		std::string result = Xml::Serialize(rootNode);

		xmlFreeNode(rootNode);

		return result;
	}
};

class StartScriptXmlCommand : public XmlCommand
{
public:
	virtual void Execute();
};

class LogScriptXmlCommand : public LogXmlCommand
{
protected:
	virtual GuardedMessageList GuardMessages();
};


//*****************************************************************
// XmlRpcProcessor

void XmlRpcProcessor::SetUrl(const char* url)
{
	m_url = url;
	WebUtil::UrlDecode(m_url);
}

bool XmlRpcProcessor::IsRpcRequest(const char* url)
{
	return !strcmp(url, "/xmlrpc") || !strncmp(url, "/xmlrpc/", 8) ||
		!strcmp(url, "/jsonrpc") || !strncmp(url, "/jsonrpc/", 9) ||
		!strcmp(url, "/jsonprpc") || !strncmp(url, "/jsonprpc/", 10);
}

void XmlRpcProcessor::Execute()
{
	m_protocol = rpUndefined;
	if (!strcmp(m_url, "/xmlrpc") || !strncmp(m_url, "/xmlrpc/", 8))
	{
		m_protocol = XmlRpcProcessor::rpXmlRpc;
	}
	else if (!strcmp(m_url, "/jsonrpc") || !strncmp(m_url, "/jsonrpc/", 9))
	{
		m_protocol = rpJsonRpc;
	}
	else if (!strcmp(m_url, "/jsonprpc") || !strncmp(m_url, "/jsonprpc/", 10))
	{
		m_protocol = rpJsonPRpc;
	}
	else
	{
		error("internal error: invalid rpc-request: %s", *m_url);
		return;
	}

	Dispatch();
}

void XmlRpcProcessor::Dispatch()
{
	char* request = m_request;

	BString<100> methodName;
	BString<100> requestId;

	if (m_httpMethod == hmGet)
	{
		request = (char*)m_url + 1;
		char* pstart = strchr(request, '/');
		if (pstart)
		{
			char* pend = strchr(pstart + 1, '?');
			if (pend)
			{
				int len = (int)(pend - pstart - 1 < (int)sizeof(methodName) - 1 ? pend - pstart - 1 : (int)sizeof(methodName) - 1);
				methodName.Set(pstart + 1, len);
				request = pend + 1;
			}
			else
			{
				methodName.Set(pstart + 1);
				request = request + strlen(request);
			}
		}
	}
	else if (m_protocol == rpXmlRpc)
	{
		WebUtil::XmlParseTagValue(m_request, "methodName", methodName, sizeof(methodName), nullptr);
	}
	else if (m_protocol == rpJsonRpc)
	{
		int valueLen = 0;
		if (const char* methodPtr = WebUtil::JsonFindField(m_request, "method", &valueLen))
		{
			valueLen = valueLen >= (int)sizeof(methodName) ? (int)sizeof(methodName) - 1 : valueLen;
			methodName.Set(methodPtr + 1, valueLen - 2);
		}
		if (const char* requestIdPtr = WebUtil::JsonFindField(m_request, "id", &valueLen))
		{
			valueLen = valueLen >= (int)sizeof(requestId) ? (int)sizeof(requestId) - 1 : valueLen;
			requestId.Set(requestIdPtr, valueLen);
		}
	}

	debug("MethodName=%s", *methodName);

	if (!strcasecmp(methodName, "system.multicall") && m_protocol == rpXmlRpc && m_httpMethod == hmPost)
	{
		MutliCall();
	}
	else
	{
		std::unique_ptr<XmlCommand> command = CreateCommand(methodName);
		command->SetRequest(request);
		command->SetProtocol(m_protocol);
		command->SetHttpMethod(m_httpMethod);
		command->SetUserAccess(m_userAccess);
		command->PrepareParams();
		m_safeMethod = command->IsSafeMethod();
		bool safeToExecute = m_safeMethod || m_httpMethod == XmlRpcProcessor::hmPost || m_protocol == XmlRpcProcessor::rpJsonPRpc;
		if (safeToExecute || command->IsError())
		{
			command->Execute();
			BuildResponse(command->GetResponse(), command->GetCallbackFunc(), command->GetFault(), requestId);
		}
		else
		{
			BuildErrorResponse(4, "Not safe procedure for HTTP-Method GET. Use Method POST instead");
		}
	}
}

void XmlRpcProcessor::MutliCall()
{
	bool error = false;
	StringBuilder response;

	response.Append("<array><data>");

	char* requestPtr = m_request;
	char* callEnd = strstr(requestPtr, "</struct>");
	while (callEnd)
	{
		*callEnd = '\0';
		debug("MutliCall, request=%s", requestPtr);
		char* nameEnd = strstr(requestPtr, "</name>");
		if (!nameEnd)
		{
			error = true;
			break;
		}

		char methodName[100];
		methodName[0] = '\0';
		WebUtil::XmlParseTagValue(nameEnd, "string", methodName, sizeof(methodName), nullptr);
		debug("MutliCall, MethodName=%s", methodName);

		std::unique_ptr<XmlCommand> command = CreateCommand(methodName);
		command->SetRequest(requestPtr);
		m_safeMethod |= command->IsSafeMethod();
		command->Execute();

		debug("MutliCall, Response=%s", command->GetResponse());

		bool fault = !strncmp(command->GetResponse(), "<fault>", 7);
		bool array = !fault && !strncmp(command->GetResponse(), "<array>", 7);
		if (!fault && !array)
		{
			response.Append("<array><data>");
		}
		response.Append("<value>");
		response.Append(command->GetResponse());
		response.Append("</value>");
		if (!fault && !array)
		{
			response.Append("</data></array>");
		}

		requestPtr = callEnd + 9; //strlen("</struct>")
		callEnd = strstr(requestPtr, "</struct>");
	}

	if (error)
	{
		BuildErrorResponse(4, "Parse error");
	}
	else
	{
		response.Append("</data></array>");
		BuildResponse(response, "", false, nullptr);
	}
}

void XmlRpcProcessor::BuildResponse(const char* response, const char* callbackFunc,
	bool fault, const char* requestId)
{
	const char XML_HEADER[] = "<?xml version=\"1.0\"?>\n<methodResponse>\n";
	const char XML_FOOTER[] = "</methodResponse>";
	const char XML_OK_OPEN[] = "<params><param><value>";
	const char XML_OK_CLOSE[] = "</value></param></params>\n";
	const char XML_FAULT_OPEN[] = "<fault><value>";
	const char XML_FAULT_CLOSE[] = "</value></fault>\n";

	const char JSON_HEADER[] = "{\n\"version\" : \"1.1\",\n";
	const char JSON_ID_OPEN[] = "\"id\" : ";
	const char JSON_ID_CLOSE[] = ",\n";
	const char JSON_FOOTER[] = "\n}";
	const char JSON_OK_OPEN[] = "\"result\" : ";
	const char JSON_OK_CLOSE[] = "";
	const char JSON_FAULT_OPEN[] = "\"error\" : ";
	const char JSON_FAULT_CLOSE[] = "";

	const char JSONP_CALLBACK_HEADER[] = "(";
	const char JSONP_CALLBACK_FOOTER[] = ")";

	bool xmlRpc = m_protocol == rpXmlRpc;

	const char* callbackHeader = m_protocol == rpJsonPRpc ? JSONP_CALLBACK_HEADER : "";
	const char* header = xmlRpc ? XML_HEADER : JSON_HEADER;
	const char* footer = xmlRpc ? XML_FOOTER : JSON_FOOTER;
	const char* openTag = fault ? (xmlRpc ? XML_FAULT_OPEN : JSON_FAULT_OPEN) : (xmlRpc ? XML_OK_OPEN : JSON_OK_OPEN);
	const char* closeTag = fault ? (xmlRpc ? XML_FAULT_CLOSE : JSON_FAULT_CLOSE ) : (xmlRpc ? XML_OK_CLOSE : JSON_OK_CLOSE);
	const char* callbackFooter = m_protocol == rpJsonPRpc ? JSONP_CALLBACK_FOOTER : "";

	debug("Response=%s", response);

	if (callbackFunc)
	{
		m_response.Append(callbackFunc);
	}
	m_response.Append(callbackHeader);
	m_response.Append(header);
	if (!xmlRpc && requestId && *requestId)
	{
		m_response.Append(JSON_ID_OPEN);
		m_response.Append(requestId);
		m_response.Append(JSON_ID_CLOSE);
	}
	m_response.Append(openTag);
	m_response.Append(response);
	m_response.Append(closeTag);
	m_response.Append(footer);
	m_response.Append(callbackFooter);

	m_contentType = xmlRpc ? "text/xml" : "application/json";
}

void XmlRpcProcessor::BuildErrorResponse(int errCode, const char* errText)
{
	ErrorXmlCommand command(errCode, errText);
	command.SetProtocol(m_protocol);
	command.Execute();
	BuildResponse(command.GetResponse(), "", command.GetFault(), nullptr);
}

std::unique_ptr<XmlCommand> XmlRpcProcessor::CreateCommand(const char* methodName)
{
	std::unique_ptr<XmlCommand> command;

	if (m_userAccess == uaAdd &&
		!(!strcasecmp(methodName, "append") || !strcasecmp(methodName, "appendurl") ||
		 !strcasecmp(methodName, "version")))
	{
		command = std::make_unique<ErrorXmlCommand>(401, "Access denied");
		warn("Received request \"%s\" from add-user, access denied", methodName);
	}
	else if (m_userAccess == uaRestricted && !strcasecmp(methodName, "saveconfig"))
	{
		command = std::make_unique<ErrorXmlCommand>(401, "Access denied");
		warn("Received request \"%s\" from restricted user, access denied", methodName);
	}
	else if (!strcasecmp(methodName, "pause") || !strcasecmp(methodName, "pausedownload") ||
		!strcasecmp(methodName, "pausedownload2"))
	{
		command = std::make_unique<PauseUnpauseXmlCommand>(true, PauseUnpauseXmlCommand::paDownload);
	}
	else if (!strcasecmp(methodName, "resume") || !strcasecmp(methodName, "resumedownload") ||
		!strcasecmp(methodName, "resumedownload2"))
	{
		command = std::make_unique<PauseUnpauseXmlCommand>(false, PauseUnpauseXmlCommand::paDownload);
	}
	else if (!strcasecmp(methodName, "shutdown"))
	{
		command = std::make_unique<ShutdownXmlCommand>();
	}
	else if (!strcasecmp(methodName, "reload"))
	{
		command = std::make_unique<ReloadXmlCommand>();
	}
	else if (!strcasecmp(methodName, "version"))
	{
		command = std::make_unique<VersionXmlCommand>();
	}
	else if (!strcasecmp(methodName, "dump"))
	{
		command = std::make_unique<DumpDebugXmlCommand>();
	}
	else if (!strcasecmp(methodName, "rate"))
	{
		command = std::make_unique<SetDownloadRateXmlCommand>();
	}
	else if (!strcasecmp(methodName, "status"))
	{
		command = std::make_unique<StatusXmlCommand>();
	}
	else if (!strcasecmp(methodName, "sysinfo"))
	{
		command = std::make_unique<SysInfoXmlCommand>();
	}
	else if (!strcasecmp(methodName, "log"))
	{
		command = std::make_unique<LogXmlCommand>();
	}
	else if (!strcasecmp(methodName, "listfiles"))
	{
		command = std::make_unique<ListFilesXmlCommand>();
	}
	else if (!strcasecmp(methodName, "listgroups"))
	{
		command = std::make_unique<ListGroupsXmlCommand>();
	}
	else if (!strcasecmp(methodName, "editqueue"))
	{
		command = std::make_unique<EditQueueXmlCommand>();
	}
	else if (!strcasecmp(methodName, "append") || !strcasecmp(methodName, "appendurl"))
	{
		command = std::make_unique<DownloadXmlCommand>();
	}
	else if (!strcasecmp(methodName, "postqueue"))
	{
		command = std::make_unique<PostQueueXmlCommand>();
	}
	else if (!strcasecmp(methodName, "writelog"))
	{
		command = std::make_unique<WriteLogXmlCommand>();
	}
	else if (!strcasecmp(methodName, "clearlog"))
	{
		command = std::make_unique<ClearLogXmlCommand>();
	}
	else if (!strcasecmp(methodName, "loadlog"))
	{
		command = std::make_unique<LoadLogXmlCommand>();
	}
	else if (!strcasecmp(methodName, "scan"))
	{
		command = std::make_unique<ScanXmlCommand>();
	}
	else if (!strcasecmp(methodName, "pausepost"))
	{
		command = std::make_unique<PauseUnpauseXmlCommand>(true, PauseUnpauseXmlCommand::paPostProcess);
	}
	else if (!strcasecmp(methodName, "resumepost"))
	{
		command = std::make_unique<PauseUnpauseXmlCommand>(false, PauseUnpauseXmlCommand::paPostProcess);
	}
	else if (!strcasecmp(methodName, "pausescan"))
	{
		command = std::make_unique<PauseUnpauseXmlCommand>(true, PauseUnpauseXmlCommand::paScan);
	}
	else if (!strcasecmp(methodName, "resumescan"))
	{
		command = std::make_unique<PauseUnpauseXmlCommand>(false, PauseUnpauseXmlCommand::paScan);
	}
	else if (!strcasecmp(methodName, "scheduleresume"))
	{
		command = std::make_unique<ScheduleResumeXmlCommand>();
	}
	else if (!strcasecmp(methodName, "history"))
	{
		command = std::make_unique<HistoryXmlCommand>();
	}
	else if (!strcasecmp(methodName, "urlqueue"))
	{
		command = std::make_unique<UrlQueueXmlCommand>();
	}
	else if (!strcasecmp(methodName, "config"))
	{
		command = std::make_unique<ConfigXmlCommand>();
	}
	else if (!strcasecmp(methodName, "loadconfig"))
	{
		command = std::make_unique<LoadConfigXmlCommand>();
	}
	else if (!strcasecmp(methodName, "loadextensions"))
	{
		command = std::make_unique<LoadExtensionsXmlCommand>();
	}
	else if (!strcasecmp(methodName, "downloadextension"))
	{
		command = std::make_unique<DownloadExtensionXmlCommand>();
	}
	else if (!strcasecmp(methodName, "updateextension"))
	{
		command = std::make_unique<UpdateExtensionXmlCommand>();
	}
	else if (!strcasecmp(methodName, "deleteextension"))
	{
		command = std::make_unique<DeleteExtensionXmlCommand>();
	}
	else if (!strcasecmp(methodName, "testextension"))
	{
		command = std::make_unique<TestExtensionXmlCommand>();
	}
	else if (!strcasecmp(methodName, "saveconfig"))
	{
		command = std::make_unique<SaveConfigXmlCommand>();
	}
	else if (!strcasecmp(methodName, "configtemplates"))
	{
		command = std::make_unique<ConfigTemplatesXmlCommand>();
	}
	else if (!strcasecmp(methodName, "viewfeed"))
	{
		command = std::make_unique<ViewFeedXmlCommand>(false);
	}
	else if (!strcasecmp(methodName, "previewfeed"))
	{
		command = std::make_unique<ViewFeedXmlCommand>(true);
	}
	else if (!strcasecmp(methodName, "fetchfeed"))
	{
		command = std::make_unique<FetchFeedXmlCommand>();
	}
	else if (!strcasecmp(methodName, "editserver"))
	{
		command = std::make_unique<EditServerXmlCommand>();
	}
	else if (!strcasecmp(methodName, "readurl"))
	{
		command = std::make_unique<ReadUrlXmlCommand>();
	}
	else if (!strcasecmp(methodName, "checkupdates"))
	{
		command = std::make_unique<CheckUpdatesXmlCommand>();
	}
	else if (!strcasecmp(methodName, "startupdate"))
	{
		command = std::make_unique<StartUpdateXmlCommand>();
	}
	else if (!strcasecmp(methodName, "logupdate"))
	{
		command = std::make_unique<LogUpdateXmlCommand>();
	}
	else if (!strcasecmp(methodName, "servervolumes"))
	{
		command = std::make_unique<ServerVolumesXmlCommand>();
	}
	else if (!strcasecmp(methodName, "resetservervolume"))
	{
		command = std::make_unique<ResetServerVolumeXmlCommand>();
	}
	else if (!strcasecmp(methodName, "testserver"))
	{
		command = std::make_unique<TestServerXmlCommand>();
	}
	else if (!strcasecmp(methodName, "testserverspeed"))
	{
		command = std::make_unique<TestServerSpeedXmlCommand>();
	}
	else if (!strcasecmp(methodName, "testdiskspeed"))
	{
		command = std::make_unique<TestDiskSpeedXmlCommand>();
	}
	else if (!strcasecmp(methodName, "testnetworkspeed"))
	{
		command = std::make_unique<TestNetworkSpeedXmlCommand>();
	}
	else if (!strcasecmp(methodName, "startscript"))
	{
		command = std::make_unique<StartScriptXmlCommand>();
	}
	else if (!strcasecmp(methodName, "logscript"))
	{
		command = std::make_unique<LogScriptXmlCommand>();
	}
	else
	{
		command = std::make_unique<ErrorXmlCommand>(1, "Invalid procedure");
	}

	return command;
}


//*****************************************************************
// Base command

XmlCommand::XmlCommand()
{
	m_response.Reserve(1024 * 10 - 1);
}

bool XmlCommand::IsJson()
{
	return m_protocol == XmlRpcProcessor::rpJsonRpc || m_protocol == XmlRpcProcessor::rpJsonPRpc;
}

void XmlCommand::AppendResponse(const char* part)
{
	m_response.Append(part);
}

void XmlCommand::AppendFmtResponse(const char* format, ...)
{
	va_list args;
	va_start(args, format);
	m_response.AppendFmtV(format, args);
	va_end(args);
}

void XmlCommand::AppendCondResponse(const char* part, bool cond)
{
	if (cond)
	{
		m_response.Append(part);
	}
}

void XmlCommand::BuildErrorResponse(int errCode, const char* errText, ...)
{
	const char* XML_RESPONSE_ERROR_BODY =
		"<struct>\n"
		"<member><name>faultCode</name><value><i4>%i</i4></value></member>\n"
		"<member><name>faultString</name><value><string>%s</string></value></member>\n"
		"</struct>\n";

	const char* JSON_RESPONSE_ERROR_BODY =
		"{\n"
		"\"name\" : \"JSONRPCError\",\n"
		"\"code\" : %i,\n"
		"\"message\" : \"%s\"\n"
		"}";

	char fullText[1024];

	va_list ap;
	va_start(ap, errText);
	vsnprintf(fullText, 1024, errText, ap);
	fullText[1024-1] = '\0';
	va_end(ap);

	BString<1024> content(IsJson() ? JSON_RESPONSE_ERROR_BODY : XML_RESPONSE_ERROR_BODY,
		errCode, *EncodeStr(fullText));

	AppendResponse(content);

	m_fault = true;
}

void XmlCommand::BuildBoolResponse(bool ok)
{
	const char* XML_RESPONSE_BOOL_BODY = "<boolean>%s</boolean>";
	const char* JSON_RESPONSE_BOOL_BODY = "%s";

	BString<1024> content(IsJson() ? JSON_RESPONSE_BOOL_BODY : XML_RESPONSE_BOOL_BODY,
		BoolToStr(ok));

	AppendResponse(content);
}

void XmlCommand::BuildIntResponse(int value)
{
	const char* XML_RESPONSE_INT_BODY = "<i4>%i</i4>";
	const char* JSON_RESPONSE_INT_BODY = "%i";

	BString<1024> content(IsJson() ? JSON_RESPONSE_INT_BODY : XML_RESPONSE_INT_BODY, value);

	AppendResponse(content);
}

void XmlCommand::PrepareParams()
{
	if (IsJson() && m_httpMethod == XmlRpcProcessor::hmPost)
	{
		char* params = strstr(m_requestPtr, "\"params\"");
		if (!params)
		{
			m_requestPtr[0] = '\0';
			return;
		}
		m_requestPtr = params + 8; // strlen("\"params\"")
	}

	if (m_protocol == XmlRpcProcessor::rpJsonPRpc)
	{
		NextParamAsStr(&m_callbackFunc);
	}
}

char* XmlCommand::XmlNextValue(char* xml, const char* tag, int* valueLength)
{
	int valueLen;
	const char* value = WebUtil::XmlFindTag(xml, "value", &valueLen);
	if (value)
	{
		char* tagContent = (char*)WebUtil::XmlFindTag(value, tag, valueLength);
		if (tagContent <= value + valueLen)
		{
			return tagContent;
		}
	}
	return nullptr;
}

bool XmlCommand::NextParamAsInt(int* value)
{
	if (m_httpMethod == XmlRpcProcessor::hmGet)
	{
		char* param = strchr(m_requestPtr, '=');
		if (!param)
		{
			return false;
		}
		*value = atoi(param + 1);
		m_requestPtr = param + 1;
		while (*m_requestPtr && strchr("-+0123456789&", *m_requestPtr))
		{
			m_requestPtr++;
		}
		return true;
	}
	else if (IsJson())
	{
		int len = 0;
		char* param = (char*)WebUtil::JsonNextValue(m_requestPtr, &len);
		if (!param || !strchr("-+0123456789", *param))
		{
			return false;
		}
		*value = atoi(param);
		m_requestPtr = param + len + 1;
		return true;
	}
	else
	{
		int len = 0;
		int tagLen = 4; //strlen("<i4>");
		char* param = XmlNextValue(m_requestPtr, "i4", &len);
		if (!param)
		{
			param = XmlNextValue(m_requestPtr, "int", &len);
			tagLen = 5; //strlen("<int>");
		}
		if (!param || !strchr("-+0123456789", *param))
		{
			return false;
		}
		*value = atoi(param);
		m_requestPtr = param + len + tagLen;
		return true;
	}
}

bool XmlCommand::NextParamAsBool(bool* value)
{
	if (m_httpMethod == XmlRpcProcessor::hmGet)
	{
		char* param;
		if (!NextParamAsStr(&param))
		{
			return false;
		}

		if (IsJson())
		{
			if (!strncmp(param, "true", 4))
			{
				*value = true;
				return true;
			}
			else if (!strncmp(param, "false", 5))
			{
				*value = false;
				return true;
			}
		}
		else
		{
			*value = param[0] == '1';
			return true;
		}
		return false;
	}
	else if (IsJson())
	{
		int len = 0;
		char* param = (char*)WebUtil::JsonNextValue(m_requestPtr, &len);
		if (!param)
		{
			return false;
		}
		if (len == 4 && !strncmp(param, "true", 4))
		{
			*value = true;
			m_requestPtr = param + len + 1;
			return true;
		}
		else if (len == 5 && !strncmp(param, "false", 5))
		{
			*value = false;
			m_requestPtr = param + len + 1;
			return true;
		}
		else
		{
			return false;
		}
	}
	else
	{
		int len = 0;
		char* param = XmlNextValue(m_requestPtr, "boolean", &len);
		if (!param)
		{
			return false;
		}
		*value = param[0] == '1';
		m_requestPtr = param + len + 9; //strlen("<boolean>");
		return true;
	}
}

bool XmlCommand::NextParamAsStr(char** value)
{
	if (m_httpMethod == XmlRpcProcessor::hmGet)
	{
		char* param = strchr(m_requestPtr, '=');
		if (!param)
		{
			return false;
		}
		param++; // skip '='
		int len = 0;
		char* paramEnd = strchr(param, '&');
		if (paramEnd)
		{
			len = (int)(paramEnd - param);
			param[len] = '\0';
		}
		else
		{
			len = strlen(param) - 1;
		}
		m_requestPtr = param + len + 1;
		*value = param;
		return true;
	}
	else if (IsJson())
	{
		int len = 0;
		char* param = (char*)WebUtil::JsonNextValue(m_requestPtr, &len);
		if (!param || len < 2 || param[0] != '"' || param[len - 1] != '"')
		{
			return false;
		}
		param++; // skip first '"'
		param[len - 2] = '\0'; // skip last '"'
		m_requestPtr = param + len;
		*value = param;
		return true;
	}
	else
	{
		int len = 0;
		char* param = XmlNextValue(m_requestPtr, "string", &len);
		if (!param)
		{
			return false;
		}
		param[len] = '\0';
		m_requestPtr = param + len + 8; //strlen("<string>")
		*value = param;
		return true;
	}
}

const char* XmlCommand::BoolToStr(bool value)
{
	return IsJson() ? (value ? "true" : "false") : (value ? "1" : "0");
}

CString XmlCommand::EncodeStr(const char* str)
{
	if (!str)
	{
		return "";
	}

	CString result;
	if (IsJson())
	{
		result = WebUtil::JsonEncode(str);
	}
	else
	{
		result = WebUtil::XmlEncode(str);
	}
	return result;
}

void XmlCommand::DecodeStr(char* str)
{
	if (IsJson())
	{
		WebUtil::JsonDecode(str);
	}
	else
	{
		WebUtil::XmlDecode(str);
	}
}

//*****************************************************************
// Commands

void ErrorXmlCommand::Execute()
{
	BuildErrorResponse(m_errCode, m_errText);
}

void PauseUnpauseXmlCommand::Execute()
{
	bool ok = true;

	g_WorkState->SetResumeTime(0);

	switch (m_pauseAction)
	{
		case paDownload:
			g_WorkState->SetPauseDownload(m_pause);
			break;

		case paPostProcess:
			g_WorkState->SetPausePostProcess(m_pause);
			break;

		case paScan:
			g_WorkState->SetPauseScan(m_pause);
			break;

		default:
			ok = false;
	}

	BuildBoolResponse(ok);
}

// bool scheduleresume(int Seconds)
void ScheduleResumeXmlCommand::Execute()
{
	int seconds = 0;
	if (!NextParamAsInt(&seconds) || seconds < 0)
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	time_t curTime = Util::CurrentTime();

	g_WorkState->SetResumeTime(curTime + seconds);

	BuildBoolResponse(true);
}

void ShutdownXmlCommand::Execute()
{
	BuildBoolResponse(true);
	ExitProc();
}

void ReloadXmlCommand::Execute()
{
	BuildBoolResponse(true);
	Reload();
}

void VersionXmlCommand::Execute()
{
	const char* XML_RESPONSE_STRING_BODY = "<string>%s</string>";
	const char* JSON_RESPONSE_STRING_BODY = "\"%s\"";

	BString<1024> content(IsJson() ? JSON_RESPONSE_STRING_BODY : XML_RESPONSE_STRING_BODY, Util::VersionRevision());

	AppendResponse(content);
}

void DumpDebugXmlCommand::Execute()
{
	g_Log->LogDebugInfo();
	BuildBoolResponse(true);
}

void SetDownloadRateXmlCommand::Execute()
{
	int rate = 0;
	if (!NextParamAsInt(&rate) || rate < 0)
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	g_WorkState->SetSpeedLimit(rate * 1024);
	BuildBoolResponse(true);
}

void StatusXmlCommand::Execute()
{
	const char* XML_STATUS_START =
		"<struct>\n"
		"<member><name>RemainingSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>RemainingSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>RemainingSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ForcedSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>ForcedSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>ForcedSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DownloadedSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>DownloadedSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>DownloadedSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>MonthSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>MonthSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>MonthSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DaySizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>DaySizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>DaySizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ArticleCacheLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>ArticleCacheHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>ArticleCacheMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DownloadRate</name><value><i4>%i</i4></value></member>\n"				// deprecated
		"<member><name>DownloadRateLo</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DownloadRateHi</name><value><i4>%i</i4></value></member>\n"
		"<member><name>AverageDownloadRate</name><value><i4>%i</i4></value></member>\n"			// deprecated
		"<member><name>AverageDownloadRateLo</name><value><i4>%i</i4></value></member>\n"
		"<member><name>AverageDownloadRateHi</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DownloadLimit</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ThreadCount</name><value><i4>%u</i4></value></member>\n"
		"<member><name>ParJobCount</name><value><i4>%i</i4></value></member>\n"					// deprecated (renamed to PostJobCount)
		"<member><name>PostJobCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>UrlCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>UpTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DownloadTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ServerPaused</name><value><boolean>%s</boolean></value></member>\n"		// deprecated (renamed to DownloadPaused)
		"<member><name>DownloadPaused</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>Download2Paused</name><value><boolean>%s</boolean></value></member>\n"	// deprecated (same as DownloadPaused)
		"<member><name>ServerStandBy</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>PostPaused</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>ScanPaused</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>QuotaReached</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>FreeDiskSpaceLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FreeDiskSpaceHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FreeDiskSpaceMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>TotalDiskSpaceLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>TotalDiskSpaceHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>TotalDiskSpaceMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FreeInterDiskSpaceLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FreeInterDiskSpaceHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FreeInterDiskSpaceMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>TotalInterDiskSpaceLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>TotalInterDiskSpaceHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>TotalInterDiskSpaceMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ServerTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ResumeTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FeedActive</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>QueueScriptCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>NewsServers</name><value><array><data>\n";

	const char* XML_STATUS_END =
		"</data></array></value></member>\n"
		"</struct>\n";

	const char* JSON_STATUS_START =
		"{\n"
		"\"RemainingSizeLo\" : %u,\n"
		"\"RemainingSizeHi\" : %u,\n"
		"\"RemainingSizeMB\" : %i,\n"
		"\"ForcedSizeLo\" : %u,\n"
		"\"ForcedSizeHi\" : %u,\n"
		"\"ForcedSizeMB\" : %i,\n"
		"\"DownloadedSizeLo\" : %u,\n"
		"\"DownloadedSizeHi\" : %u,\n"
		"\"DownloadedSizeMB\" : %i,\n"
		"\"MonthSizeLo\" : %u,\n"
		"\"MonthSizeHi\" : %u,\n"
		"\"MonthSizeMB\" : %i,\n"
		"\"DaySizeLo\" : %u,\n"
		"\"DaySizeHi\" : %u,\n"
		"\"DaySizeMB\" : %i,\n"
		"\"ArticleCacheLo\" : %u,\n"
		"\"ArticleCacheHi\" : %u,\n"
		"\"ArticleCacheMB\" : %i,\n"
		"\"DownloadRate\" : %i,\n"				// deprecated
		"\"DownloadRateLo\" : %u,\n"
		"\"DownloadRateHi\" : %u,\n"
		"\"AverageDownloadRate\" : %i,\n"		// deprecated
		"\"AverageDownloadRateLo\" : %u,\n"
		"\"AverageDownloadRateHi\" : %u,\n"
		"\"DownloadLimit\" : %i,\n"
		"\"ThreadCount\" : %u,\n"
		"\"ParJobCount\" : %i,\n"				// deprecated (renamed to PostJobCount)
		"\"PostJobCount\" : %i,\n"
		"\"UrlCount\" : %i,\n"
		"\"UpTimeSec\" : %i,\n"
		"\"DownloadTimeSec\" : %i,\n"
		"\"ServerPaused\" : %s,\n"				// deprecated (renamed to DownloadPaused)
		"\"DownloadPaused\" : %s,\n"
		"\"Download2Paused\" : %s,\n"			// deprecated (same as DownloadPaused)
		"\"ServerStandBy\" : %s,\n"
		"\"PostPaused\" : %s,\n"
		"\"ScanPaused\" : %s,\n"
		"\"QuotaReached\" : %s,\n"
		"\"FreeDiskSpaceLo\" : %u,\n"
		"\"FreeDiskSpaceHi\" : %u,\n"
		"\"FreeDiskSpaceMB\" : %i,\n"
		"\"TotalDiskSpaceLo\" : %u,\n"
		"\"TotalDiskSpaceHi\" : %u,\n"
		"\"TotalDiskSpaceMB\" : %i,\n"
		"\"FreeInterDiskSpaceLo\" : %u,\n"
		"\"FreeInterDiskSpaceHi\" : %u,\n"
		"\"FreeInterDiskSpaceMB\" : %i,\n"
		"\"TotalInterDiskSpaceLo\" : %u,\n"
		"\"TotalInterDiskSpaceHi\" : %u,\n"
		"\"TotalInterDiskSpaceMB\" : %i,\n"
		"\"ServerTime\" : %i,\n"
		"\"ResumeTime\" : %i,\n"
		"\"FeedActive\" : %s,\n"
		"\"QueueScriptCount\" : %i,\n"
		"\"NewsServers\" : [\n";

	const char* JSON_STATUS_END =
		"]\n"
		"}";

	const char* XML_NEWSSERVER_ITEM =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Active</name><value><boolean>%s</boolean></value></member>\n"
		"</struct></value>\n";

	const char* JSON_NEWSSERVER_ITEM =
		"{\n"
		"\"ID\" : %i,\n"
		"\"Active\" : %s\n"
		"}";

	int postJobCount = 0;
	int urlCount = 0;
	int64 remainingSize, forcedSize;
	{
		GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
		for (NzbInfo* nzbInfo : downloadQueue->GetQueue())
		{
			postJobCount += nzbInfo->GetPostInfo() ? 1 : 0;
			urlCount += nzbInfo->GetKind() == NzbInfo::nkUrl ? 1 : 0;
		}
		downloadQueue->CalcRemainingSize(&remainingSize, &forcedSize);
	}

	uint32 remainingSizeHi, remainingSizeLo;
	Util::SplitInt64(remainingSize, &remainingSizeHi, &remainingSizeLo);
	int remainingMBytes = (int)(remainingSize / 1024 / 1024);

	uint32 forcedSizeHi, forcedSizeLo;
	Util::SplitInt64(forcedSize, &forcedSizeHi, &forcedSizeLo);
	int forcedMBytes = (int)(forcedSize / 1024 / 1024);

	int64 articleCache = g_ArticleCache->GetAllocated();
	uint32 articleCacheHi, articleCacheLo;
	Util::SplitInt64(articleCache, &articleCacheHi, &articleCacheLo);
	int articleCacheMBytes = (int)(articleCache / 1024 / 1024);

	int64 downloadRate = g_StatMeter->CalcCurrentDownloadSpeed();
	uint32 downloadRateHi, downloadRateLo;
	Util::SplitInt64(downloadRate, &downloadRateHi, &downloadRateLo);

	int downloadLimit = (int)(g_WorkState->GetSpeedLimit());
	bool downloadPaused = g_WorkState->GetPauseDownload();
	bool postPaused = g_WorkState->GetPausePostProcess();
	bool scanPaused = g_WorkState->GetPauseScan();
	bool quotaReached = g_WorkState->GetQuotaReached();
	int threadCount = Thread::GetThreadCount() - 1; // not counting itself

	uint32 downloadedSizeHi, downloadedSizeLo;
	int upTimeSec, downloadTimeSec;
	int64 allBytes;
	bool serverStandBy;
	g_StatMeter->CalcTotalStat(&upTimeSec, &downloadTimeSec, &allBytes, &serverStandBy);
	int downloadedMBytes = (int)(allBytes / 1024 / 1024);
	Util::SplitInt64(allBytes, &downloadedSizeHi, &downloadedSizeLo);

	int64 averageDownloadRate = downloadTimeSec > 0 ? allBytes / downloadTimeSec : 0;
	uint32 averageDownloadRateHi, averageDownloadRateLo;
	Util::SplitInt64(averageDownloadRate, &averageDownloadRateHi, &averageDownloadRateLo);

	int64 monthBytes, dayBytes;
	g_StatMeter->CalcQuotaUsage(monthBytes, dayBytes);
	uint32 monthSizeHi, monthSizeLo;
	int monthMBytes = (int)(monthBytes / 1024 / 1024);
	Util::SplitInt64(monthBytes, &monthSizeHi, &monthSizeLo);

	uint32 daySizeHi, daySizeLo;
	int dayMBytes = (int)(dayBytes / 1024 / 1024);
	Util::SplitInt64(dayBytes, &daySizeHi, &daySizeLo);

	uint32 freeDiskSpaceHi, freeDiskSpaceLo;
	uint32 totalDiskSpaceHi, totalDiskSpaceLo;
	uint32 freeInterDiskSpaceHi, freeInterDiskSpaceLo;
	uint32 totalInterDiskSpaceHi, totalInterDiskSpaceLo;

	int64 freeDiskSpace = 0;
	int64 totalDiskSpace = 0;
	int64 freeInterDiskSpace = 0;
	int64 totalInterDiskSpace = 0;

	if (Util::EmptyStr(g_Options->GetDestDir()))
	{
		freeDiskSpace = 0;
		totalDiskSpace = 0;
	}
	else
	{
		auto res = FileSystem::GetDiskState(g_Options->GetDestDir());
		if (res.has_value())
		{
			const auto& value = res.value();
			freeDiskSpace = value.available;
			totalDiskSpace = value.total;
		}
	}

	if (Util::EmptyStr(g_Options->GetInterDir()))
	{
		freeInterDiskSpace = freeDiskSpace;
		totalInterDiskSpace = totalDiskSpace;
	}
	else
	{
		auto res = FileSystem::GetDiskState(g_Options->GetInterDir());
		if (res.has_value())
		{
			const auto& value = res.value();
			freeInterDiskSpace = value.available;
			totalInterDiskSpace = value.total;
		}
	}

	Util::SplitInt64(freeDiskSpace, &freeDiskSpaceHi, &freeDiskSpaceLo);
	Util::SplitInt64(totalDiskSpace, &totalDiskSpaceHi, &totalDiskSpaceLo);
	Util::SplitInt64(freeInterDiskSpace, &freeInterDiskSpaceHi, &freeInterDiskSpaceLo);
	Util::SplitInt64(totalInterDiskSpace, &totalInterDiskSpaceHi, &totalInterDiskSpaceLo);

	int freeDiskSpaceMB = static_cast<int>(freeDiskSpace / 1024 / 1024);
	int totalDiskSpaceMB = static_cast<int>(totalDiskSpace / 1024 / 1024);
	int freeInterDiskSpaceMB = static_cast<int>(freeInterDiskSpace / 1024 / 1024);
	int totalInterDiskSpaceMB = static_cast<int>(totalInterDiskSpace / 1024 / 1024);

	int serverTime = (int)Util::CurrentTime();
	int resumeTime = (int)g_WorkState->GetResumeTime();
	bool feedActive = g_FeedCoordinator->HasActiveDownloads();
	int queuedScripts = g_QueueScriptCoordinator->GetQueueSize();

	AppendFmtResponse(IsJson() ? JSON_STATUS_START : XML_STATUS_START,
		remainingSizeLo, remainingSizeHi, remainingMBytes, forcedSizeLo,
		forcedSizeHi, forcedMBytes, downloadedSizeLo, downloadedSizeHi, downloadedMBytes,
		monthSizeLo, monthSizeHi, monthMBytes, daySizeLo, daySizeHi, dayMBytes,
		articleCacheLo, articleCacheHi, articleCacheMBytes,
		Util::SafeIntCast<int64, int32>(downloadRate),
		downloadRateLo,
		downloadRateHi,
		Util::SafeIntCast<int64, int32>(averageDownloadRate),
		averageDownloadRateLo,
		averageDownloadRateHi,
		downloadLimit, threadCount,
		postJobCount, postJobCount, urlCount, upTimeSec, downloadTimeSec,
		BoolToStr(downloadPaused), BoolToStr(downloadPaused), BoolToStr(downloadPaused),
		BoolToStr(serverStandBy), BoolToStr(postPaused), BoolToStr(scanPaused), BoolToStr(quotaReached),
		freeDiskSpaceLo, 
		freeDiskSpaceHi, 
		freeDiskSpaceMB, 
		totalDiskSpaceLo, 
		totalDiskSpaceHi, 
		totalDiskSpaceMB,
		freeInterDiskSpaceLo, 
		freeInterDiskSpaceHi, 
		freeInterDiskSpaceMB, 
		totalInterDiskSpaceLo, 
		totalInterDiskSpaceHi, 
		totalInterDiskSpaceMB,
		serverTime, resumeTime, BoolToStr(feedActive), queuedScripts);

	int index = 0;
	for (NewsServer* server : g_ServerPool->GetServers())
	{
		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_NEWSSERVER_ITEM : XML_NEWSSERVER_ITEM,
			server->GetId(), BoolToStr(server->GetActive()));
	}

	AppendResponse(IsJson() ? JSON_STATUS_END : XML_STATUS_END);
}

void SysInfoXmlCommand::Execute()
{
	std::string response = IsJson()
		? System::ToJsonStr(*g_SystemInfo)
		: System::ToXmlStr(*g_SystemInfo);

	AppendResponse(response.c_str());
}

// struct[] log(idfrom, entries)
void LogXmlCommand::Execute()
{
	m_idFrom = 0;
	m_nrEntries = 0;
	if (!NextParamAsInt(&m_idFrom) || !NextParamAsInt(&m_nrEntries) || (m_nrEntries > 0 && m_idFrom > 0))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	debug("iIDFrom=%i", m_idFrom);
	debug("iNrEntries=%i", m_nrEntries);

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	GuardedMessageList messages = GuardMessages();

	int start = messages->size();
	if (m_nrEntries > 0)
	{
		if (m_nrEntries > (int)messages->size())
		{
			m_nrEntries = messages->size();
		}
		start = messages->size() - m_nrEntries;
	}
	if (m_idFrom > 0 && !messages->empty())
	{
		m_nrEntries = messages->size();
		start = m_idFrom - messages->front().GetId();
		if (start < 0)
		{
			start = 0;
		}
	}

	const char* XML_LOG_ITEM =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Kind</name><value><string>%s</string></value></member>\n"
		"<member><name>Time</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Text</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_LOG_ITEM =
		"{\n"
		"\"ID\" : %i,\n"
		"\"Kind\" : \"%s\",\n"
		"\"Time\" : %i,\n"
		"\"Text\" : \"%s\"\n"
		"}";

	const char* messageType[] = { "INFO", "WARNING", "ERROR", "DEBUG", "DETAIL" };

	int index = 0;

	for (uint32 i = (uint32)start; i < messages->size(); i++)
	{
		Message& message = messages->at(i);

		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_LOG_ITEM : XML_LOG_ITEM,
			message.GetId(), messageType[message.GetKind()], (int)message.GetTime(),
			*EncodeStr(message.GetText()));
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

GuardedMessageList LogXmlCommand::GuardMessages()
{
	return g_Log->GuardMessages();
}

// struct[] listfiles(int IDFrom, int IDTo, int NZBID)
// For backward compatibility with 0.8 parameter "NZBID" is optional
void ListFilesXmlCommand::Execute()
{
	int idStart = 0;
	int idEnd = 0;
	if (NextParamAsInt(&idStart) && (!NextParamAsInt(&idEnd) || idEnd < idStart))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	// For backward compatibility with 0.8 parameter "NZBID" is optional (error checking omitted)
	int nzbId = 0;
	NextParamAsInt(&nzbId);

	if (nzbId > 0 && (idStart != 0 || idEnd != 0))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	debug("iIDStart=%i", idStart);
	debug("iIDEnd=%i", idEnd);

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	const char* XML_LIST_ITEM =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FileSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FileSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>RemainingSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>RemainingSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>PostTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FilenameConfirmed</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>Paused</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>NZBID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>NZBName</name><value><string>%s</string></value></member>\n"
		"<member><name>NZBNicename</name><value><string>%s</string></value></member>\n"	// deprecated, use "NZBName" instead
		"<member><name>NZBFilename</name><value><string>%s</string></value></member>\n"
		"<member><name>Subject</name><value><string>%s</string></value></member>\n"
		"<member><name>Filename</name><value><string>%s</string></value></member>\n"
		"<member><name>DestDir</name><value><string>%s</string></value></member>\n"
		"<member><name>Category</name><value><string>%s</string></value></member>\n"
		"<member><name>Priority</name><value><i4>%i</i4></value></member>\n"			// deprecated, use "Priority" of group instead
		"<member><name>ActiveDownloads</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Progress</name><value><i4>%u</i4></value></member>\n"
		"</struct></value>\n";

	const char* JSON_LIST_ITEM =
		"{\n"
		"\"ID\" : %i,\n"
		"\"FileSizeLo\" : %u,\n"
		"\"FileSizeHi\" : %u,\n"
		"\"RemainingSizeLo\" : %u,\n"
		"\"RemainingSizeHi\" : %u,\n"
		"\"PostTime\" : %i,\n"
		"\"FilenameConfirmed\" : %s,\n"
		"\"Paused\" : %s,\n"
		"\"NZBID\" : %i,\n"
		"\"NZBName\" : \"%s\",\n"
		"\"NZBNicename\" : \"%s\",\n" 		// deprecated, use "NZBName" instead
		"\"NZBFilename\" : \"%s\",\n"
		"\"Subject\" : \"%s\",\n"
		"\"Filename\" : \"%s\",\n"
		"\"DestDir\" : \"%s\",\n"
		"\"Category\" : \"%s\",\n"
		"\"Priority\" : %i,\n"				// deprecated, use "Priority" of group instead
		"\"ActiveDownloads\" : %i,\n"
		"\"Progress\" : %i\n"
		"}";

	int index = 0;

	GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
	for (NzbInfo* nzbInfo : downloadQueue->GetQueue())
	{
		for (FileInfo* fileInfo : nzbInfo->GetFileList())
		{
			if ((nzbId > 0 && nzbId == fileInfo->GetNzbInfo()->GetId()) ||
				(nzbId == 0 && (idStart == 0 || (idStart <= fileInfo->GetId() && fileInfo->GetId() <= idEnd))))
			{
				uint32 fileSizeHi, fileSizeLo;
				uint32 remainingSizeLo, remainingSizeHi;
				Util::SplitInt64(fileInfo->GetSize(), &fileSizeHi, &fileSizeLo);
				Util::SplitInt64(fileInfo->GetRemainingSize(), &remainingSizeHi, &remainingSizeLo);
				CString xmlNzbNicename = EncodeStr(fileInfo->GetNzbInfo()->GetName());

				int progress = fileInfo->GetFailedSize() == 0 && fileInfo->GetSuccessSize() == 0 ? 0 :
					(int)(1000 - fileInfo->GetRemainingSize() * 1000 / (fileInfo->GetSize() - fileInfo->GetMissedSize()));

				AppendCondResponse(",\n", IsJson() && index++ > 0);
				AppendFmtResponse(IsJson() ? JSON_LIST_ITEM : XML_LIST_ITEM,
					fileInfo->GetId(), fileSizeLo, fileSizeHi, remainingSizeLo, remainingSizeHi,
					(int)fileInfo->GetTime(), BoolToStr(fileInfo->GetFilenameConfirmed()),
					BoolToStr(fileInfo->GetPaused()), fileInfo->GetNzbInfo()->GetId(),
					*xmlNzbNicename, *xmlNzbNicename, *EncodeStr(fileInfo->GetNzbInfo()->GetFilename()),
					*EncodeStr(fileInfo->GetSubject()), *EncodeStr(fileInfo->GetFilename()),
					*EncodeStr(fileInfo->GetNzbInfo()->GetDestDir()), *EncodeStr(fileInfo->GetNzbInfo()->GetCategory()),
					fileInfo->GetNzbInfo()->GetPriority(), fileInfo->GetActiveDownloads(), progress);
			}
		}
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

void NzbInfoXmlCommand::AppendNzbInfoFields(NzbInfo* nzbInfo)
{
	const char* XML_NZB_ITEM_START =
		"<member><name>NZBID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>NZBName</name><value><string>%s</string></value></member>\n"
		"<member><name>NZBNicename</name><value><string>%s</string></value></member>\n"	// deprecated, use "NZBName" instead
		"<member><name>Kind</name><value><string>%s</string></value></member>\n"
		"<member><name>URL</name><value><string>%s</string></value></member>\n"
		"<member><name>NZBFilename</name><value><string>%s</string></value></member>\n"
		"<member><name>DestDir</name><value><string>%s</string></value></member>\n"
		"<member><name>FinalDir</name><value><string>%s</string></value></member>\n"
		"<member><name>Category</name><value><string>%s</string></value></member>\n"
		"<member><name>ParStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>ExParStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>UnpackStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>MoveStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>ScriptStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>DeleteStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>MarkStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>UrlStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>FileSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FileSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FileSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FileCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>MinPostTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>MaxPostTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>TotalArticles</name><value><i4>%i</i4></value></member>\n"
		"<member><name>SuccessArticles</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FailedArticles</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Health</name><value><i4>%i</i4></value></member>\n"
		"<member><name>CriticalHealth</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DupeKey</name><value><string>%s</string></value></member>\n"
		"<member><name>DupeScore</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DupeMode</name><value><string>%s</string></value></member>\n"
		"<member><name>Deleted</name><value><boolean>%s</boolean></value></member>\n"	 // deprecated, use "DeleteStatus" instead
		"<member><name>DownloadedSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>DownloadedSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>DownloadedSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DownloadTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>PostTotalTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ParTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>RepairTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>UnpackTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>MessageCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ExtraParBlocks</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Parameters</name><value><array><data>\n";

	const char* XML_NZB_ITEM_SCRIPT_START =
		"</data></array></value></member>\n"
		"<member><name>ScriptStatuses</name><value><array><data>\n";

	const char* XML_NZB_ITEM_STATS_START =
		"</data></array></value></member>\n"
		"<member><name>ServerStats</name><value><array><data>\n";

	const char* XML_NZB_ITEM_END =
		"</data></array></value></member>\n";

	const char* JSON_NZB_ITEM_START =
		"\"NZBID\" : %i,\n"
		"\"NZBName\" : \"%s\",\n"
		"\"NZBNicename\" : \"%s\",\n"		// deprecated, use NZBName instead
		"\"Kind\" : \"%s\",\n"
		"\"URL\" : \"%s\",\n"
		"\"NZBFilename\" : \"%s\",\n"
		"\"DestDir\" : \"%s\",\n"
		"\"FinalDir\" : \"%s\",\n"
		"\"Category\" : \"%s\",\n"
		"\"ParStatus\" : \"%s\",\n"
		"\"ExParStatus\" : \"%s\",\n"
		"\"UnpackStatus\" : \"%s\",\n"
		"\"MoveStatus\" : \"%s\",\n"
		"\"ScriptStatus\" : \"%s\",\n"
		"\"DeleteStatus\" : \"%s\",\n"
		"\"MarkStatus\" : \"%s\",\n"
		"\"UrlStatus\" : \"%s\",\n"
		"\"FileSizeLo\" : %u,\n"
		"\"FileSizeHi\" : %u,\n"
		"\"FileSizeMB\" : %i,\n"
		"\"FileCount\" : %i,\n"
		"\"MinPostTime\" : %i,\n"
		"\"MaxPostTime\" : %i,\n"
		"\"TotalArticles\" : %i,\n"
		"\"SuccessArticles\" : %i,\n"
		"\"FailedArticles\" : %i,\n"
		"\"Health\" : %i,\n"
		"\"CriticalHealth\" : %i,\n"
		"\"DupeKey\" : \"%s\",\n"
		"\"DupeScore\" : %i,\n"
		"\"DupeMode\" : \"%s\",\n"
		"\"Deleted\" : %s,\n"			  // deprecated, use "DeleteStatus" instead
		"\"DownloadedSizeLo\" : %u,\n"
		"\"DownloadedSizeHi\" : %u,\n"
		"\"DownloadedSizeMB\" : %i,\n"
		"\"DownloadTimeSec\" : %i,\n"
		"\"PostTotalTimeSec\" : %i,\n"
		"\"ParTimeSec\" : %i,\n"
		"\"RepairTimeSec\" : %i,\n"
		"\"UnpackTimeSec\" : %i,\n"
		"\"MessageCount\" : %i,\n"
		"\"ExtraParBlocks\" : %i,\n"
		"\"Parameters\" : [\n";

	const char* JSON_NZB_ITEM_SCRIPT_START =
		"],\n"
		"\"ScriptStatuses\" : [\n";

	const char* JSON_NZB_ITEM_STATS_START =
		"],\n"
		"\"ServerStats\" : [\n";

	const char* JSON_NZB_ITEM_END =
		"]\n";

	const char* XML_PARAMETER_ITEM =
		"<value><struct>\n"
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>Value</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_PARAMETER_ITEM =
		"{\n"
		"\"Name\" : \"%s\",\n"
		"\"Value\" : \"%s\"\n"
		"}";

	const char* XML_SCRIPT_ITEM =
		"<value><struct>\n"
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>Status</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_SCRIPT_ITEM =
		"{\n"
		"\"Name\" : \"%s\",\n"
		"\"Status\" : \"%s\"\n"
		"}";

	const char* XML_STAT_ITEM =
		"<value><struct>\n"
		"<member><name>ServerID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>SuccessArticles</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FailedArticles</name><value><i4>%i</i4></value></member>\n"
		"</struct></value>\n";

	const char* JSON_STAT_ITEM =
		"{\n"
		"\"ServerID\" : %i,\n"
		"\"SuccessArticles\" : %i,\n"
		"\"FailedArticles\" : %i\n"
		"}";

	const char* kindName[] = { "NZB", "URL" };
	const char* parStatusName[] = { "NONE", "NONE", "FAILURE", "SUCCESS", "REPAIR_POSSIBLE", "MANUAL" };
	const char* unpackStatusName[] = { "NONE", "NONE", "FAILURE", "SUCCESS", "SPACE", "PASSWORD" };
	const char* moveStatusName[] = { "NONE", "FAILURE", "SUCCESS" };
	const char* scriptStatusName[] = { "NONE", "FAILURE", "SUCCESS" };
	const char* deleteStatusName[] = { "NONE", "MANUAL", "HEALTH", "DUPE", "BAD", "GOOD", "COPY", "SCAN" };
	const char* markStatusName[] = { "NONE", "BAD", "GOOD", "SUCCESS" };
	const char* urlStatusName[] = { "NONE", "UNKNOWN", "SUCCESS", "FAILURE", "UNKNOWN", "SCAN_SKIPPED", "SCAN_FAILURE" };
	const char* dupeModeName[] = { "SCORE", "ALL", "FORCE" };

	uint32 fileSizeHi, fileSizeLo, fileSizeMB;
	Util::SplitInt64(nzbInfo->GetSize(), &fileSizeHi, &fileSizeLo);
	fileSizeMB = (int)(nzbInfo->GetSize() / 1024 / 1024);

	uint32 downloadedSizeHi, downloadedSizeLo, downloadedSizeMB;
	Util::SplitInt64(nzbInfo->GetDownloadedSize(), &downloadedSizeHi, &downloadedSizeLo);
	downloadedSizeMB = (int)(nzbInfo->GetDownloadedSize() / 1024 / 1024);

	int messageCount = nzbInfo->GetMessageCount() > 0 ? nzbInfo->GetMessageCount() : nzbInfo->GetCachedMessageCount();

	CString xmlNzbNicename = EncodeStr(nzbInfo->GetName());
	const char* exParStatus = nzbInfo->GetExtraParBlocks() > 0 ? "RECIPIENT" : nzbInfo->GetExtraParBlocks() < 0 ? "DONOR" : "NONE";

	AppendFmtResponse(IsJson() ? JSON_NZB_ITEM_START : XML_NZB_ITEM_START,
			nzbInfo->GetId(), *xmlNzbNicename, *xmlNzbNicename, kindName[nzbInfo->GetKind()],
			*EncodeStr(nzbInfo->GetUrl()), *EncodeStr(nzbInfo->GetFilename()),
			*EncodeStr(nzbInfo->GetDestDir()), *EncodeStr(nzbInfo->GetFinalDir()),
			*EncodeStr(nzbInfo->GetCategory()), parStatusName[nzbInfo->GetParStatus()], exParStatus,
			unpackStatusName[nzbInfo->GetUnpackStatus()], moveStatusName[nzbInfo->GetMoveStatus()],
			scriptStatusName[nzbInfo->GetScriptStatuses()->CalcTotalStatus()],
			deleteStatusName[nzbInfo->GetDeleteStatus()], markStatusName[nzbInfo->GetMarkStatus()],
			urlStatusName[nzbInfo->GetUrlStatus()],
			fileSizeLo, fileSizeHi, fileSizeMB, nzbInfo->GetFileCount(),
			(int)nzbInfo->GetMinTime(), (int)nzbInfo->GetMaxTime(),
			nzbInfo->GetTotalArticles(), nzbInfo->GetCurrentSuccessArticles(), nzbInfo->GetCurrentFailedArticles(),
			nzbInfo->CalcHealth(), nzbInfo->CalcCriticalHealth(false),
			*EncodeStr(nzbInfo->GetDupeKey()), nzbInfo->GetDupeScore(), dupeModeName[nzbInfo->GetDupeMode()],
			BoolToStr(nzbInfo->GetDeleteStatus() != NzbInfo::dsNone),
			downloadedSizeLo, downloadedSizeHi, downloadedSizeMB, nzbInfo->GetDownloadSec(),
			(int)(nzbInfo->GetPostTotalSec() + (nzbInfo->GetPostInfo() && nzbInfo->GetPostInfo()->GetStartTime() ?
				Util::CurrentTime() - nzbInfo->GetPostInfo()->GetStartTime() : 0)),
			nzbInfo->GetParSec(), nzbInfo->GetRepairSec(), nzbInfo->GetUnpackSec(), messageCount, nzbInfo->GetExtraParBlocks());

	// Post-processing parameters
	int paramIndex = 0;
	for (NzbParameter& parameter : nzbInfo->GetParameters())
	{
		AppendCondResponse(",\n", IsJson() && paramIndex++ > 0);
		AppendFmtResponse(IsJson() ? JSON_PARAMETER_ITEM : XML_PARAMETER_ITEM,
			*EncodeStr(parameter.GetName()), *EncodeStr(parameter.GetValue()));
	}

	AppendResponse(IsJson() ? JSON_NZB_ITEM_SCRIPT_START : XML_NZB_ITEM_SCRIPT_START);

	// Script statuses
	int scriptIndex = 0;
	for (ScriptStatus& scriptStatus : nzbInfo->GetScriptStatuses())
	{
		AppendCondResponse(",\n", IsJson() && scriptIndex++ > 0);
		AppendFmtResponse(IsJson() ? JSON_SCRIPT_ITEM : XML_SCRIPT_ITEM,
			*EncodeStr(scriptStatus.GetName()), *EncodeStr(scriptStatusName[scriptStatus.GetStatus()]));
	}

	AppendResponse(IsJson() ? JSON_NZB_ITEM_STATS_START : XML_NZB_ITEM_STATS_START);

	// Server stats
	int statIndex = 0;
	for (ServerStat& serverStat : nzbInfo->GetCurrentServerStats())
	{
		AppendCondResponse(",\n", IsJson() && statIndex++ > 0);
		AppendFmtResponse(IsJson() ? JSON_STAT_ITEM : XML_STAT_ITEM,
				 serverStat.GetServerId(), serverStat.GetSuccessArticles(), serverStat.GetFailedArticles());
	}

	AppendResponse(IsJson() ? JSON_NZB_ITEM_END : XML_NZB_ITEM_END);
}

void NzbInfoXmlCommand::AppendPostInfoFields(PostInfo* postInfo, int logEntries, bool postQueue)
{
	const char* XML_GROUPQUEUE_ITEM_START =
		"<member><name>PostInfoText</name><value><string>%s</string></value></member>\n"
		"<member><name>PostStageProgress</name><value><i4>%i</i4></value></member>\n"
		"<member><name>PostStageTimeSec</name><value><i4>%i</i4></value></member>\n";
		// PostTotalTimeSec is printed by method "AppendNZBInfoFields"

	const char* XML_POSTQUEUE_ITEM_START =
		"<member><name>ProgressLabel</name><value><string>%s</string></value></member>\n"
		"<member><name>StageProgress</name><value><i4>%i</i4></value></member>\n"
		"<member><name>StageTimeSec</name><value><i4>%i</i4></value></member>\n"
		"<member><name>TotalTimeSec</name><value><i4>%i</i4></value></member>\n";

	const char* XML_LOG_START =
		"<member><name>Log</name><value><array><data>\n";

	const char* XML_POSTQUEUE_ITEM_END =
		"</data></array></value></member>\n";

	const char* JSON_GROUPQUEUE_ITEM_START =
		"\"PostInfoText\" : \"%s\",\n"
		"\"PostStageProgress\" : %i,\n"
		"\"PostStageTimeSec\" : %i,\n";

	const char* JSON_POSTQUEUE_ITEM_START =
		"\"ProgressLabel\" : \"%s\",\n"
		"\"StageProgress\" : %i,\n"
		"\"StageTimeSec\" : %i,\n"
		"\"TotalTimeSec\" : %i,\n";

	const char* JSON_LOG_START =
		"\"Log\" : [\n";

	const char* JSON_POSTQUEUE_ITEM_END =
		"]\n";

	const char* XML_LOG_ITEM =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Kind</name><value><string>%s</string></value></member>\n"
		"<member><name>Time</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Text</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_LOG_ITEM =
		"{\n"
		"\"ID\" : %i,\n"
		"\"Kind\" : \"%s\",\n"
		"\"Time\" : %i,\n"
		"\"Text\" : \"%s\"\n"
		"}";

	const char* messageType[] = { "INFO", "WARNING", "ERROR", "DEBUG", "DETAIL"};

	const char* itemStart = postQueue ? IsJson() ? JSON_POSTQUEUE_ITEM_START : XML_POSTQUEUE_ITEM_START :
		IsJson() ? JSON_GROUPQUEUE_ITEM_START : XML_GROUPQUEUE_ITEM_START;

	if (postInfo)
	{
		time_t curTime = Util::CurrentTime();

		AppendFmtResponse(itemStart, *EncodeStr(postInfo->GetProgressLabel()),
			postInfo->GetStageProgress(),
			(int)(postInfo->GetStageTime() ? curTime - postInfo->GetStageTime() : 0),
			(int)(postInfo->GetStartTime() ? curTime - postInfo->GetStartTime() : 0));
	}
	else
	{
		AppendFmtResponse(itemStart, "NONE", "", 0, 0, 0, 0);
	}

	AppendResponse(IsJson() ? JSON_LOG_START : XML_LOG_START);

	if (logEntries > 0 && postInfo)
	{
		GuardedMessageList messages = postInfo->GetNzbInfo()->GuardCachedMessages();
		if (!messages->empty())
		{
			if (logEntries > (int)messages->size())
			{
				logEntries = messages->size();
			}
			int start = messages->size() - logEntries;

			int index = 0;
			for (uint32 i = (uint32)start; i < messages->size(); i++)
			{
				Message& message = messages->at(i);

				AppendCondResponse(",\n", IsJson() && index++ > 0);
				AppendFmtResponse(IsJson() ? JSON_LOG_ITEM : XML_LOG_ITEM,
					message.GetId(), messageType[message.GetKind()], (int)message.GetTime(),
					*EncodeStr(message.GetText()));
			}
		}
	}

	AppendResponse(IsJson() ? JSON_POSTQUEUE_ITEM_END : XML_POSTQUEUE_ITEM_END);
}

// struct[] listgroups(int NumberOfLogEntries)
void ListGroupsXmlCommand::Execute()
{
	int nrEntries = 0;
	NextParamAsInt(&nrEntries);

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	const char* XML_LIST_ITEM_START =
		"<value><struct>\n"
		"<member><name>FirstID</name><value><i4>%i</i4></value></member>\n"				// deprecated, use "NZBID" instead
		"<member><name>LastID</name><value><i4>%i</i4></value></member>\n"				// deprecated, use "NZBID" instead
		"<member><name>RemainingSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>RemainingSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>RemainingSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>PausedSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>PausedSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>PausedSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>RemainingFileCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>RemainingParCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>MinPriority</name><value><i4>%i</i4></value></member>\n"
		"<member><name>MaxPriority</name><value><i4>%i</i4></value></member>\n"
		"<member><name>ActiveDownloads</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Status</name><value><string>%s</string></value></member>\n";

	const char* XML_LIST_ITEM_END =
		"</struct></value>\n";

	const char* JSON_LIST_ITEM_START =
		"{\n"
		"\"FirstID\" : %i,\n"					// deprecated, use "NZBID" instead
		"\"LastID\" : %i,\n"					// deprecated, use "NZBID" instead
		"\"RemainingSizeLo\" : %u,\n"
		"\"RemainingSizeHi\" : %u,\n"
		"\"RemainingSizeMB\" : %i,\n"
		"\"PausedSizeLo\" : %u,\n"
		"\"PausedSizeHi\" : %u,\n"
		"\"PausedSizeMB\" : %i,\n"
		"\"RemainingFileCount\" : %i,\n"
		"\"RemainingParCount\" : %i,\n"
		"\"MinPriority\" : %i,\n"
		"\"MaxPriority\" : %i,\n"
		"\"ActiveDownloads\" : %i,\n"
		"\"Status\" : \"%s\",\n";

	const char* JSON_LIST_ITEM_END =
		"}";

	int index = 0;

	GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
	for (NzbInfo* nzbInfo : downloadQueue->GetQueue())
	{
		uint32 remainingSizeLo, remainingSizeHi, remainingSizeMB;
		uint32 pausedSizeLo, pausedSizeHi, pausedSizeMB;
		Util::SplitInt64(nzbInfo->GetRemainingSize(), &remainingSizeHi, &remainingSizeLo);
		remainingSizeMB = (int)(nzbInfo->GetRemainingSize() / 1024 / 1024);
		Util::SplitInt64(nzbInfo->GetPausedSize(), &pausedSizeHi, &pausedSizeLo);
		pausedSizeMB = (int)(nzbInfo->GetPausedSize() / 1024 / 1024);
		const char* status = DetectStatus(nzbInfo);

		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_LIST_ITEM_START : XML_LIST_ITEM_START,
			nzbInfo->GetId(), nzbInfo->GetId(), remainingSizeLo, remainingSizeHi, remainingSizeMB,
			pausedSizeLo, pausedSizeHi, pausedSizeMB, (int)nzbInfo->GetFileList()->size(),
			nzbInfo->GetRemainingParCount(), nzbInfo->GetPriority(), nzbInfo->GetPriority(),
			nzbInfo->GetActiveDownloads(), status);

		AppendNzbInfoFields(nzbInfo);
		AppendCondResponse(",\n", IsJson());
		AppendPostInfoFields(nzbInfo->GetPostInfo(), nrEntries, false);

		AppendResponse(IsJson() ? JSON_LIST_ITEM_END : XML_LIST_ITEM_END);
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

const char* ListGroupsXmlCommand::DetectStatus(NzbInfo* nzbInfo)
{
	const char* postStageName[] = { "PP_QUEUED", "LOADING_PARS", "VERIFYING_SOURCES", "REPAIRING",
		"VERIFYING_REPAIRED", "RENAMING", "RENAMING", "UNPACKING", "MOVING", "MOVING", "POST_UNPACK_RENAMING", 
		"EXECUTING_SCRIPT", "PP_FINISHED" };

	const char* status = nullptr;

	if (nzbInfo->GetPostInfo())
	{
		bool queueScriptActive = false;
		if (nzbInfo->GetPostInfo()->GetStage() == PostInfo::ptQueued &&
			g_QueueScriptCoordinator->HasJob(nzbInfo->GetId(), &queueScriptActive))
		{
			status = queueScriptActive ? "QS_EXECUTING" : "QS_QUEUED";
		}
		else if (nzbInfo->GetDirectUnpackStatus() == NzbInfo::nsRunning)
		{
			status = "UNPACKING";
		}
		else
		{
			status = postStageName[nzbInfo->GetPostInfo()->GetStage()];
		}
	}
	else if (nzbInfo->GetActiveDownloads() > 0)
	{
		status = nzbInfo->GetKind() == NzbInfo::nkUrl ? "FETCHING" : "DOWNLOADING";
	}
	else if ((nzbInfo->GetPausedSize() > 0) && (nzbInfo->GetRemainingSize() == nzbInfo->GetPausedSize()))
	{
		status = "PAUSED";
	}
	else
	{
		status = "QUEUED";
	}

	return status;
}

struct EditCommandEntry
{
	int actionId;
	const char* actionName;
};

EditCommandEntry EditCommandNameMap[] = {
	{ DownloadQueue::eaFileMoveOffset, "FileMoveOffset" },
	{ DownloadQueue::eaFileMoveTop, "FileMoveTop" },
	{ DownloadQueue::eaFileMoveBottom, "FileMoveBottom" },
	{ DownloadQueue::eaFilePause, "FilePause" },
	{ DownloadQueue::eaFileResume, "FileResume" },
	{ DownloadQueue::eaFileDelete, "FileDelete" },
	{ DownloadQueue::eaFilePauseAllPars, "FilePauseAllPars" },
	{ DownloadQueue::eaFilePauseExtraPars, "FilePauseExtraPars" },
	{ DownloadQueue::eaFileReorder, "FileReorder" },
	{ DownloadQueue::eaFileSplit, "FileSplit" },
	{ DownloadQueue::eaGroupMoveOffset, "GroupMoveOffset" },
	{ DownloadQueue::eaGroupMoveTop, "GroupMoveTop" },
	{ DownloadQueue::eaGroupMoveBottom, "GroupMoveBottom" },
	{ DownloadQueue::eaGroupMoveBefore, "GroupMoveBefore" },
	{ DownloadQueue::eaGroupMoveAfter, "GroupMoveAfter" },
	{ DownloadQueue::eaGroupPause, "GroupPause" },
	{ DownloadQueue::eaGroupResume, "GroupResume" },
	{ DownloadQueue::eaGroupDelete, "GroupDelete" },
	{ DownloadQueue::eaGroupParkDelete, "GroupParkDelete" },
	{ DownloadQueue::eaGroupDupeDelete, "GroupDupeDelete" },
	{ DownloadQueue::eaGroupFinalDelete, "GroupFinalDelete" },
	{ DownloadQueue::eaGroupPauseAllPars, "GroupPauseAllPars" },
	{ DownloadQueue::eaGroupPauseExtraPars, "GroupPauseExtraPars" },
	{ DownloadQueue::eaGroupSetPriority, "GroupSetPriority" },
	{ DownloadQueue::eaGroupSetCategory, "GroupSetCategory" },
	{ DownloadQueue::eaGroupApplyCategory, "GroupApplyCategory" },
	{ DownloadQueue::eaGroupMerge, "GroupMerge" },
	{ DownloadQueue::eaGroupSetParameter, "GroupSetParameter" },
	{ DownloadQueue::eaGroupSetName, "GroupSetName" },
	{ DownloadQueue::eaGroupSetDupeKey, "GroupSetDupeKey" },
	{ DownloadQueue::eaGroupSetDupeScore, "GroupSetDupeScore" },
	{ DownloadQueue::eaGroupSetDupeMode, "GroupSetDupeMode" },
	{ DownloadQueue::eaGroupSort, "GroupSort" },
	{ DownloadQueue::eaGroupSortFiles, "GroupSortFiles" },
	{ DownloadQueue::eaPostDelete, "PostDelete" },
	{ DownloadQueue::eaHistoryDelete, "HistoryDelete" },
	{ DownloadQueue::eaHistoryFinalDelete, "HistoryFinalDelete" },
	{ DownloadQueue::eaHistoryReturn, "HistoryReturn" },
	{ DownloadQueue::eaHistoryProcess, "HistoryProcess" },
	{ DownloadQueue::eaHistoryRedownload, "HistoryRedownload" },
	{ DownloadQueue::eaHistoryRetryFailed, "HistoryRetryFailed" },
	{ DownloadQueue::eaHistorySetParameter, "HistorySetParameter" },
	{ DownloadQueue::eaHistorySetDupeKey, "HistorySetDupeKey" },
	{ DownloadQueue::eaHistorySetDupeScore, "HistorySetDupeScore" },
	{ DownloadQueue::eaHistorySetDupeMode, "HistorySetDupeMode" },
	{ DownloadQueue::eaHistorySetDupeBackup, "HistorySetDupeBackup" },
	{ DownloadQueue::eaHistoryMarkBad, "HistoryMarkBad" },
	{ DownloadQueue::eaHistoryMarkGood, "HistoryMarkGood" },
	{ DownloadQueue::eaHistoryMarkSuccess, "HistoryMarkSuccess" },
	{ DownloadQueue::eaHistorySetCategory, "HistorySetCategory" },
	{ DownloadQueue::eaHistorySetName, "HistorySetName" },
	{ 0, nullptr }
};

// v18:
//    bool editqueue(string Command, string Args, int[] IDs)
// v17:
//    bool editqueue(string Command, int Offset, string Args, int[] IDs)
void EditQueueXmlCommand::Execute()
{
	char* editCommand;
	if (!NextParamAsStr(&editCommand))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}
	debug("EditCommand=%s", editCommand);

	int action = -1;
	for (int i = 0; const char* name = EditCommandNameMap[i].actionName; i++)
	{
		if (!strcasecmp(editCommand, name))
		{
			action = EditCommandNameMap[i].actionId;
			break;
		}
	}

	if (action == -1)
	{
		BuildErrorResponse(3, "Invalid action");
		return;
	}

	int offset = 0;
	bool hasOffset = NextParamAsInt(&offset);

	char* args;
	if (!NextParamAsStr(&args))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}
	debug("Args=%s", args);

	DecodeStr(args);

	BString<100> offsetStr("%i", offset);
	if (hasOffset && (action == DownloadQueue::eaFileMoveOffset ||
		action == DownloadQueue::eaGroupMoveOffset))
	{
		args = *offsetStr;
	}

	IdList idList;
	int id = 0;
	while (NextParamAsInt(&id))
	{
		idList.push_back(id);
	}

	bool ok = DownloadQueue::Guard()->EditList(&idList, nullptr, DownloadQueue::mmId,
		(DownloadQueue::EEditAction)action, args);

	BuildBoolResponse(ok);
}

// v16:
//   int append(string NZBFilename, string NZBContent, string Category, int Priority, bool AddToTop, bool AddPaused, string DupeKey, int DupeScore, string DupeMode, struct[] Parameters)
// v13 (new param order and new result type):
//   int append(string NZBFilename, string NZBContent, string Category, int Priority, bool AddToTop, bool AddPaused, string DupeKey, int DupeScore, string DupeMode)
// v12 (backward compatible, some params are optional):
//   bool append(string NZBFilename, string Category, int Priority, bool AddToTop, string Content, bool AddPaused, string DupeKey, int DupeScore, string DupeMode)
void DownloadXmlCommand::Execute()
{
	bool v13 = true;

	char* nzbFilename;
	if (!NextParamAsStr(&nzbFilename))
	{
		BuildErrorResponse(2, "Invalid parameter (NZBFileName)");
		return;
	}

	char* nzbContent;
	if (!NextParamAsStr(&nzbContent))
	{
		BuildErrorResponse(2, "Invalid parameter (NZBContent)");
		return;
	}

	char* category;
	if (!NextParamAsStr(&category))
	{
		v13 = false;
		category = nzbContent;
	}

	DecodeStr(nzbFilename);
	DecodeStr(category);

	debug("FileName=%s", nzbFilename);

	// For backward compatibility with 0.8 parameter "Priority" is optional (error checking omitted)
	int priority = 0;
	NextParamAsInt(&priority);

	bool addTop;
	if (!NextParamAsBool(&addTop))
	{
		BuildErrorResponse(2, "Invalid parameter (AddTop)");
		return;
	}

	if (!v13 && !NextParamAsStr(&nzbContent))
	{
		BuildErrorResponse(2, "Invalid parameter (FileContent)");
		return;
	}
	DecodeStr(nzbContent);

	bool addPaused = false;
	char* dupeKey = nullptr;
	int dupeScore = 0;
	EDupeMode dupeMode = dmScore;
	if (NextParamAsBool(&addPaused))
	{
		if (!NextParamAsStr(&dupeKey))
		{
			BuildErrorResponse(2, "Invalid parameter (DupeKey)");
			return;
		}
		DecodeStr(dupeKey);
		if (!NextParamAsInt(&dupeScore))
		{
			BuildErrorResponse(2, "Invalid parameter (DupeScore)");
			return;
		}
		char* dupeModeStr = nullptr;
		if (!NextParamAsStr(&dupeModeStr) ||
			(strcasecmp(dupeModeStr, "score") && strcasecmp(dupeModeStr, "all") && strcasecmp(dupeModeStr, "force")))
		{
			BuildErrorResponse(2, "Invalid parameter (DupeMode)");
			return;
		}
		dupeMode = !strcasecmp(dupeModeStr, "all") ? dmAll :
			!strcasecmp(dupeModeStr, "force") ? dmForce : dmScore;
	}
	else if (v13)
	{
		BuildErrorResponse(2, "Invalid parameter (AddPaused)");
		return;
	}

	NzbParameterList Params;
	if (v13)
	{
		char* paramName = nullptr;
		char* paramValue = nullptr;
		while (NextParamAsStr(&paramName))
		{
			if (!NextParamAsStr(&paramValue))
			{
				BuildErrorResponse(2, "Invalid parameter (Parameters)");
				return;
			}
			Params.SetParameter(paramName, paramValue);
		}
	}

	if (!strncasecmp(nzbContent, "http://", 7) || !strncasecmp(nzbContent, "https://", 8))
	{
		// add url
		std::unique_ptr<NzbInfo> nzbInfo = std::make_unique<NzbInfo>();
		nzbInfo->SetKind(NzbInfo::nkUrl);
		nzbInfo->SetUrl(nzbContent);
		nzbInfo->SetFilename(nzbFilename);
		nzbInfo->SetCategory(category);
		nzbInfo->SetPriority(priority);
		nzbInfo->SetAddUrlPaused(addPaused);
		nzbInfo->SetDupeKey(dupeKey ? dupeKey : "");
		nzbInfo->SetDupeScore(dupeScore);
		nzbInfo->SetDupeMode(dupeMode);
		nzbInfo->GetParameters()->CopyFrom(&Params);
		int nzbId = nzbInfo->GetId();

		info("Queue %s", *nzbInfo->MakeNiceUrlName(nzbContent, nzbFilename));

		g_UrlCoordinator->AddUrlToQueue(std::move(nzbInfo), addTop);

		if (v13)
		{
			BuildIntResponse(nzbId);
		}
		else
		{
			BuildBoolResponse(true);
		}
	}
	else
	{
		// add file content
		int len = WebUtil::DecodeBase64(nzbContent, 0, nzbContent);
		nzbContent[len] = '\0';
		//debug("FileContent=%s", szFileContent);

		int nzbId = -1;
		g_Scanner->AddExternalFile(nzbFilename, category, priority,
			dupeKey, dupeScore, dupeMode, Params.empty() ? nullptr : &Params, addTop, addPaused, nullptr,
			nullptr, nzbContent, len, &nzbId);

		if (v13)
		{
			BuildIntResponse(nzbId);
		}
		else
		{
			BuildBoolResponse(nzbId > 0);
		}
	}
}

// deprecated
void PostQueueXmlCommand::Execute()
{
	int nrEntries = 0;
	NextParamAsInt(&nrEntries);

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	const char* XML_POSTQUEUE_ITEM_START =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>InfoName</name><value><string>%s</string></value></member>\n"
		"<member><name>ParFilename</name><value><string></string></value></member>\n"		// deprecated, always empty
		"<member><name>Stage</name><value><string>%s</string></value></member>\n"
		"<member><name>FileProgress</name><value><i4>%i</i4></value></member>\n";

	const char* XML_POSTQUEUE_ITEM_END =
		"</struct></value>\n";

	const char* JSON_POSTQUEUE_ITEM_START =
		"{\n"
		"\"ID\" : %i,\n"
		"\"InfoName\" : \"%s\",\n"
		"\"ParFilename\" : \"\",\n"		// deprecated, always empty
		"\"Stage\" : \"%s\",\n"
		"\"FileProgress\" : %i,\n";

	const char* JSON_POSTQUEUE_ITEM_END =
		"}";

	const char* postStageName[] = { "QUEUED", "LOADING_PARS", "VERIFYING_SOURCES", "REPAIRING",
		"VERIFYING_REPAIRED", "RENAMING", "RENAMING", "UNPACKING", "MOVING", "MOVING", "EXECUTING_SCRIPT", "FINISHED" };

	int index = 0;

	GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
	for (NzbInfo* nzbInfo : downloadQueue->GetQueue())
	{
		PostInfo* postInfo = nzbInfo->GetPostInfo();
		if (!postInfo)
		{
			continue;
		}

		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_POSTQUEUE_ITEM_START : XML_POSTQUEUE_ITEM_START,
			nzbInfo->GetId(), *EncodeStr(postInfo->GetNzbInfo()->GetName()),
			postStageName[postInfo->GetStage()], postInfo->GetFileProgress());

		AppendNzbInfoFields(postInfo->GetNzbInfo());
		AppendCondResponse(",\n", IsJson());
		AppendPostInfoFields(postInfo, nrEntries, true);

		AppendResponse(IsJson() ? JSON_POSTQUEUE_ITEM_END : XML_POSTQUEUE_ITEM_END);
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

void WriteLogXmlCommand::Execute()
{
	char* kind;
	char* text;
	if (!NextParamAsStr(&kind) || !NextParamAsStr(&text))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	DecodeStr(text);

	debug("Kind=%s, Text=%s", kind, text);

	if (!strcmp(kind, "INFO"))
	{
		info("%s", text);
	}
	else if (!strcmp(kind, "WARNING"))
	{
		warn("%s", text);
	}
	else if (!strcmp(kind, "ERROR"))
	{
		error("%s", text);
	}
	else if (!strcmp(kind, "DETAIL"))
	{
		detail("%s", text);
	}
	else if (!strcmp(kind, "DEBUG"))
	{
		debug("%s", text);
	}
	else
	{
		BuildErrorResponse(3, "Invalid Kind");
		return;
	}

	BuildBoolResponse(true);
}

void ClearLogXmlCommand::Execute()
{
	g_Log->Clear();

	BuildBoolResponse(true);
}

void ScanXmlCommand::Execute()
{
	bool syncMode = false;
	// optional parameter "SyncMode"
	NextParamAsBool(&syncMode);

	g_Scanner->ScanNzbDir(syncMode);
	BuildBoolResponse(true);
}

// struct[] history(bool hidden)
// Parameter "hidden" is optional (new in v12)
void HistoryXmlCommand::Execute()
{
	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	const char* XML_HISTORY_ITEM_START =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"					// Deprecated, use "NZBID" instead
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>RemainingFileCount</name><value><i4>%i</i4></value></member>\n"
		"<member><name>RetryData</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>HistoryTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Status</name><value><string>%s</string></value></member>\n"
		"<member><name>Log</name><value><array><data></data></array></value></member>\n";	// Deprected, always empty

	const char* JSON_HISTORY_ITEM_START =
		"{\n"
		"\"ID\" : %i,\n"								// Deprecated, use "NZBID" instead
		"\"Name\" : \"%s\",\n"
		"\"RemainingFileCount\" : %i,\n"
		"\"RetryData\" : %s,\n"
		"\"HistoryTime\" : %i,\n"
		"\"Status\" : \"%s\",\n"
		"\"Log\" : [],\n";								// Deprected, always empty

	const char* XML_HISTORY_ITEM_END =
		"</struct></value>";

	const char* JSON_HISTORY_ITEM_END =
		"}";

	const char* XML_HISTORY_DUP_ITEM =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"				// Deprecated, use "NZBID" instead
		"<member><name>NZBID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Kind</name><value><string>%s</string></value></member>\n"
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>HistoryTime</name><value><i4>%i</i4></value></member>\n"
		"<member><name>FileSizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FileSizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>FileSizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DupeKey</name><value><string>%s</string></value></member>\n"
		"<member><name>DupeScore</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DupeMode</name><value><string>%s</string></value></member>\n"
		"<member><name>DupStatus</name><value><string>%s</string></value></member>\n"
		"<member><name>Status</name><value><string>%s</string></value></member>\n";

	const char* JSON_HISTORY_DUP_ITEM =
		"{\n"
		"\"ID\" : %i,\n"							// Deprecated, use "NZBID" instead
		"\"NZBID\" : %i,\n"
		"\"Kind\" : \"%s\",\n"
		"\"Name\" : \"%s\",\n"
		"\"HistoryTime\" : %i,\n"
		"\"FileSizeLo\" : %u,\n"
		"\"FileSizeHi\" : %u,\n"
		"\"FileSizeMB\" : %i,\n"
		"\"DupeKey\" : \"%s\",\n"
		"\"DupeScore\" : %i,\n"
		"\"DupeMode\" : \"%s\",\n"
		"\"DupStatus\" : \"%s\",\n"
		"\"Status\" : \"%s\"\n";

	const char* dupStatusName[] = { "UNKNOWN", "SUCCESS", "FAILURE", "DELETED", "DUPE", "BAD", "GOOD" };
	const char* dupeModeName[] = { "SCORE", "ALL", "FORCE" };

	bool dup = false;
	NextParamAsBool(&dup);

	int index = 0;

	GuardedDownloadQueue guard = DownloadQueue::Guard();
	for (HistoryInfo* historyInfo : guard->GetHistory())
	{
		if (historyInfo->GetKind() == HistoryInfo::hkDup && !dup)
		{
			continue;
		}

		NzbInfo* nzbInfo = nullptr;

		const char* status = DetectStatus(historyInfo);

		AppendCondResponse(",\n", IsJson() && index++ > 0);

		if (historyInfo->GetKind() == HistoryInfo::hkNzb ||
			historyInfo->GetKind() == HistoryInfo::hkUrl)
		{
			nzbInfo = historyInfo->GetNzbInfo();

			AppendFmtResponse(IsJson() ? JSON_HISTORY_ITEM_START : XML_HISTORY_ITEM_START,
				historyInfo->GetId(), *EncodeStr(historyInfo->GetName()), nzbInfo->GetParkedFileCount(),
				BoolToStr(nzbInfo->GetCompletedFiles()->size()), (int)historyInfo->GetTime(), status);
		}
		else if (historyInfo->GetKind() == HistoryInfo::hkDup)
		{
			DupInfo* dupInfo = historyInfo->GetDupInfo();

			uint32 fileSizeHi, fileSizeLo, fileSizeMB;
			Util::SplitInt64(dupInfo->GetSize(), &fileSizeHi, &fileSizeLo);
			fileSizeMB = (int)(dupInfo->GetSize() / 1024 / 1024);

			AppendFmtResponse(IsJson() ? JSON_HISTORY_DUP_ITEM : XML_HISTORY_DUP_ITEM,
				historyInfo->GetId(), historyInfo->GetId(), "DUP", *EncodeStr(historyInfo->GetName()),
				(int)historyInfo->GetTime(), fileSizeLo, fileSizeHi, fileSizeMB,
				*EncodeStr(dupInfo->GetDupeKey()), dupInfo->GetDupeScore(),
				dupeModeName[dupInfo->GetDupeMode()], dupStatusName[dupInfo->GetStatus()],
				status);
		}

		if (nzbInfo)
		{
			AppendNzbInfoFields(nzbInfo);
		}

		AppendResponse(IsJson() ? JSON_HISTORY_ITEM_END : XML_HISTORY_ITEM_END);
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

const char* HistoryXmlCommand::DetectStatus(HistoryInfo* historyInfo)
{
	const char* status = "FAILURE/INTERNAL_ERROR";

	if (historyInfo->GetKind() == HistoryInfo::hkNzb || historyInfo->GetKind() == HistoryInfo::hkUrl)
	{
		NzbInfo* nzbInfo = historyInfo->GetNzbInfo();
		status = nzbInfo->MakeTextStatus(false);
	}
	else if (historyInfo->GetKind() == HistoryInfo::hkDup)
	{
		DupInfo* dupInfo = historyInfo->GetDupInfo();
		const char* dupStatusName[] = { "FAILURE/INTERNAL_ERROR", "SUCCESS/HIDDEN", "FAILURE/HIDDEN",
			"DELETED/MANUAL", "DELETED/DUPE", "FAILURE/BAD", "SUCCESS/GOOD" };
		status = dupStatusName[dupInfo->GetStatus()];
	}

	return status;
}

// Deprecated in v13
void UrlQueueXmlCommand::Execute()
{
	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	const char* XML_URLQUEUE_ITEM =
		"<value><struct>\n"
		"<member><name>ID</name><value><i4>%i</i4></value></member>\n"
		"<member><name>NZBFilename</name><value><string>%s</string></value></member>\n"
		"<member><name>URL</name><value><string>%s</string></value></member>\n"
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>Category</name><value><string>%s</string></value></member>\n"
		"<member><name>Priority</name><value><i4>%i</i4></value></member>\n"
		"</struct></value>\n";

	const char* JSON_URLQUEUE_ITEM =
		"{\n"
		"\"ID\" : %i,\n"
		"\"NZBFilename\" : \"%s\",\n"
		"\"URL\" : \"%s\",\n"
		"\"Name\" : \"%s\",\n"
		"\"Category\" : \"%s\",\n"
		"\"Priority\" : %i\n"
		"}";

	int index = 0;

	GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
	for (NzbInfo* nzbInfo : downloadQueue->GetQueue())
	{
		if (nzbInfo->GetKind() == NzbInfo::nkUrl)
		{
			AppendCondResponse(",\n", IsJson() && index++ > 0);
			AppendFmtResponse(IsJson() ? JSON_URLQUEUE_ITEM : XML_URLQUEUE_ITEM,
				nzbInfo->GetId(), *EncodeStr(nzbInfo->GetFilename()), *EncodeStr(nzbInfo->GetUrl()),
				*EncodeStr(nzbInfo->GetName()), *EncodeStr(nzbInfo->GetCategory()), nzbInfo->GetPriority());
		}
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

// struct[] config()
void ConfigXmlCommand::Execute()
{
	const char* XML_CONFIG_ITEM =
		"<value><struct>\n"
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>Value</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_CONFIG_ITEM =
		"{\n"
		"\"Name\" : \"%s\",\n"
		"\"Value\" : \"%s\"\n"
		"}";

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	int index = 0;

	for (Options::OptEntry& optEntry : g_Options->GuardOptEntries())
	{
		CString xmlValue = EncodeStr(m_userAccess == XmlRpcProcessor::uaRestricted &&
			optEntry.Restricted() ? "***" : optEntry.GetValue());

		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_CONFIG_ITEM : XML_CONFIG_ITEM,
			*EncodeStr(optEntry.GetName()), *xmlValue);
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

// struct[] loadconfig()
void LoadConfigXmlCommand::Execute()
{
	const char* XML_CONFIG_ITEM =
		"<value><struct>\n"
		"<member><name>Name</name><value><string>%s</string></value></member>\n"
		"<member><name>Value</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_CONFIG_ITEM =
		"{\n"
		"\"Name\" : \"%s\",\n"
		"\"Value\" : \"%s\"\n"
		"}";

	Options::OptEntries optEntries;
	if (!g_ScriptConfig->LoadConfig(&optEntries))
	{
		BuildErrorResponse(3, "Could not read configuration file");
		return;
	}

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	int index = 0;

	for (Options::OptEntry& optEntry: optEntries)
	{
		CString xmlValue = EncodeStr(m_userAccess == XmlRpcProcessor::uaRestricted &&
			optEntry.Restricted() ? "***" : optEntry.GetValue());

		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_CONFIG_ITEM : XML_CONFIG_ITEM,
			*EncodeStr(optEntry.GetName()), *xmlValue);
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

void LoadExtensionsXmlCommand::Execute()
{
	bool loadFromDisk = false;
	bool isJson = IsJson();

	if (!NextParamAsBool(&loadFromDisk))
	{
		BuildErrorResponse(2, "Invalid parameter (Load from disk)");
		return;
	}

	if (loadFromDisk)
	{
		const auto& error = g_ExtensionManager->LoadExtensions();
		if (error)
		{
			BuildErrorResponse(3, error.value().c_str());
			return;
		}	
	}

	AppendResponse(isJson ? "[\n" : "<array><data>\n");

	int index = 0;

	for (const auto extension : g_ExtensionManager->GetExtensions())
	{
		std::string response = isJson
			? Extension::ToJsonStr(*extension)
			: Extension::ToXmlStr(*extension);

		AppendCondResponse(",\n", isJson && index++ > 0);
		AppendResponse(response.c_str());
	}

	AppendResponse(isJson ? "\n]" : "</data></array>\n");
}

void DownloadExtensionXmlCommand::Execute()
{
	char* url;
	if (!NextParamAsStr(&url))
	{
		BuildErrorResponse(2, "Invalid parameter (URL)");
		return;
	}
	DecodeStr(url);

	char* extName;
	if (!NextParamAsStr(&extName))
	{
		BuildErrorResponse(2, "Invalid parameter (Extension name)");
		return;
	}
	DecodeStr(extName);

	if (Util::EmptyStr(g_Options->GetTempDir()))
	{
		BuildErrorResponse(3, "\"TempDir\" is not specified");
		return;
	}

	const auto result = g_ExtensionManager->DownloadExtension(url, extName);
	bool ok = std::get<0>(result) == WebDownloader::adFinished;
	if (!ok)
	{
		BuildErrorResponse(3, "Failed to read URL");
		return;
	}

	Tokenizer tokDir(g_Options->GetScriptDir(), ",;");
	const char* scriptDir = tokDir.Next();
	if (Util::EmptyStr(scriptDir))
	{
		BuildErrorResponse(3, "\"ScriptDir\" is not specified");
		return;
	}

	const std::string& filename = std::get<1>(result);
	const auto error = g_ExtensionManager->InstallExtension(filename, scriptDir);
	if (error)
	{
		BuildErrorResponse(3, error.value().c_str());
		return;
	}

	BuildBoolResponse(true);
}

void UpdateExtensionXmlCommand::Execute()
{
	char* url;
	if (!NextParamAsStr(&url))
	{
		BuildErrorResponse(2, "Invalid parameter (URL)");
		return;
	}
	DecodeStr(url);

	char* extName;
	if (!NextParamAsStr(&extName))
	{
		BuildErrorResponse(2, "Invalid parameter (Extension name)");
		return;
	}
	DecodeStr(extName);

	const auto result = g_ExtensionManager->DownloadExtension(url, extName);
	bool ok = std::get<0>(result) == WebDownloader::adFinished;
	if (!ok)
	{
		BuildErrorResponse(3, "Failed to read URL");
		return;
	}

	const std::string& filename = std::get<1>(result);
	const auto error = g_ExtensionManager->UpdateExtension(filename, extName);
	if (error)
	{
		BuildErrorResponse(3, error.value().c_str());
		return;
	}

	BuildBoolResponse(true);
}

void DeleteExtensionXmlCommand::Execute()
{
	char* extName;
	if (!NextParamAsStr(&extName))
	{
		BuildErrorResponse(2, "Invalid parameter (Extension name)");
		return;
	}
	DecodeStr(extName);

	const auto error = g_ExtensionManager->DeleteExtension(extName);
	if (error)
	{
		BuildErrorResponse(2, error.value().c_str());
		return;
	}

	BuildBoolResponse(true);
}

void TestExtensionXmlCommand::Execute()
{
	char* extEntryFileName;
	if (!NextParamAsStr(&extEntryFileName))
	{
		BuildErrorResponse(2, "Invalid parameter (Extension entry file name)");
		return;
	}
	DecodeStr(extEntryFileName);

	const auto found = Util::FindExecutorProgram(extEntryFileName, g_Options->GetShellOverride());
	if (!found)
	{
		BuildErrorResponse(2, "Failed to find the corresponding executor");
		return;
	}

	BuildBoolResponse(true);
}

// bool saveconfig(struct[] data)
void SaveConfigXmlCommand::Execute()
{
	Options::OptEntries optEntries;

	char* name;
	char* value;
	char* dummy;
	while ((IsJson() && NextParamAsStr(&dummy) && NextParamAsStr(&name) &&
			NextParamAsStr(&dummy) && NextParamAsStr(&value)) ||
		   (!IsJson() && NextParamAsStr(&name) && NextParamAsStr(&value))) 
	{
		DecodeStr(name);
		DecodeStr(value);
		optEntries.emplace_back(name, value);
	}

	// save to config file
	bool ok = g_ScriptConfig->SaveConfig(&optEntries);

	BuildBoolResponse(ok);
}

// struct[] configtemplates(bool loadFromDisk)
// parameter "loadFromDisk" is optional (new in v14)
void ConfigTemplatesXmlCommand::Execute()
{
	const char* XML_CONFIG_ITEM =
		"<value><struct>\n"
		"<member><name>Template</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_CONFIG_ITEM =
		"{\n"
		"\"Template\" : \"%s\"\n"
		"}";

	bool loadFromDisk = false;
	NextParamAsBool(&loadFromDisk);

	ScriptConfig::ConfigTemplates* configTemplates = g_ScriptConfig->GetConfigTemplates();

	ScriptConfig::ConfigTemplates loadedConfigTemplates;
	if (loadFromDisk)
	{
		if (!g_ScriptConfig->LoadConfigTemplates(&loadedConfigTemplates))
		{
			BuildErrorResponse(3, "Could not read configuration templates");
			return;
		}
		configTemplates = &loadedConfigTemplates;
	}

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	int index = 0;

	for (ScriptConfig::ConfigTemplate& configTemplate : configTemplates)
	{
		AppendCondResponse(",\n", IsJson() && index++ > 0);
		AppendFmtResponse(IsJson() ? JSON_CONFIG_ITEM : XML_CONFIG_ITEM,
			*EncodeStr(configTemplate.GetTemplate()));
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

// struct[] viewfeed(int id)
// v12:
// struct[] previewfeed(string name, string url, string filter, bool pauseNzb, string category,
//		int priority, bool includeNonMatching, int cacheTimeSec, string cacheId)
// v16:
// struct[] previewfeed(int id, string name, string url, string filter, bool backlog, bool pauseNzb, string category,
//		int priority, int interval, string feedfilter, bool includeNonMatching, int cacheTimeSec, string cacheId)
void ViewFeedXmlCommand::Execute()
{
	bool includeNonMatching = false;
	std::shared_ptr<FeedItemList> feedItems;

	if (m_preview)
	{
		int id = 0;
		char* name;
		char* url;
		char* filter;
		bool backlog = true;
		bool pauseNzb;
		char* category;
		int interval = 0;
		int priority;
		char* feedFilter = nullptr;
		char* cacheId;
		int cacheTimeSec;

		// if the first parameter is int then it's the v16 signature
		bool v16 = NextParamAsInt(&id);

		if (!NextParamAsStr(&name) || !NextParamAsStr(&url) || !NextParamAsStr(&filter) ||
			(v16 && !NextParamAsBool(&backlog)) || !NextParamAsBool(&pauseNzb) ||
			!NextParamAsStr(&category) || !NextParamAsInt(&priority) ||
			(v16 && (!NextParamAsInt(&interval) || !NextParamAsStr(&feedFilter))) ||
			!NextParamAsBool(&includeNonMatching) || !NextParamAsInt(&cacheTimeSec) ||
			!NextParamAsStr(&cacheId))
		{
			BuildErrorResponse(2, "Invalid parameter");
			return;
		}

		DecodeStr(name);
		DecodeStr(url);
		DecodeStr(filter);
		DecodeStr(cacheId);
		DecodeStr(category);

		debug("Url=%s", url);
		debug("Filter=%s", filter);

		feedItems = g_FeedCoordinator->PreviewFeed(id, name, url, filter, backlog, pauseNzb,
			category, priority, interval, feedFilter, cacheTimeSec, cacheId);
	}
	else
	{
		int id = 0;
		if (!NextParamAsInt(&id) || !NextParamAsBool(&includeNonMatching))
		{
			BuildErrorResponse(2, "Invalid parameter");
			return;
		}

		debug("ID=%i", id);

		feedItems = g_FeedCoordinator->ViewFeed(id);
	}

	if (!feedItems)
	{
		BuildErrorResponse(3, "Could not read feed");
		return;
	}

	const char* XML_FEED_ITEM =
		"<value><struct>\n"
		"<member><name>Title</name><value><string>%s</string></value></member>\n"
		"<member><name>Filename</name><value><string>%s</string></value></member>\n"
		"<member><name>URL</name><value><string>%s</string></value></member>\n"
		"<member><name>SizeLo</name><value><i4>%u</i4></value></member>\n"
		"<member><name>SizeHi</name><value><i4>%u</i4></value></member>\n"
		"<member><name>SizeMB</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Category</name><value><string>%s</string></value></member>\n"
		"<member><name>AddCategory</name><value><string>%s</string></value></member>\n"
		"<member><name>PauseNzb</name><value><boolean>%s</boolean></value></member>\n"
		"<member><name>Priority</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Time</name><value><i4>%i</i4></value></member>\n"
		"<member><name>Match</name><value><string>%s</string></value></member>\n"
		"<member><name>Rule</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DupeKey</name><value><string>%s</string></value></member>\n"
		"<member><name>DupeScore</name><value><i4>%i</i4></value></member>\n"
		"<member><name>DupeMode</name><value><string>%s</string></value></member>\n"
		"<member><name>Status</name><value><string>%s</string></value></member>\n"
		"</struct></value>\n";

	const char* JSON_FEED_ITEM =
		"{\n"
		"\"Title\" : \"%s\",\n"
		"\"Filename\" : \"%s\",\n"
		"\"URL\" : \"%s\",\n"
		"\"SizeLo\" : %u,\n"
		"\"SizeHi\" : %u,\n"
		"\"SizeMB\" : %i,\n"
		"\"Category\" : \"%s\",\n"
		"\"AddCategory\" : \"%s\",\n"
		"\"PauseNzb\" : %s,\n"
		"\"Priority\" : %i,\n"
		"\"Time\" : %i,\n"
		"\"Match\" : \"%s\",\n"
		"\"Rule\" : %i,\n"
		"\"DupeKey\" : \"%s\",\n"
		"\"DupeScore\" : %i,\n"
		"\"DupeMode\" : \"%s\",\n"
		"\"Status\" : \"%s\"\n"
		"}";

	const char* statusType[] = { "UNKNOWN", "BACKLOG", "FETCHED", "NEW" };
	const char* matchStatusType[] = { "IGNORED", "ACCEPTED", "REJECTED" };
	const char* dupeModeType[] = { "SCORE", "ALL", "FORCE" };

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");
	int index = 0;

	for (FeedItemInfo& feedItemInfo : feedItems.get())
	{
		if (includeNonMatching || feedItemInfo.GetMatchStatus() == FeedItemInfo::msAccepted)
		{
			uint32 sizeHi, sizeLo;
			Util::SplitInt64(feedItemInfo.GetSize(), &sizeHi, &sizeLo);
			int sizeMB = (int)(feedItemInfo.GetSize() / 1024 / 1024);

			AppendCondResponse(",\n", IsJson() && index++ > 0);
			AppendFmtResponse(IsJson() ? JSON_FEED_ITEM : XML_FEED_ITEM,
				*EncodeStr(feedItemInfo.GetTitle()), *EncodeStr(feedItemInfo.GetFilename()),
				*EncodeStr(feedItemInfo.GetUrl()), sizeLo, sizeHi, sizeMB,
				*EncodeStr(feedItemInfo.GetCategory()), *EncodeStr(feedItemInfo.GetAddCategory()),
				BoolToStr(feedItemInfo.GetPauseNzb()), feedItemInfo.GetPriority(), (int)feedItemInfo.GetTime(),
				matchStatusType[feedItemInfo.GetMatchStatus()], feedItemInfo.GetMatchRule(),
				*EncodeStr(feedItemInfo.GetDupeKey()), feedItemInfo.GetDupeScore(),
				dupeModeType[feedItemInfo.GetDupeMode()], statusType[feedItemInfo.GetStatus()]);
		}
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

// bool fetchfeed(int ID)
void FetchFeedXmlCommand::Execute()
{
	int id;
	if (!NextParamAsInt(&id))
	{
		BuildErrorResponse(2, "Invalid parameter (ID)");
		return;
	}

	g_FeedCoordinator->FetchFeed(id);

	BuildBoolResponse(true);
}

// bool editserver(int ID, bool Active)
void EditServerXmlCommand::Execute()
{
	bool ok = false;
	int first = true;

	int id;
	while (NextParamAsInt(&id))
	{
		first = false;

		bool active;
		if (!NextParamAsBool(&active))
		{
			BuildErrorResponse(2, "Invalid parameter");
			return;
		}

		for (NewsServer* server : g_ServerPool->GetServers())
		{
			if (server->GetId() == id)
			{
				server->SetActive(active);
				ok = true;
			}
		}
	}

	if (first)
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	if (ok)
	{
		g_ServerPool->Changed();
	}

	BuildBoolResponse(ok);
}

// string readurl(string url, string infoname)
void ReadUrlXmlCommand::Execute()
{
	char* url;
	if (!NextParamAsStr(&url))
	{
		BuildErrorResponse(2, "Invalid parameter (URL)");
		return;
	}
	DecodeStr(url);

	char* infoName;
	if (!NextParamAsStr(&infoName))
	{
		BuildErrorResponse(2, "Invalid parameter (InfoName)");
		return;
	}
	DecodeStr(infoName);

	// generate temp file name
	BString<1024> tempFileName;
	int num = 1;
	while (num == 1 || FileSystem::FileExists(tempFileName))
	{
		tempFileName.Format("%s%creadurl-%i.tmp", g_Options->GetTempDir(), PATH_SEPARATOR, num);
		num++;
	}

	std::unique_ptr<WebDownloader> downloader = std::make_unique<WebDownloader>();
	downloader->SetUrl(url);
	downloader->SetForce(true);
	downloader->SetRetry(false);
	downloader->SetOutputFilename(tempFileName);
	downloader->SetInfoName(infoName);

	// do sync download
	WebDownloader::EStatus status = downloader->DownloadWithRedirects(5);
	bool ok = status == WebDownloader::adFinished;

	downloader.reset();

	if (ok)
	{
		CharBuffer fileContent;
		FileSystem::LoadFileIntoBuffer(tempFileName, fileContent, true);
		CString xmlContent = EncodeStr(fileContent);
		AppendResponse(IsJson() ? "\"" : "<string>");
		AppendResponse(xmlContent);
		AppendResponse(IsJson() ? "\"" : "</string>");
	}
	else
	{
		BuildErrorResponse(3, "Could not read url");
	}

	FileSystem::DeleteFile(tempFileName);
}

// string checkupdates()
void CheckUpdatesXmlCommand::Execute()
{
	CString updateInfo;
	bool ok = g_Maintenance->CheckUpdates(updateInfo);

	if (ok)
	{
		CString xmlContent = EncodeStr(updateInfo);
		AppendResponse(IsJson() ? "\"" : "<string>");
		AppendResponse(xmlContent);
		AppendResponse(IsJson() ? "\"" : "</string>");
	}
	else
	{
		BuildErrorResponse(3, "Could not read update info from update-info-script");
	}
}

// bool startupdate(string branch)
void StartUpdateXmlCommand::Execute()
{
	char* branchName;
	if (!NextParamAsStr(&branchName))
	{
		BuildErrorResponse(2, "Invalid parameter (Branch)");
		return;
	}
	DecodeStr(branchName);

	Maintenance::EBranch branch;
	if (!strcasecmp(branchName, "stable"))
	{
		branch = Maintenance::brStable;
	}
	else if (!strcasecmp(branchName, "testing"))
	{
		branch = Maintenance::brTesting;
	}
	else if (!strcasecmp(branchName, "devel"))
	{
		branch = Maintenance::brDevel;
	}
	else
	{
		BuildErrorResponse(2, "Invalid parameter (Branch)");
		return;
	}

	bool ok = g_Maintenance->StartUpdate(branch);

	BuildBoolResponse(ok);
}

// struct[] logupdate(idfrom, entries)
GuardedMessageList LogUpdateXmlCommand::GuardMessages()
{
	return g_Maintenance->GuardMessages();
}

// struct[] servervolumes(bool BytesPerSeconds, bool BytesPerMinutes, bool BytesPerHours, bool BytesPerDays)
void ServerVolumesXmlCommand::Execute()
{
	const char* XML_VOLUME_ITEM_START =
	"<value><struct>\n"
	"<member><name>ServerID</name><value><i4>%i</i4></value></member>\n"
	"<member><name>DataTime</name><value><i4>%i</i4></value></member>\n"
	"<member><name>FirstDay</name><value><i4>%i</i4></value></member>\n"
	"<member><name>TotalSizeLo</name><value><i4>%u</i4></value></member>\n"
	"<member><name>TotalSizeHi</name><value><i4>%u</i4></value></member>\n"
	"<member><name>TotalSizeMB</name><value><i4>%i</i4></value></member>\n"
	"<member><name>CustomSizeLo</name><value><i4>%u</i4></value></member>\n"
	"<member><name>CustomSizeHi</name><value><i4>%u</i4></value></member>\n"
	"<member><name>CustomSizeMB</name><value><i4>%i</i4></value></member>\n"
	"<member><name>CustomTime</name><value><i4>%i</i4></value></member>\n"
	"<member><name>SecSlot</name><value><i4>%i</i4></value></member>\n"
	"<member><name>MinSlot</name><value><i4>%i</i4></value></member>\n"
	"<member><name>HourSlot</name><value><i4>%i</i4></value></member>\n"
	"<member><name>DaySlot</name><value><i4>%i</i4></value></member>\n";

	const char* XML_BYTES_ARRAY_START =
	"<member><name>%s</name><value><array><data>\n";

	const char* XML_BYTES_ARRAY_ITEM =
	"<value><struct>\n"
	"<member><name>SizeLo</name><value><i4>%u</i4></value></member>\n"
	"<member><name>SizeHi</name><value><i4>%u</i4></value></member>\n"
	"<member><name>SizeMB</name><value><i4>%i</i4></value></member>\n"
	"</struct></value>\n";

	const char* XML_BYTES_ARRAY_END =
	"</data></array></value></member>\n";

	const char* XML_VOLUME_ITEM_END =
	"</struct></value>\n";

	const char* JSON_VOLUME_ITEM_START =
	"{\n"
	"\"ServerID\" : %i,\n"
	"\"DataTime\" : %i,\n"
	"\"FirstDay\" : %i,\n"
	"\"TotalSizeLo\" : %u,\n"
	"\"TotalSizeHi\" : %u,\n"
	"\"TotalSizeMB\" : %i,\n"
	"\"CustomSizeLo\" : %u,\n"
	"\"CustomSizeHi\" : %u,\n"
	"\"CustomSizeMB\" : %i,\n"
	"\"CustomTime\" : %i,\n"
	"\"SecSlot\" : %i,\n"
	"\"MinSlot\" : %i,\n"
	"\"HourSlot\" : %i,\n"
	"\"DaySlot\" : %i,\n";

	const char* JSON_BYTES_ARRAY_START =
	"\"%s\" : [\n";

	const char* JSON_BYTES_ARRAY_ITEM =
	"{\n"
	"\"SizeLo\" : %u,\n"
	"\"SizeHi\" : %u,\n"
	"\"SizeMB\" : %i\n"
	"}";

	const char* JSON_BYTES_ARRAY_END =
	"]\n";

	const char* JSON_VOLUME_ITEM_END =
	"}\n";

	AppendResponse(IsJson() ? "[\n" : "<array><data>\n");

	bool BytesPer[] = {true, true, true, true};
	NextParamAsBool(&BytesPer[0]) && NextParamAsBool(&BytesPer[1]) &&
		NextParamAsBool(&BytesPer[2]) && NextParamAsBool(&BytesPer[3]);

	int index = 0;

	for (ServerVolume& serverVolume : g_StatMeter->GuardServerVolumes())
	{
		uint32 totalSizeHi, totalSizeLo, totalSizeMB;
		Util::SplitInt64(serverVolume.GetTotalBytes(), &totalSizeHi, &totalSizeLo);
		totalSizeMB = (int)(serverVolume.GetTotalBytes() / 1024 / 1024);

		uint32 customSizeHi, customSizeLo, customSizeMB;
		Util::SplitInt64(serverVolume.GetCustomBytes(), &customSizeHi, &customSizeLo);
		customSizeMB = (int)(serverVolume.GetCustomBytes() / 1024 / 1024);

		AppendCondResponse(",\n", IsJson() && index > 0);
		AppendFmtResponse(IsJson() ? JSON_VOLUME_ITEM_START : XML_VOLUME_ITEM_START,
				 index, (int)serverVolume.GetDataTime(), serverVolume.GetFirstDay(),
				 totalSizeLo, totalSizeHi, totalSizeMB, customSizeLo, customSizeHi, customSizeMB,
				 (int)serverVolume.GetCustomTime(), serverVolume.GetSecSlot(),
				 serverVolume.GetMinSlot(), serverVolume.GetHourSlot(), serverVolume.GetDaySlot());

		ServerVolume::VolumeArray* VolumeArrays[] = { serverVolume.BytesPerSeconds(),
			serverVolume.BytesPerMinutes(), serverVolume.BytesPerHours(), serverVolume.BytesPerDays() };
		const char* VolumeNames[] = { "BytesPerSeconds", "BytesPerMinutes", "BytesPerHours", "BytesPerDays" };

		for (int i=0; i<4; i++)
		{
			if (BytesPer[i])
			{
				ServerVolume::VolumeArray* volumeArray = VolumeArrays[i];
				const char* arrayName = VolumeNames[i];

				AppendFmtResponse(IsJson() ? JSON_BYTES_ARRAY_START : XML_BYTES_ARRAY_START, arrayName);

				int index2 = 0;
				for (int64 bytes : *volumeArray)
				{
					uint32 sizeHi, sizeLo, sizeMB;
					Util::SplitInt64(bytes, &sizeHi, &sizeLo);
					sizeMB = (int)(bytes / 1024 / 1024);

					AppendCondResponse(",\n", IsJson() && index2++ > 0);
					AppendFmtResponse(IsJson() ? JSON_BYTES_ARRAY_ITEM : XML_BYTES_ARRAY_ITEM,
							 sizeLo, sizeHi, sizeMB);
				}

				AppendResponse(IsJson() ? JSON_BYTES_ARRAY_END : XML_BYTES_ARRAY_END);
				AppendCondResponse(",\n", IsJson() && i < 3);
			}
		}
		AppendResponse(IsJson() ? JSON_VOLUME_ITEM_END : XML_VOLUME_ITEM_END);
		index++;
	}

	AppendResponse(IsJson() ? "\n]" : "</data></array>\n");
}

// bool resetservervolume(int serverid, string counter);
void ResetServerVolumeXmlCommand::Execute()
{
	int serverId;
	char* counter;
	if (!NextParamAsInt(&serverId) || !NextParamAsStr(&counter))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	if (strcmp(counter, "CUSTOM"))
	{
		BuildErrorResponse(3, "Invalid Counter");
		return;
	}

	bool ok = false;
	int index = 0;
	for (ServerVolume& serverVolume : g_StatMeter->GuardServerVolumes())
	{
		if (index == serverId || serverId == -1)
		{
			serverVolume.ResetCustom();
			ok = true;
		}
		index++;
	}

	BuildBoolResponse(ok);
}

// struct[] loadlog(nzbid, logidfrom, logentries)
void LoadLogXmlCommand::Execute()
{
	m_nzbInfo = nullptr;
	m_nzbId = 0;
	if (!NextParamAsInt(&m_nzbId))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	LogXmlCommand::Execute();
	m_downloadQueue.reset();
}

GuardedMessageList LoadLogXmlCommand::GuardMessages()
{
	// TODO: optimize for m_iIDFrom and m_iNrEntries
	g_DiskState->LoadNzbMessages(m_nzbId, &m_messages);

	if (m_messages.empty())
	{
		// When we returing cached messages from m_nzbInfo we have to make sure that
		// m_nzbInfo isn't destroyed during we are working with CachedMessages.
		// For that purpose we set a lock on DownloadQueue,
		// which will be released later in LoadLogXmlCommand::Execute().

		m_downloadQueue = std::make_unique<GuardedDownloadQueue>(DownloadQueue::Guard());
		m_nzbInfo = (*m_downloadQueue)->GetQueue()->Find(m_nzbId);
		if (m_nzbInfo)
		{
			return m_nzbInfo->GuardCachedMessages();
		}
		else
		{
			// No nzb-object available, the lock on DownloadQueue can be released right now.
			m_downloadQueue.reset();
		}
	}

	return GuardedMessageList(&m_messages, nullptr);
}

// string testserver(string host, int port, string username, string password, bool encryption, string cipher, int timeout);
void TestServerXmlCommand::Execute()
{
	const char* XML_RESPONSE_STR_BODY = "<string>%s</string>";
	const char* JSON_RESPONSE_STR_BODY = "\"%s\"";

	char* host;
	int port;
	char* username;
	char* password;
	bool encryption;
	char* cipher;
	int timeout;
	int certVerifLevel;

	if (!NextParamAsStr(&host) || !NextParamAsInt(&port) || !NextParamAsStr(&username) ||
		!NextParamAsStr(&password) || !NextParamAsBool(&encryption) ||
		!NextParamAsStr(&cipher) || !NextParamAsInt(&timeout) ||
		!NextParamAsInt(&certVerifLevel))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	if (certVerifLevel < 0 || certVerifLevel >= Options::ECertVerifLevel::Count)
	{
		BuildErrorResponse(2, "Invalid parameter (Certificate Verification Level).");
		return;
	}

	NewsServer server(0, true, "test server", host, port, 0, username, password, false, 
					encryption, cipher, 1, 0, 0, 0, false, certVerifLevel);
	TestConnection connection(&server, this);
	connection.SetTimeout(timeout == 0 ? g_Options->GetArticleTimeout() : timeout);
	connection.SetSuppressErrors(false);

	bool ok = connection.Connect();
	if (ok)
	{
		// generate a unique non-existent message-id since we don't want a real article to be returned
		BString<1024> id;
		while (id.Length() < 30)
		{
			id.AppendFmt("%i", rand());
		}
		const char* response = connection.Request(BString<1024>("ARTICLE <%s@nzbget.com>\r\n", *id));
		ok = response && (*response == '4' || *response == '2');
	}

	BString<1024> content(IsJson() ? JSON_RESPONSE_STR_BODY : XML_RESPONSE_STR_BODY,
		ok ? "" : m_errText.Empty() ? "Unknown error" : *m_errText);

	AppendResponse(content);
}

void TestServerXmlCommand::PrintError(const char* errMsg)
{
	if (m_errText.Empty())
	{
		m_errText = EncodeStr(errMsg);
	}
}

void TestServerSpeedXmlCommand::Execute()
{
	char* nzbFileUrl;
	if (!NextParamAsStr(&nzbFileUrl))
	{
		BuildErrorResponse(2, "Invalid parameter: NZB file url");
		return;
	}

	int serverId;
	if (!NextParamAsInt(&serverId))
	{
		BuildErrorResponse(2, "Invalid parameter: Server ID");
		return;
	}

	std::unique_ptr<NzbInfo> nzbInfo = std::make_unique<NzbInfo>();
	nzbInfo->SetKind(NzbInfo::nkUrl);
	nzbInfo->SetUrl(nzbFileUrl);
	nzbInfo->SetFilename(nzbFileUrl);
	nzbInfo->SetPriority(NzbInfo::FORCE_PRIORITY);
	nzbInfo->SetExtraPriority(NzbInfo::FORCE_PRIORITY);
	nzbInfo->SetAddUrlPaused(false);
	nzbInfo->SetDupeMode(EDupeMode::dmForce);
	nzbInfo->SetDesiredServerId(serverId);
	nzbInfo->SetSkipScriptProcessing(true);
	nzbInfo->SetSkipDiskWrite(true);

	g_UrlCoordinator->AddUrlToQueue(std::move(nzbInfo), true);

	BuildBoolResponse(true);
}



void TestDiskSpeedXmlCommand::Execute()
{
	char* dirPath;
	int writeBufferKiB;
	int maxFileSizeGiB;
	int timeoutSec;

	if (!NextParamAsStr(&dirPath))
	{
		BuildErrorResponse(2, "Invalid argument (Path)");
		return;
	}

	if (!NextParamAsInt(&writeBufferKiB))
	{
		BuildErrorResponse(2, "Invalid argument (Write Buffer)");
		return;
	}

	if (writeBufferKiB < 0)
	{
		BuildErrorResponse(2, "Write Buffer cannot be negative");
		return;
	}

	if (!NextParamAsInt(&maxFileSizeGiB))
	{
		BuildErrorResponse(2, "Invalid argument (Max file size)");
		return;
	}

	if (maxFileSizeGiB < 1)
	{
		BuildErrorResponse(2, "Max file size must be positive");
		return;
	}

	if (!NextParamAsInt(&timeoutSec))
	{
		BuildErrorResponse(2, "Invalid argument (Timeout)");
		return;
	}

	if (timeoutSec < 1)
	{
		BuildErrorResponse(2, "Timeout must be positive");
		return;
	}

	try
	{
		size_t bufferSizeBytes = writeBufferKiB * 1024;
		uint64_t maxFileSizeBytes = maxFileSizeGiB * 1024ull * 1024ull * 1024ull;
		 
		Benchmark::DiskBenchmark db;
		auto [size, time] = db.Run(
			dirPath, 
			bufferSizeBytes, 
			maxFileSizeBytes, 
			std::chrono::seconds(timeoutSec)
		);

		std::string jsonStr = IsJson() ?
			ToJsonStr(size, time) :
			ToXmlStr(size, time);

		AppendResponse(jsonStr.c_str());
	}
	catch (const std::exception& e)
	{
		BuildErrorResponse(2, e.what());
	}
}

void TestNetworkSpeedXmlCommand::Execute()
{
	try
	{
		g_WorkState->SetPauseDownload(true);

		Network::SpeedTest sp;
		double speedMbps = sp.RunTest();
		std::string respStr = IsJson() ?
			ToJsonStr(speedMbps) :
			ToXmlStr(speedMbps);

		AppendResponse(respStr.c_str());

		g_WorkState->SetPauseDownload(false);
	}
	catch (const std::exception& e)
	{
		BuildErrorResponse(2, e.what());
		g_WorkState->SetPauseDownload(false);
	}
}

// bool startscript(string script, string command, string context, struct[] options);
void StartScriptXmlCommand::Execute()
{
	char* script;
	char* command;
	char* context;
	if (!NextParamAsStr(&script) || !NextParamAsStr(&command) || !NextParamAsStr(&context))
	{
		BuildErrorResponse(2, "Invalid parameter");
		return;
	}

	std::unique_ptr<Options::OptEntries> optEntries = std::make_unique<Options::OptEntries>();

	char* name;
	char* value;
	char* dummy;
	while ((IsJson() && NextParamAsStr(&dummy) && NextParamAsStr(&name) &&
		NextParamAsStr(&dummy) && NextParamAsStr(&value)) ||
		(!IsJson() && NextParamAsStr(&name) && NextParamAsStr(&value)))
	{
		DecodeStr(name);
		DecodeStr(value);
		optEntries->emplace_back(name, value);
	}

	bool ok = CommandScriptController::StartScript(script, command, std::move(optEntries));

	BuildBoolResponse(ok);
}

// struct[] logscript(idfrom, entries)
GuardedMessageList LogScriptXmlCommand::GuardMessages()
{
	return g_CommandScriptLog->GuardMessages();
}
