/** 
 * @file llwebsharing.cpp
 * @author Aimee
 * @brief Web Snapshot Sharing
 *
 * $LicenseInfo:firstyear=2010&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "llwebsharing.h"

#include "llagentui.h"
#include "llbufferstream.h"
#include "llhttpclient.h"
#include "llhttpstatuscodes.h"
#include "llsdserialize.h"
#include "llsdutil.h"
#include "llurl.h"
#include "llviewercontrol.h"

#include <boost/regex.hpp>
#include <boost/algorithm/string/replace.hpp>



///////////////////////////////////////////////////////////////////////////////
//
class LLWebSharingConfigResponder : public LLHTTPClient::Responder
{
	LOG_CLASS(LLWebSharingConfigResponder);
public:
	/// Overrides the default LLSD parsing behaviour, to allow parsing a JSON response.
	virtual void completedRaw(U32 status, const std::string& reason,
							  const LLChannelDescriptors& channels,
							  const LLIOPipe::buffer_ptr_t& buffer)
	{
		LLSD content;
		LLBufferStream istr(channels, buffer.get());
		LLPointer<LLSDParser> parser = new LLSDNotationParser();

		if (parser->parse(istr, content, LLSDSerialize::SIZE_UNLIMITED) == LLSDParser::PARSE_FAILURE)
		{
			LL_WARNS("WebSharing") << "Failed to deserialize LLSD from JSON response. " << " [" << status << "]: " << reason << LL_ENDL;
		}
		else
		{
			completed(status, reason, content);
		}
	}

	virtual void error(U32 status, const std::string& reason)
	{
		LL_WARNS("WebSharing") << "Error [" << status << "]: " << reason << LL_ENDL;
	}

	virtual void result(const LLSD& content)
	{
		LLWebSharing::instance().receiveConfig(content);
	}
};



///////////////////////////////////////////////////////////////////////////////
//
class LLWebSharingOpenIDAuthResponder : public LLHTTPClient::Responder
{
	LOG_CLASS(LLWebSharingOpenIDAuthResponder);
public:
	/* virtual */ void completedHeader(U32 status, const std::string& reason, const LLSD& content)
	{
		completed(status, reason, content);
	}

	/* virtual */ void completedRaw(U32 status, const std::string& reason,
									const LLChannelDescriptors& channels,
									const LLIOPipe::buffer_ptr_t& buffer)
	{
		/// Left empty to override the default LLSD parsing behaviour.
	}

	virtual void error(U32 status, const std::string& reason)
	{
		if (HTTP_UNAUTHORIZED == status)
		{
			LL_WARNS("WebSharing") << "AU account not authenticated." << LL_ENDL;
			// *TODO: No account found on AU, so start the account creation process here.
		}
		else
		{
			LL_WARNS("WebSharing") << "Error [" << status << "]: " << reason << LL_ENDL;
			LLWebSharing::instance().retryOpenIDAuth();
		}

	}

	virtual void result(const LLSD& content)
	{
		if (content.has("set-cookie"))
		{
			// OpenID request succeeded and returned a session cookie.
			LLWebSharing::instance().receiveSessionCookie(content["set-cookie"].asString());
		}
	}
};



///////////////////////////////////////////////////////////////////////////////
//
class LLWebSharingSecurityTokenResponder : public LLHTTPClient::Responder
{
	LOG_CLASS(LLWebSharingSecurityTokenResponder);
public:
	/// Overrides the default LLSD parsing behaviour, to allow parsing a JSON response.
	virtual void completedRaw(U32 status, const std::string& reason,
							  const LLChannelDescriptors& channels,
							  const LLIOPipe::buffer_ptr_t& buffer)
	{
		LLSD content;
		LLBufferStream istr(channels, buffer.get());
		LLPointer<LLSDParser> parser = new LLSDNotationParser();

		if (parser->parse(istr, content, LLSDSerialize::SIZE_UNLIMITED) == LLSDParser::PARSE_FAILURE)
		{
			LL_WARNS("WebSharing") << "Failed to deserialize LLSD from JSON response. " << " [" << status << "]: " << reason << LL_ENDL;
			LLWebSharing::instance().retryOpenIDAuth();
		}
		else
		{
			completed(status, reason, content);
		}
	}

	virtual void error(U32 status, const std::string& reason)
	{
		LL_WARNS("WebSharing") << "Error [" << status << "]: " << reason << LL_ENDL;
		LLWebSharing::instance().retryOpenIDAuth();
	}

