phoenix-firestorm/indra/newview/lllogininstance.cpp

723 lines
27 KiB
C++

/**
* @file lllogininstance.cpp
* @brief Viewer's host for a login connection.
*
* $LicenseInfo:firstyear=2009&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 "lllogininstance.h"
// llcommon
#include "llevents.h"
#include "stringize.h"
#include "llsdserialize.h"
// login
#include "lllogin.h"
// newview
#include "llhasheduniqueid.h"
#include "llviewernetwork.h"
#include "llviewercontrol.h"
#include "llversioninfo.h"
#include "llslurl.h"
#include "llstartup.h"
#include "llfloaterreg.h"
#include "llnotifications.h"
#include "llnotificationsutil.h"
#include "llwindow.h"
#include "llviewerwindow.h"
#include "llprogressview.h"
#include "llsecapi.h"
#include "llstartup.h"
#include "llmachineid.h"
#include "llevents.h"
#include "llappviewer.h"
#include "llsdserialize.h"
#include "lltrans.h"
#include <boost/regex.hpp>
#include <sstream>
const S32 LOGIN_MAX_RETRIES = 0; // Viewer should not autmatically retry login
const F32 LOGIN_SRV_TIMEOUT_MIN = 10.f;
const F32 LOGIN_SRV_TIMEOUT_MAX = 180.f;
const F32 LOGIN_DNS_TIMEOUT_FACTOR = 0.9f; // make DNS wait shorter then retry time
class LLLoginInstance::Disposable {
public:
virtual ~Disposable() {}
};
static const char * const TOS_REPLY_PUMP = "lllogininstance_tos_callback";
static const char * const TOS_LISTENER_NAME = "lllogininstance_tos";
std::string construct_start_string();
// LLLoginInstance
//-----------------------------------------------------------------------------
LLLoginInstance::LLLoginInstance() :
mLoginModule(new LLLogin()),
mNotifications(NULL),
mLoginState("offline"),
mSaveMFA(true),
mAttemptComplete(false),
mTransferRate(0.0f),
mDispatcher("LLLoginInstance", "change")
{
mLoginModule->getEventPump().listen("lllogininstance",
boost::bind(&LLLoginInstance::handleLoginEvent, this, _1));
// This internal use of LLEventDispatcher doesn't really need
// per-function descriptions.
mDispatcher.add("fail.login", "", boost::bind(&LLLoginInstance::handleLoginFailure, this, _1));
mDispatcher.add("connect", "", boost::bind(&LLLoginInstance::handleLoginSuccess, this, _1));
mDispatcher.add("disconnect", "", boost::bind(&LLLoginInstance::handleDisconnect, this, _1));
mDispatcher.add("indeterminate", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1));
}
void LLLoginInstance::setPlatformInfo(const std::string platform,
const std::string platform_version,
const std::string platform_name)
{
mPlatform = platform;
mPlatformVersion = platform_version;
mPlatformVersionName = platform_name;
}
LLLoginInstance::~LLLoginInstance()
{
}
void LLLoginInstance::connect(LLPointer<LLCredential> credentials)
{
std::vector<std::string> uris;
LLGridManager::getInstance()->getLoginURIs(uris);
if (uris.size() < 1)
{
LL_WARNS() << "Failed to get login URIs during connect. No connect for you!" << LL_ENDL;
return;
}
connect(uris.front(), credentials);
}
void LLLoginInstance::connect(const std::string& uri, LLPointer<LLCredential> credentials)
{
mAttemptComplete = false; // Reset attempt complete at this point!
constructAuthParams(credentials);
mLoginModule->connect(uri, mRequestData);
}
void LLLoginInstance::reconnect()
{
// Sort of like connect, only using the pre-existing
// request params.
std::vector<std::string> uris;
LLGridManager::getInstance()->getLoginURIs(uris);
mLoginModule->connect(uris.front(), mRequestData);
gViewerWindow->setShowProgress(true,!gSavedSettings.getBOOL("FSDisableLoginScreens"));
}
void LLLoginInstance::disconnect()
{
mAttemptComplete = false; // Reset attempt complete at this point!
mRequestData.clear();
mLoginModule->disconnect();
}
LLSD LLLoginInstance::getResponse()
{
return mResponseData;
}
void LLLoginInstance::constructAuthParams(LLPointer<LLCredential> user_credential)
{
// Set up auth request options.
//#define LL_MINIMIAL_REQUESTED_OPTIONS
LLSD requested_options;
// *Note: this is where gUserAuth used to be created.
requested_options.append("inventory-root");
requested_options.append("inventory-skeleton");
//requested_options.append("inventory-meat");
//requested_options.append("inventory-skel-targets");
#if (!defined LL_MINIMIAL_REQUESTED_OPTIONS)
// Not requesting library will trigger mFatalNoLibraryRootFolder
requested_options.append("inventory-lib-root");
requested_options.append("inventory-lib-owner");
requested_options.append("inventory-skel-lib");
// requested_options.append("inventory-meat-lib");
requested_options.append("initial-outfit");
requested_options.append("gestures");
requested_options.append("display_names");
requested_options.append("event_categories");
requested_options.append("event_notifications");
requested_options.append("classified_categories");
requested_options.append("adult_compliant");
requested_options.append("buddy-list");
requested_options.append("newuser-config");
requested_options.append("ui-config");
//send this info to login.cgi for stats gathering
//since viewerstats isn't reliable enough
requested_options.append("advanced-mode");
#endif
requested_options.append("max-agent-groups");
requested_options.append("map-server-url");
requested_options.append("voice-config");
requested_options.append("tutorial_setting");
requested_options.append("login-flags");
requested_options.append("global-textures");
if(gSavedSettings.getBOOL("ConnectAsGod"))
{
gSavedSettings.setBOOL("UseDebugMenus", true);
requested_options.append("god-connect");
}
// <FS:AW various patches>
#ifdef OPENSIM // <FS:AW optional opensim support>
//TODO: make this more flexible
if (LLGridManager::getInstance()->isInOpenSim())
{
requested_options.append("currency");// <FS:AW opensim currency support>
requested_options.append("max_groups");// <FS:AW opensim max groups support>
requested_options.append("search"); // <FS:AW opensim search support>
requested_options.append("destination_guide_url");// <FS:AW opensim destinations and avatar picker>
requested_options.append("avatar_picker_url");// <FS:AW opensim destinations and avatar picker>
//not in this patch
//requested_options.append("profile-server-url");
}
#endif // OPENSIM // <FS:AW optional opensim support>
// </FS:AW various patches>
LLSD request_params;
unsigned char hashed_unique_id_string[MD5HEX_STR_SIZE];
if ( ! llHashedUniqueID(hashed_unique_id_string) )
{
LL_WARNS("LLLogin") << "Not providing a unique id in request params" << LL_ENDL;
}
request_params["start"] = construct_start_string();
request_params["agree_to_tos"] = false; // Always false here. Set true in
request_params["read_critical"] = false; // handleTOSResponse
request_params["last_exec_event"] = mLastExecEvent;
request_params["last_exec_duration"] = mLastExecDuration;
request_params["last_exec_session_id"] = mLastAgentSessionId.asString();
request_params["mac"] = (char*)hashed_unique_id_string;
request_params["version"] = LLVersionInfo::instance().getVersion();
request_params["channel"] = LLVersionInfo::instance().getChannel();
request_params["platform"] = mPlatform;
request_params["address_size"] = ADDRESS_SIZE;
request_params["platform_version"] = mPlatformVersion;
request_params["platform_string"] = mPlatformVersionName;
request_params["id0"] = mSerialNumber;
request_params["host_id"] = gSavedSettings.getString("HostID");
request_params["extended_errors"] = true; // request message_id and message_args
request_params["token"] = "";
// log request_params _before_ adding the credentials or sensitive MFA hash data
LL_DEBUGS("LLLogin") << "Login parameters: " << LLSDOStreamer<LLSDNotationFormatter>(request_params) << LL_ENDL;
// Copy the credentials into the request after logging the rest
LLSD credentials(user_credential->getLoginParams());
for (LLSD::map_const_iterator it = credentials.beginMap();
it != credentials.endMap();
it++
)
{
request_params[it->first] = it->second;
}
std::string mfa_hash = gSavedSettings.getString("MFAHash"); //non-persistent to enable testing
// <FS:Beq> add some debug output for mfa_hash
if (mfa_hash.empty())
{
LL_DEBUGS("MFA") << "MFA hash from settings is empty (expected)" << LL_ENDL;
}
else
{
LL_DEBUGS("MFA") << "MFA hash from settings is set to: " << mfa_hash << LL_ENDL;
}
// </FS:Beq>
std::string grid(LLGridManager::getInstance()->getGridId());
std::string user_id = user_credential->userID();
if (gSecAPIHandler)
{
if (mfa_hash.empty())
{
// normal execution, mfa_hash was not set from debug setting so load from protected store
LLSD data_map = gSecAPIHandler->getProtectedData("mfa_hash", grid);
if (data_map.isMap() && data_map.has(user_id))
{
mfa_hash = data_map[user_id].asString();
}
// <FS:Beq> add some debug output for mfa_hash
if (mfa_hash.empty())
{
LL_DEBUGS("MFA") << "MFA hash from secuire store is empty" << LL_ENDL;
}
else
{
LL_DEBUGS("MFA") << "MFA hash is from secure store set to: " << mfa_hash << LL_ENDL;
}
// </FS:Beq>
}
else
{
LL_DEBUGS("MFA") << "MFA hash written to protected map" << LL_ENDL; // <FS:Beq/> add some debug output for mfa_hash
// SL-16888 the mfa_hash is being overridden for testing so save it for consistency for future login requests
gSecAPIHandler->addToProtectedMap("mfa_hash", grid, user_id, mfa_hash);
}
}
else
{
LL_WARNS() << "unable to access protected store for mfa_hash" << LL_ENDL;
}
request_params["mfa_hash"] = mfa_hash;
LL_DEBUGS("MFA") << "mfa_hash in request params is set to: " << request_params["mfa_hash"] << LL_ENDL; // <FS:Beq/> add some debug output for mfa_hash
// Specify desired timeout/retry options
LLSD http_params;
F32 srv_timeout = llclamp(gSavedSettings.getF32("LoginSRVTimeout"), LOGIN_SRV_TIMEOUT_MIN, LOGIN_SRV_TIMEOUT_MAX);
http_params["timeout"] = srv_timeout;
http_params["retries"] = LOGIN_MAX_RETRIES;
http_params["DNSCacheTimeout"] = srv_timeout * LOGIN_DNS_TIMEOUT_FACTOR; //Default: indefinite
mRequestData.clear();
mRequestData["method"] = "login_to_simulator";
mRequestData["params"] = request_params;
mRequestData["options"] = requested_options;
mRequestData["http_params"] = http_params;
//#if LL_RELEASE_FOR_DOWNLOAD
// mRequestData["wait_for_updater"] = !gSavedSettings.getBOOL("CmdLineSkipUpdater")
// && !LLAppViewer::instance()->isUpdaterMissing()
// && !gNonInteractive;
//#else
mRequestData["wait_for_updater"] = false;
//#endif
}
bool LLLoginInstance::handleLoginEvent(const LLSD& event)
{
LL_DEBUGS("LLLogin") << "LoginListener called!: \n" << event << LL_ENDL;
if(!(event.has("state") && event.has("change") && event.has("progress")))
{
LL_ERRS("LLLogin") << "Unknown message from LLLogin: " << event << LL_ENDL;
}
mLoginState = event["state"].asString();
mResponseData = event["data"];
if(event.has("transfer_rate"))
{
mTransferRate = event["transfer_rate"].asReal();
}
// Call the method registered in constructor, if any, for more specific
// handling
mDispatcher.try_call(event);
return false;
}
void LLLoginInstance::handleLoginFailure(const LLSD& event)
{
// TODO: we are handling failure in two separate places -
// here and in STATE_LOGIN_PROCESS_RESPONSE processing
// consider uniting them.
// Login has failed.
// Figure out why and respond...
LLSD response = event["data"];
LLSD updater = response["updater"];
// Always provide a response to the updater, if in fact the updater
// contacted us, if in fact the ping contains a 'reply' key. Most code
// paths tell it not to proceed with updating.
ResponsePtr resp(std::make_shared<LLEventAPI::Response>
(LLSDMap("update", false), updater));
std::string reason_response = response["reason"].asString();
std::string message_response = response["message"].asString();
LL_DEBUGS("LLLogin") << "reason " << reason_response
<< " message " << message_response
<< LL_ENDL;
if (response.has("mfa_hash"))
{
mRequestData["params"]["mfa_hash"] = response["mfa_hash"];
mRequestData["params"]["token"] = "";
saveMFAHash(response);
}
// For the cases of critical message or TOS agreement,
// start the TOS dialog. The dialog response will be handled
// by the LLLoginInstance::handleTOSResponse() callback.
// The callback intiates the login attempt next step, either
// to reconnect or to end the attempt in failure.
if(reason_response == "tos")
{
LL_INFOS("LLLogin") << " ToS" << LL_ENDL;
LLSD data(LLSD::emptyMap());
data["message"] = message_response;
data["reply_pump"] = TOS_REPLY_PUMP;
if (gViewerWindow)
gViewerWindow->setShowProgress(false, false);
LLFloaterReg::showInstance("message_tos", data);
LLEventPumps::instance().obtain(TOS_REPLY_PUMP)
.listen(TOS_LISTENER_NAME,
boost::bind(&LLLoginInstance::handleTOSResponse,
this, _1, "agree_to_tos"));
}
else if(reason_response == "critical")
{
LL_INFOS("LLLogin") << "LLLoginInstance::handleLoginFailure Crit" << LL_ENDL;
LLSD data(LLSD::emptyMap());
data["message"] = message_response;
data["reply_pump"] = TOS_REPLY_PUMP;
if(response.has("error_code"))
{
data["error_code"] = response["error_code"];
}
if(response.has("certificate"))
{
data["certificate"] = response["certificate"];
}
if (gViewerWindow)
gViewerWindow->setShowProgress(false, false);
LLFloaterReg::showInstance("message_critical", data);
LLEventPumps::instance().obtain(TOS_REPLY_PUMP)
.listen(TOS_LISTENER_NAME,
boost::bind(&LLLoginInstance::handleTOSResponse,
this, _1, "read_critical"));
}
else if(reason_response == "update")
{
// This can happen if the user clicked Login quickly, before we heard
// back from the Viewer Version Manager, but login failed because
// login.cgi is insisting on a required update. We were called with an
// event that bundles both the login.cgi 'response' and the
// synchronization event from the 'updater'.
std::string login_version = response["message_args"]["VERSION"];
std::string vvm_version = updater["VERSION"];
std::string relnotes = updater["URL"];
LL_WARNS("LLLogin") << "Login failed because an update to version " << login_version << " is required." << LL_ENDL;
// vvm_version might be empty because we might not have gotten
// SLVersionChecker's LoginSync handshake. But if it IS populated, it
// should (!) be the same as the version we got from login.cgi.
if ((! vvm_version.empty()) && vvm_version != login_version)
{
LL_WARNS("LLLogin") << "VVM update version " << vvm_version
<< " differs from login version " << login_version
<< "; presenting VVM version to match release notes URL"
<< LL_ENDL;
login_version = vvm_version;
}
if (relnotes.empty() || relnotes.find("://") == std::string::npos)
{
relnotes = LLTrans::getString("RELEASE_NOTES_BASE_URL");
if (!LLStringUtil::endsWith(relnotes, "/"))
relnotes += "/";
relnotes += LLURI::escape(login_version) + ".html";
}
if (gViewerWindow)
gViewerWindow->setShowProgress(false, false);
LLSD args;
args["VERSION"] = login_version;
args["URL"] = relnotes;
if (updater.isUndefined())
{
// If the updater failed to shake hands, better advise the user to
// download the update him/herself.
LLNotificationsUtil::add(
"RequiredUpdate",
args,
updater,
boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
}
else
{
// If we've heard from the updater that an update is required,
// then display the prompt that assures the user we'll take care
// of it. This is the one case in which we bind 'resp':
// instead of destroying our Response object (and thus sending a
// negative reply to the updater) as soon as we exit this
// function, bind our shared_ptr so it gets passed into
// syncWithUpdater. That ensures that the response is delayed
// until the user has responded to the notification.
LLNotificationsUtil::add(
"PauseForUpdate",
args,
updater,
boost::bind(&LLLoginInstance::syncWithUpdater, this, resp, _1, _2));
}
}
else if(reason_response == "mfa_challenge")
{
LL_DEBUGS("LLLogin") << " MFA challenge" << LL_ENDL;
if (gViewerWindow)
{
gViewerWindow->setShowProgress(false, false);
}
showMFAChallange(LLTrans::getString(response["message_id"].asString()));
}
else if( reason_response == "key"
|| reason_response == "presence"
|| reason_response == "connect"
|| !message_response.empty() // will be handled in STATE_LOGIN_PROCESS_RESPONSE
|| !response["message_id"].asString().empty()
)
{
// these are events that have already been communicated elsewhere
attemptComplete();
}
else
{
LL_WARNS("LLLogin") << "Login failed for an unknown reason: " << LLSDOStreamer<LLSDNotationFormatter>(response) << LL_ENDL;
if (gViewerWindow)
gViewerWindow->setShowProgress(false, false);
LLNotificationsUtil::add("LoginFailedUnknown", LLSD::emptyMap(), LLSD::emptyMap(), boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
}
}
void LLLoginInstance::syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response)
{
LL_INFOS("LLLogin") << "LLLoginInstance::syncWithUpdater" << LL_ENDL;
// 'resp' points to an instance of LLEventAPI::Response that will be
// destroyed as soon as we return and the notification response functor is
// unregistered. Modify it so that it tells the updater to go ahead and
// perform the update. Naturally, if we allowed the user a choice as to
// whether to proceed or not, this assignment would reflect the user's
// selection.
(*resp)["update"] = true;
attemptComplete();
}
void LLLoginInstance::handleLoginDisallowed(const LLSD& notification, const LLSD& response)
{
attemptComplete();
}
void LLLoginInstance::handleLoginSuccess(const LLSD& event)
{
LL_INFOS("LLLogin") << "LLLoginInstance::handleLoginSuccess" << LL_ENDL;
attemptComplete();
mRequestData.clear();
}
void LLLoginInstance::handleDisconnect(const LLSD& event)
{
// placeholder
LL_INFOS("LLLogin") << "LLLoginInstance::handleDisconnect placeholder " << LL_ENDL;
}
void LLLoginInstance::handleIndeterminate(const LLSD& event)
{
// The indeterminate response means that the server
// gave the viewer a new url and params to try.
// The login module handles the retry, but it gives us the
// server response so that we may show
// the user some status.
LLSD message = event.get("data").get("message");
if(message.isDefined())
{
LL_INFOS("LLLogin") << "LLLoginInstance::handleIndeterminate " << message.asString() << LL_ENDL;
LLSD progress_update;
progress_update["desc"] = message;
LLEventPumps::getInstance()->obtain("LLProgressView").post(progress_update);
}
}
bool LLLoginInstance::handleTOSResponse(bool accepted, const std::string& key)
{
if(accepted)
{
LL_INFOS("LLLogin") << "LLLoginInstance::handleTOSResponse: accepted " << LL_ENDL;
// Set the request data to true and retry login.
mRequestData["params"][key] = true;
if (!mRequestData["params"]["token"].asString().empty())
{
// SL-18511 this TOS failure happened while we are in the middle of an MFA challenge/response.
// the previously entered token is very likely expired, so prompt again
showMFAChallange(LLTrans::getString("LoginFailedAuthenticationMFARequired"));
}
else
{
reconnect();
}
}
else
{
LL_INFOS("LLLogin") << "LLLoginInstance::handleTOSResponse: attemptComplete" << LL_ENDL;
attemptComplete();
}
LLEventPumps::instance().obtain(TOS_REPLY_PUMP).stopListening(TOS_LISTENER_NAME);
return true;
}
void LLLoginInstance::showMFAChallange(const std::string& message)
{
LLSD args(llsd::map("MESSAGE", message));
LLSD payload;
if (gSavedSettings.getBOOL("RememberUser"))
{
LLNotificationsUtil::add("PromptMFATokenWithSave", args, payload,
boost::bind(&LLLoginInstance::handleMFAChallenge, this, _1, _2));
}
else
{
LLNotificationsUtil::add("PromptMFAToken", args, payload,
boost::bind(&LLLoginInstance::handleMFAChallenge, this, _1, _2));
}
}
bool LLLoginInstance::handleMFAChallenge(LLSD const & notif, LLSD const & response)
{
bool continue_clicked = response["continue"].asBoolean();
std::string token = response["token"].asString();
LL_DEBUGS("LLLogin") << "PromptMFAToken: response: " << response << " continue_clicked" << continue_clicked << LL_ENDL;
// strip out whitespace - SL-17034/BUG-231938
token = boost::regex_replace(token, boost::regex("\\s"), "");
if (continue_clicked && !token.empty())
{
LL_INFOS("LLLogin") << "PromptMFAToken: token submitted" << LL_ENDL;
// Set the request data to true and retry login.
mRequestData["params"]["token"] = token;
mSaveMFA = response.has("ignore") ? response["ignore"].asBoolean() : false;
reconnect();
} else {
LL_INFOS("LLLogin") << "PromptMFAToken: no token, attemptComplete" << LL_ENDL;
attemptComplete();
}
return true;
}
void LLLoginInstance::saveMFAHash(LLSD const& response)
{
std::string grid(LLGridManager::getInstance()->getGridId());
std::string user_id(LLStartUp::getUserId());
// Only save mfa_hash for future logins if the user wants their info remembered.
// <FS:Beq> add some debug output for mfa_hash
LL_DEBUGS("MFA") << "In saveMFAHagsh, grid: " << grid
<< ", user_id: " << user_id
<< ", remember user: " << (gSavedSettings.getBOOL("RememberUser")? "true" : "false")
<< ", saveMFA: " << (LLLoginInstance::getInstance()->saveMFA()? "true" : "false")
<< LL_ENDL;
if (response.has("mfa_hash"))
{
LL_DEBUGS("MFA") << "In saveMFAHash, mfa_hash: " << response["mfa_hash"].asString()
<< LL_ENDL;
}
else
{
LL_DEBUGS("MFA") << "In saveMFAHash, no mfa_hash in response"
<< LL_ENDL;
}
// </FS:Beq>
if (response.has("mfa_hash") && gSavedSettings.getBOOL("RememberUser") && LLLoginInstance::getInstance()->saveMFA())
{
// <FS:Beq> add some debug output for mfa_hash
LL_DEBUGS("MFA") << "In saveMFAHash, mfa_hash: " << response["mfa_hash"].asString()
<< LL_ENDL;
// </FS:Beq>
gSecAPIHandler->addToProtectedMap("mfa_hash", grid, user_id, response["mfa_hash"]);
}
else if (!LLLoginInstance::getInstance()->saveMFA())
{
// <FS:Beq> add some debug output for mfa_hash
LL_DEBUGS("MFA") << "In saveMFAHash, removing mfa_hash from protected map"
<< LL_ENDL;
// User does not want to save mfa_hash, remove it from the protected map
gSecAPIHandler->removeFromProtectedMap("mfa_hash", grid, user_id);
}
// TODO(brad) - related to SL-17223 consider building a better interface that sync's automatically
// <FS:Beq> add some debug output for mfa_hash
LL_DEBUGS("MFA") << "In saveMFAHash, syncing protected map"
<< LL_ENDL;
gSecAPIHandler->syncProtectedMap();
}
std::string construct_start_string()
{
std::string start;
LLSLURL start_slurl = LLStartUp::getStartSLURL();
switch(start_slurl.getType())
{
case LLSLURL::LOCATION:
{
// a startup URL was specified
LLVector3 position = start_slurl.getPosition();
// NOTE - do not xml escape here, will get escaped properly later by LLSD::asXMLRPCValue()
// see secondlife/viewer#2395
start =
STRINGIZE( "uri:"
<< start_slurl.getRegion() << "&"
<< position[VX] << "&"
<< position[VY] << "&"
<< position[VZ]);
break;
}
case LLSLURL::HOME_LOCATION:
{
start = "home";
break;
}
default:
{
start = "last";
}
}
return start;
}