	virtual void result(const LLSD& content)
	{
		if (content[0].has("st") && content[0].has("expires"))
		{
			const std::string& token   = content[0]["st"].asString();
			const std::string& expires = content[0]["expires"].asString();
			if (LLWebSharing::instance().receiveSecurityToken(token, expires))
			{
				// Sucessfully received a valid security token.
				return;
			}
		}
		else
		{
			LL_WARNS("WebSharing") << "No security token received." << LL_ENDL;
		}

		LLWebSharing::instance().retryOpenIDAuth();
	}
};



///////////////////////////////////////////////////////////////////////////////
//
class LLWebSharingUploadResponder : public LLHTTPClient::Responder
{
	LOG_CLASS(LLWebSharingUploadResponder);
public:
	/// Overrides the default LLSD parsing behaviour, to allow parsing a JSON response.
	virtual void completedRaw(U32 status, const std::string& reason,
							  const LLChannelDescriptors& channels,
							  const LLIOPipe::buffer_ptr_t& buffer)
	{
/*
		 // Dump the body, for debugging.

		 LLBufferStream istr1(channels, buffer.get());
		 std::ostringstream ostr;
		 std::string body;

		 while (istr1.good())
		 {
			char buf[1024];
			istr1.read(buf, sizeof(buf));
			body.append(buf, istr1.gcount());
		 }
		 LL_DEBUGS("WebSharing") << body << LL_ENDL;
*/
		LLSD content;
		LLBufferStream istr(channels, buffer.get());
		LLPointer<LLSDParser> parser = new LLSDNotationParser();

		if (parser->parse(istr, content, LLSDSerialize::SIZE_UNLIMITED) == LLSDParser::PARSE_FAILURE)
		{
			LL_WARNS("WebSharing") << "Failed to deserialize LLSD from JSON response. " << " [" << status << "]: " << reason << LL_ENDL;
		}
		else
		{
			completed(status, reason, content);
		}
	}

	virtual void error(U32 status, const std::string& reason)
	{
		LL_WARNS("WebSharing") << "Error [" << status << "]: " << reason << LL_ENDL;
	}

	virtual void result(const LLSD& content)
	{
		if (content[0].has("result") && content[0].has("id") &&
			content[0]["id"].asString() == "newMediaItem")
		{
			// *TODO: Upload successful, continue from here to post metadata and create AU activity.
		}
		else
		{
			LL_WARNS("WebSharing") << "Error [" << content[0]["code"].asString()
								   << "]: " << content[0]["message"].asString() << LL_ENDL;
		}
	}
};



///////////////////////////////////////////////////////////////////////////////
//
LLWebSharing::LLWebSharing()
:	mConfig(),
	mSecurityToken(LLSD::emptyMap()),
	mEnabled(false),
	mRetries(0),
	mImage(NULL),
	mMetadata(LLSD::emptyMap())
{
}

void LLWebSharing::init()
{
	if (!mEnabled)
	{
		sendConfigRequest();
	}
}

bool LLWebSharing::shareSnapshot(LLImageJPEG* snapshot, LLSD& metadata)
{
	LL_INFOS("WebSharing") << metadata << LL_ENDL;

	if (mImage)
	{
		// *TODO: Handle this possibility properly, queue them up?
		LL_WARNS("WebSharing") << "Snapshot upload already in progress." << LL_ENDL;
		return false;
	}

	mImage = snapshot;
	mMetadata = metadata;

	// *TODO: Check whether we have a valid security token already and re-use it.
	sendOpenIDAuthRequest();
	return true;
}

bool LLWebSharing::setOpenIDCookie(const std::string& cookie)
{
	LL_DEBUGS("WebSharing") << "Setting OpenID cookie " << cookie << LL_ENDL;
	mOpenIDCookie = cookie;
	return validateConfig();
}

bool LLWebSharing::receiveConfig(const LLSD& config)
{
	LL_DEBUGS("WebSharing") << "Received config data: " << config << LL_ENDL;
	mConfig = config;
	return validateConfig();
}

bool LLWebSharing::receiveSessionCookie(const std::string& cookie)
{
	LL_DEBUGS("WebSharing") << "Received AU session cookie: " << cookie << LL_ENDL;
	mSessionCookie = cookie;

	// Fetch a security token using the new session cookie.
	LLWebSharing::instance().sendSecurityTokenRequest();

	return (!mSessionCookie.empty());
}

bool LLWebSharing::receiveSecurityToken(const std::string& token, const std::string& expires)
{
	mSecurityToken["st"] = token;
	mSecurityToken["expires"] = LLDate(expires);

	if (!securityTokenIsValid(mSecurityToken))
	{
		LL_WARNS("WebSharing") << "Invalid security token received: \"" << token << "\" Expires: " << expires << LL_ENDL;
		return false;
	}

	LL_DEBUGS("WebSharing") << "Received security token: \"" << token << "\" Expires: " << expires << LL_ENDL;
	mRetries = 0;

	// Continue the upload process now that we have a security token.
	sendUploadRequest();

	return true;
}

void LLWebSharing::sendConfigRequest()
{
	std::string config_url = gSavedSettings.getString("SnapshotConfigURL");
	LL_DEBUGS("WebSharing") << "Requesting Snapshot Sharing config data from: " << config_url << LL_ENDL;

	LLSD headers = LLSD::emptyMap();
	headers["Accept"] = "application/json";

	LLHTTPClient::get(config_url, new LLWebSharingConfigResponder(), headers);
}

void LLWebSharing::sendOpenIDAuthRequest()
{
	std::string auth_url = mConfig["openIdAuthUrl"];
	LL_DEBUGS("WebSharing") << "Starting OpenID Auth: " << auth_url << LL_ENDL;

	LLSD headers = LLSD::emptyMap();
	headers["Cookie"] = mOpenIDCookie;
	headers["Accept"] = "*/*";

	// Send request, successful login will trigger fetching a security token.
	LLHTTPClient::get(auth_url, new LLWebSharingOpenIDAuthResponder(), headers);
}

bool LLWebSharing::retryOpenIDAuth()
{
	if (mRetries++ >= MAX_AUTH_RETRIES)
	{
		LL_WARNS("WebSharing") << "Exceeded maximum number of authorization attempts, aborting." << LL_ENDL;
		mRetries = 0;
		return false;
	}

	LL_WARNS("WebSharing") << "Authorization failed, retrying (" << mRetries << "/" << MAX_AUTH_RETRIES << ")" << LL_ENDL;
	sendOpenIDAuthRequest();
	return true;
}

void LLWebSharing::sendSecurityTokenRequest()
{
	std::string token_url = mConfig["securityTokenUrl"];
	LL_DEBUGS("WebSharing") << "Fetching security token from: " << token_url << LL_ENDL;

	LLSD headers = LLSD::emptyMap();
	headers["Cookie"] = mSessionCookie;

	headers["Accept"] = "application/json";
	headers["Content-Type"] = "application/json";

	std::ostringstream body;
	body << "{ \"gadgets\": [{ \"url\":\""
		 << mConfig["gadgetSpecUrl"].asString()
		 << "\" }] }";

	// postRaw() takes ownership of the buffer and releases it later.
	size_t size = body.str().size();
	U8 *data = new U8[size];
	memcpy(data, body.str().data(), size);

	// Send request, receiving a valid token will trigger snapshot upload.
	LLHTTPClient::postRaw(token_url, data, size, new LLWebSharingSecurityTokenResponder(), headers);
}

void LLWebSharing::sendUploadRequest()
{
	LLUriTemplate upload_template(mConfig["openSocialRpcUrlTemplate"].asString());
	std::string upload_url(upload_template.buildURI(mSecurityToken));

	LL_DEBUGS("WebSharing") << "Posting upload to: " << upload_url << LL_ENDL;

	static const std::string BOUNDARY("------------abcdef012345xyZ");

	LLSD headers = LLSD::emptyMap();
	headers["Cookie"] = mSessionCookie;

	headers["Accept"] = "application/json";
	headers["Content-Type"] = "multipart/form-data; boundary=" + BOUNDARY;

	std::ostringstream body;
	body << "--" << BOUNDARY << "\r\n"
		 << "Content-Disposition: form-data; name=\"request\"\r\n\r\n"
		 << "[{"
		 <<	  "\"method\":\"mediaItems.create\","
		 <<	  "\"params\": {"
		 <<	    "\"userId\":[\"@me\"],"
		 <<	    "\"groupId\":\"@self\","
		 <<	    "\"mediaItem\": {"
		 <<	      "\"mimeType\":\"image/jpeg\","
		 <<	      "\"type\":\"image\","
		 <<       "\"url\":\"@field:image1\""
		 <<	    "}"
		 <<	  "},"
		 <<	  "\"id\":\"newMediaItem\""
		 <<	"}]"
		 <<	"--" << BOUNDARY << "\r\n"
		 <<	"Content-Disposition: form-data; name=\"image1\"\r\n\r\n";

	// Insert the image data.
	// *FIX: Treating this as a string will probably screw it up ...
	U8* image_data = mImage->getData();
	for (S32 i = 0; i < mImage->getDataSize(); ++i)
	{
		body << image_data[i];
	}

	body <<	"\r\n--" << BOUNDARY << "--\r\n";

	// postRaw() takes ownership of the buffer and releases it later.
	size_t size = body.str().size();
	U8 *data = new U8[size];
	memcpy(data, body.str().data(), size);

	// Send request, successful upload will trigger posting metadata.
	LLHTTPClient::postRaw(upload_url, data, size, new LLWebSharingUploadResponder(), headers);
}

bool LLWebSharing::validateConfig()
{
	// Check the OpenID Cookie has been set.
	if (mOpenIDCookie.empty())
	{
		mEnabled = false;
		return mEnabled;
	}

	if (!mConfig.isMap())
	{
		mEnabled = false;
		return mEnabled;
	}

	// Template to match the received config against.
	LLSD required(LLSD::emptyMap());
	required["gadgetSpecUrl"] = "";
	required["loginTokenUrl"] = "";
	required["openIdAuthUrl"] = "";
	required["photoPageUrlTemplate"] = "";
	required["openSocialRpcUrlTemplate"] = "";
	required["securityTokenUrl"] = "";
	required["tokenBasedLoginUrlTemplate"] = "";
	required["viewerIdUrl"] = "";

	std::string mismatch(llsd_matches(required, mConfig));
	if (!mismatch.empty())
	{
		LL_WARNS("WebSharing") << "Malformed config data response: " << mismatch << LL_ENDL;
		mEnabled = false;
		return mEnabled;
	}

	mEnabled = true;
	return mEnabled;
}

// static
bool LLWebSharing::securityTokenIsValid(LLSD& token)
{
	return (token.has("st") &&
			token.has("expires") &&
			(token["st"].asString() != "") &&
			(token["expires"].asDate() > LLDate::now()));
}



///////////////////////////////////////////////////////////////////////////////
//
LLUriTemplate::LLUriTemplate(const std::string& uri_template)
	:
	mTemplate(uri_template)
{
}

std::string LLUriTemplate::buildURI(const LLSD& vars)
{
	// *TODO: Separate parsing the template from building the URI.
	// Parsing only needs to happen on construction/assignnment.

	static const std::string VAR_NAME_REGEX("[[:alpha:]][[:alnum:]\\._-]*");
	// Capture var name with and without surrounding {}
	static const std::string VAR_REGEX("\\{(" + VAR_NAME_REGEX + ")\\}");
	// Capture delimiter and comma separated list of var names.
	static const std::string JOIN_REGEX("\\{-join\\|(&)\\|(" + VAR_NAME_REGEX + "(?:," + VAR_NAME_REGEX + ")*)\\}");

	std::string uri = mTemplate;
	boost::smatch results;

	// Validate and expand join operators : {-join|&|var1,var2,...}

	boost::regex join_regex(JOIN_REGEX);

	while (boost::regex_search(uri, results, join_regex))
	{
		// Extract the list of var names from the results.
		std::string delim = results[1].str();
		std::string var_list = results[2].str();

		// Expand the list of vars into a query string with their values
		std::string query = expandJoin(delim, var_list, vars);

		// Substitute the query string into the template.
		uri = boost::regex_replace(uri, join_regex, query, boost::format_first_only);
	}

	// Expand vars : {var1}

	boost::regex var_regex(VAR_REGEX);

	std::set<std::string> var_names;
	std::string::const_iterator start = uri.begin();
	std::string::const_iterator end = uri.end();

	// Extract the var names used.
	while (boost::regex_search(start, end, results, var_regex))
	{
		var_names.insert(results[1].str());
		start = results[0].second;
	}

	// Replace each var with its value.
	for (std::set<std::string>::const_iterator it = var_names.begin(); it != var_names.end(); ++it)
	{
		std::string var = *it;
		if (vars.has(var))
		{
			boost::replace_all(uri, "{" + var + "}", vars[var].asString());
		}
	}

	return uri;
}

std::string LLUriTemplate::expandJoin(const std::string& delim, const std::string& var_list, const LLSD& vars)
{
	std::ostringstream query;

	typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
	boost::char_separator<char> sep(",");
	tokenizer var_names(var_list, sep);
	tokenizer::const_iterator it = var_names.begin();

	// First var does not need a delimiter
	if (it != var_names.end())
	{
		const std::string& name = *it;
		if (vars.has(name))
		{
			// URL encode the value before appending the name=value pair.
			query << name << "=" << escapeURL(vars[name].asString());
		}
	}

	for (++it; it != var_names.end(); ++it)
	{
		const std::string& name = *it;
		if (vars.has(name))
		{
			// URL encode the value before appending the name=value pair.
			query << delim << name << "=" << escapeURL(vars[name].asString());
		}
	}

	return query.str();
}

// static
std::string LLUriTemplate::escapeURL(const std::string& unescaped)
{
	char* escaped = curl_escape(unescaped.c_str(), unescaped.size());
	std::string result = escaped;
	curl_free(escaped);
	return result;
}