/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSim Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Xml;
using System.Drawing;
using OpenSim.Framework;
using OpenSim.Framework.Servers;
using OpenSim.Framework.Communications;
using OpenSim.Framework.Communications.Cache;
using OpenMetaverse;
using OpenMetaverse.Imaging;
using Nini.Config;
namespace OpenSim.ApplicationPlugins.Rest.Inventory
{
public class RestAppearanceServices : IRest
{
private static readonly int PARM_USERID = 0;
// private static readonly int PARM_PATH = 1;
private bool enabled = false;
private string qPrefix = "appearance";
///
/// The constructor makes sure that the service prefix is absolute
/// and the registers the service handler and the allocator.
///
public RestAppearanceServices()
{
Rest.Log.InfoFormat("{0} User appearance services initializing", MsgId);
Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
// If a relative path was specified for the handler's domain,
// add the standard prefix to make it absolute, e.g. /admin
if (!qPrefix.StartsWith(Rest.UrlPathSeparator))
{
Rest.Log.InfoFormat("{0} Domain is relative, adding absolute prefix", MsgId);
qPrefix = String.Format("{0}{1}{2}", Rest.Prefix, Rest.UrlPathSeparator, qPrefix);
Rest.Log.InfoFormat("{0} Domain is now <{1}>", MsgId, qPrefix);
}
// Register interface using the absolute URI.
Rest.Plugin.AddPathHandler(DoAppearance,qPrefix,Allocate);
// Activate if everything went OK
enabled = true;
Rest.Log.InfoFormat("{0} User appearance services initialization complete", MsgId);
}
///
/// Post-construction, pre-enabled initialization opportunity
/// Not currently exploited.
///
public void Initialize()
{
}
///
/// Called by the plug-in to halt service processing. Local processing is
/// disabled.
///
public void Close()
{
enabled = false;
Rest.Log.InfoFormat("{0} User appearance services closing down", MsgId);
}
///
/// This property is declared locally because it is used a lot and
/// brevity is nice.
///
internal string MsgId
{
get { return Rest.MsgId; }
}
#region Interface
///
/// The plugin (RestHandler) calls this method to allocate the request
/// state carrier for a new request. It is destroyed when the request
/// completes. All request-instance specific state is kept here. This
/// is registered when this service provider is registered.
///
/// Inbound HTTP request information
/// Outbound HTTP request information
/// REST service domain prefix
/// A RequestData instance suitable for this service
private RequestData Allocate(OSHttpRequest request, OSHttpResponse response, string prefix)
{
return (RequestData) new AppearanceRequestData(request, response, prefix);
}
///
/// This method is registered with the handler when this service provider
/// is initialized. It is called whenever the plug-in identifies this service
/// provider as the best match for a given request.
/// It handles all aspects of inventory REST processing, i.e. /admin/inventory
///
/// A consolidated HTTP request work area
private void DoAppearance(RequestData hdata)
{
AppearanceRequestData rdata = (AppearanceRequestData) hdata;
Rest.Log.DebugFormat("{0} DoAppearance ENTRY", MsgId);
// If we're disabled, do nothing.
if (!enabled)
{
return;
}
// Now that we know this is a serious attempt to
// access inventory data, we should find out who
// is asking, and make sure they are authorized
// to do so. We need to validate the caller's
// identity before revealing anything about the
// status quo. Authenticate throws an exception
// via Fail if no identity information is present.
//
// With the present HTTP server we can't use the
// builtin authentication mechanisms because they
// would be enforced for all in-bound requests.
// Instead we look at the headers ourselves and
// handle authentication directly.
try
{
// digest scheme seems borked: disable it for the time
// being
rdata.scheme = Rest.AS_BASIC;
if (!rdata.IsAuthenticated)
{
rdata.Fail(Rest.HttpStatusCodeNotAuthorized,String.Format("user \"{0}\" could not be authenticated", rdata.userName));
}
}
catch (RestException e)
{
if (e.statusCode == Rest.HttpStatusCodeNotAuthorized)
{
Rest.Log.WarnFormat("{0} User not authenticated", MsgId);
Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization"));
}
else
{
Rest.Log.ErrorFormat("{0} User authentication failed", MsgId);
Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization"));
}
throw (e);
}
Rest.Log.DebugFormat("{0} Authenticated {1}", MsgId, rdata.userName);
// We can only get here if we are authorized
//
// The requestor may have specified an UUID or
// a conjoined FirstName LastName string. We'll
// try both. If we fail with the first, UUID,
// attempt, we try the other. As an example, the
// URI for a valid inventory request might be:
//
// http://:/admin/inventory/Arthur Dent
//
// Indicating that this is an inventory request for
// an avatar named Arthur Dent. This is ALL that is
// required to designate a GET for an entire
// inventory.
//
// Do we have at least a user agent name?
if (rdata.Parameters.Length < 1)
{
Rest.Log.WarnFormat("{0} Appearance: No user agent identifier specified", MsgId);
rdata.Fail(Rest.HttpStatusCodeBadRequest, "no user identity specified");
}
// The first parameter MUST be the agent identification, either an UUID
// or a space-separated First-name Last-Name specification. We check for
// an UUID first, if anyone names their character using a valid UUID
// that identifies another existing avatar will cause this a problem...
try
{
rdata.uuid = new UUID(rdata.Parameters[PARM_USERID]);
Rest.Log.DebugFormat("{0} UUID supplied", MsgId);
rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid);
}
catch
{
string[] names = rdata.Parameters[PARM_USERID].Split(Rest.CA_SPACE);
if (names.Length == 2)
{
Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId);
rdata.userProfile = Rest.UserServices.GetUserProfile(names[0],names[1]);
}
else
{
Rest.Log.WarnFormat("{0} A Valid UUID or both first and last names must be specified", MsgId);
rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid user identity");
}
}
// If the user profile is null then either the server is broken, or the
// user is not known. We always assume the latter case.
if (rdata.userProfile != null)
{
Rest.Log.DebugFormat("{0} User profile obtained for agent {1} {2}",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
}
else
{
Rest.Log.WarnFormat("{0} No user profile for {1}", MsgId, rdata.path);
rdata.Fail(Rest.HttpStatusCodeNotFound, "unrecognized user identity");
}
// If we get to here, then we have effectively validated the user's
switch (rdata.method)
{
case Rest.HEAD : // Do the processing, set the status code, suppress entity
DoGet(rdata);
rdata.buffer = null;
break;
case Rest.GET : // Do the processing, set the status code, return entity
DoGet(rdata);
break;
case Rest.PUT : // Update named element
DoUpdate(rdata);
break;
case Rest.POST : // Add new information to identified context.
DoExtend(rdata);
break;
case Rest.DELETE : // Delete information
DoDelete(rdata);
break;
default :
Rest.Log.WarnFormat("{0} Method {1} not supported for {2}",
MsgId, rdata.method, rdata.path);
rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed,
String.Format("{0} not supported", rdata.method));
break;
}
}
#endregion Interface
#region method-specific processing
///
/// This method implements GET processing for user's appearance.
///
/// HTTP service request work area
private void DoGet(AppearanceRequestData rdata)
{
rdata.userAppearance = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID);
if (rdata.userAppearance == null)
{
rdata.Fail(Rest.HttpStatusCodeNoContent,
String.Format("appearance data not found for user {0} {1}",
rdata.userProfile.FirstName, rdata.userProfile.SurName));
}
rdata.initXmlWriter();
FormatUserAppearance(rdata);
// Indicate a successful request
rdata.Complete();
// Send the response to the user. The body will be implicitly
// constructed from the result of the XML writer.
rdata.Respond(String.Format("Appearance {0} Normal completion", rdata.method));
}
///
/// POST adds NEW information to the user profile database.
/// This effectively resets the appearance before applying those
/// characteristics supplied in the request.
///
private void DoExtend(AppearanceRequestData rdata)
{
bool created = false;
bool modified = false;
string newnode = String.Empty;
Rest.Log.DebugFormat("{0} POST ENTRY", MsgId);
AvatarAppearance old = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID);
rdata.userAppearance = new AvatarAppearance();
// Although the following behavior is admitted by HTTP I am becoming
// increasingly doubtful that it is appropriate for REST. If I attempt to
// add a new record, and it already exists, then it seems to me that the
// attempt should fail, rather than update the existing record.
if (GetUserAppearance(rdata))
{
modified = rdata.userAppearance != null;
created = !modified;
Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance);
// Rest.UserServices.UpdateUserProfile(rdata.userProfile);
}
else
{
created = true;
Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance);
// Rest.UserServices.UpdateUserProfile(rdata.userProfile);
}
if (created)
{
newnode = String.Format("{0} {1}", rdata.userProfile.FirstName,
rdata.userProfile.SurName);
// Must include a location header with a URI that identifies the new resource.
rdata.AddHeader(Rest.HttpHeaderLocation,String.Format("http://{0}{1}:{2}{3}{4}",
rdata.hostname,rdata.port,rdata.path,Rest.UrlPathSeparator, newnode));
rdata.Complete(Rest.HttpStatusCodeCreated);
}
else
{
if (modified)
{
rdata.Complete(Rest.HttpStatusCodeOK);
}
else
{
rdata.Complete(Rest.HttpStatusCodeNoContent);
}
}
rdata.Respond(String.Format("Appearance {0} : Normal completion", rdata.method));
}
///
/// This updates the user's appearance. not all aspects need to be provided,
/// only those supplied will be changed.
///
private void DoUpdate(AppearanceRequestData rdata)
{
bool created = false;
bool modified = false;
rdata.userAppearance = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID);
// If the user exists then this is considered a modification regardless
// of what may, or may not be, specified in the payload.
if (rdata.userAppearance != null)
{
modified = true;
Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance);
Rest.UserServices.UpdateUserProfile(rdata.userProfile);
}
if (created)
{
rdata.Complete(Rest.HttpStatusCodeCreated);
}
else
{
if (modified)
{
rdata.Complete(Rest.HttpStatusCodeOK);
}
else
{
rdata.Complete(Rest.HttpStatusCodeNoContent);
}
}
rdata.Respond(String.Format("Appearance {0} : Normal completion", rdata.method));
}
///
/// Delete the specified user's appearance. This actually performs a reset
/// to the default avatar appearance, if the info is already there.
/// Existing ownership is preserved. All prior updates are lost and can not
/// be recovered.
///
private void DoDelete(AppearanceRequestData rdata)
{
AvatarAppearance old = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID);
if (old != null)
{
rdata.userAppearance = new AvatarAppearance();
rdata.userAppearance.Owner = old.Owner;
Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance);
rdata.Complete();
}
else
{
rdata.Complete(Rest.HttpStatusCodeNoContent);
}
rdata.Respond(String.Format("Appearance {0} : Normal completion", rdata.method));
}
#endregion method-specific processing
private bool GetUserAppearance(AppearanceRequestData rdata)
{
XmlReader xml;
bool indata = false;
rdata.initXmlReader();
xml = rdata.reader;
while (xml.Read())
{
switch (xml.NodeType)
{
case XmlNodeType.Element :
switch (xml.Name)
{
case "Appearance" :
if (xml.MoveToAttribute("Height"))
{
rdata.userAppearance.AvatarHeight = (float) Convert.ToDouble(xml.Value);
indata = true;
}
if (xml.MoveToAttribute("Owner"))
{
rdata.userAppearance.Owner = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Serial"))
{
rdata.userAppearance.Serial = Convert.ToInt32(xml.Value);
indata = true;
}
break;
case "Body" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.BodyItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.BodyAsset = xml.Value;
indata = true;
}
break;
case "Skin" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.SkinItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.SkinAsset = xml.Value;
indata = true;
}
break;
case "Hair" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.HairItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.HairAsset = xml.Value;
indata = true;
}
break;
case "Eyes" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.EyesItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.EyesAsset = xml.Value;
indata = true;
}
break;
case "Shirt" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.ShirtItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.ShirtAsset = xml.Value;
indata = true;
}
break;
case "Pants" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.PantsItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.PantsAsset = xml.Value;
indata = true;
}
break;
case "Shoes" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.ShoesItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.ShoesAsset = xml.Value;
indata = true;
}
break;
case "Socks" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.SocksItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.SocksAsset = xml.Value;
indata = true;
}
break;
case "Jacket" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.JacketItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.JacketAsset = xml.Value;
indata = true;
}
break;
case "Gloves" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.GlovesItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.GlovesAsset = xml.Value;
indata = true;
}
break;
case "UnderShirt" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.UnderShirtItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.UnderShirtAsset = xml.Value;
indata = true;
}
break;
case "UnderPants" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.UnderPantsItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.UnderPantsAsset = xml.Value;
indata = true;
}
break;
case "Skirt" :
if (xml.MoveToAttribute("Item"))
{
rdata.userAppearance.SkirtItem = xml.Value;
indata = true;
}
if (xml.MoveToAttribute("Asset"))
{
rdata.userAppearance.SkirtAsset = xml.Value;
indata = true;
}
break;
case "Attachment" :
{
int ap;
UUID asset;
UUID item;
if (xml.MoveToAttribute("AtPoint"))
{
ap = Convert.ToInt32(xml.Value);
if (xml.MoveToAttribute("Asset"))
{
asset = new UUID(xml.Value);
if (xml.MoveToAttribute("Asset"))
{
item = new UUID(xml.Value);
rdata.userAppearance.SetAttachment(ap, item, asset);
indata = true;
}
}
}
}
break;
case "Texture" :
if (xml.MoveToAttribute("Default"))
{
rdata.userAppearance.Texture = new Primitive.TextureEntry(new UUID(xml.Value));
indata = true;
}
break;
case "Face" :
{
uint index;
if (xml.MoveToAttribute("Index"))
{
index = Convert.ToUInt32(xml.Value);
if (xml.MoveToAttribute("Id"))
{
rdata.userAppearance.Texture.CreateFace(index).TextureID = new UUID(xml.Value);
indata = true;
}
}
}
break;
case "VisualParameters" :
{
xml.ReadContentAsBase64(rdata.userAppearance.VisualParams,
0, rdata.userAppearance.VisualParams.Length);
indata = true;
}
break;
}
break;
}
}
return indata;
}
private void FormatPart(AppearanceRequestData rdata, string part, UUID item, UUID asset)
{
if (item != UUID.Zero || asset != UUID.Zero)
{
rdata.writer.WriteStartElement(part);
if (item != UUID.Zero)
{
rdata.writer.WriteAttributeString("Item",item.ToString());
}
if (asset != UUID.Zero)
{
rdata.writer.WriteAttributeString("Item",asset.ToString());
}
rdata.writer.WriteEndElement();
}
}
private void FormatUserAppearance(AppearanceRequestData rdata)
{
Rest.Log.DebugFormat("{0} FormatUserAppearance", MsgId);
if (rdata.userAppearance != null)
{
Rest.Log.DebugFormat("{0} FormatUserAppearance: appearance object exists", MsgId);
rdata.writer.WriteStartElement("Appearance");
rdata.writer.WriteAttributeString("Height", rdata.userAppearance.AvatarHeight.ToString());
if (rdata.userAppearance.Owner != null)
rdata.writer.WriteAttributeString("Owner", rdata.userAppearance.Owner.ToString());
rdata.writer.WriteAttributeString("Serial", rdata.userAppearance.Serial.ToString());
FormatPart(rdata, "Body", rdata.userAppearance.BodyItem, rdata.userAppearance.BodyAsset);
FormatPart(rdata, "Skin", rdata.userAppearance.SkinItem, rdata.userAppearance.SkinAsset);
FormatPart(rdata, "Hair", rdata.userAppearance.HairItem, rdata.userAppearance.HairAsset);
FormatPart(rdata, "Eyes", rdata.userAppearance.EyesItem, rdata.userAppearance.EyesAsset);
FormatPart(rdata, "Shirt", rdata.userAppearance.ShirtItem, rdata.userAppearance.ShirtAsset);
FormatPart(rdata, "Pants", rdata.userAppearance.PantsItem, rdata.userAppearance.PantsAsset);
FormatPart(rdata, "Skirt", rdata.userAppearance.SkirtItem, rdata.userAppearance.SkirtAsset);
FormatPart(rdata, "Shoes", rdata.userAppearance.ShoesItem, rdata.userAppearance.ShoesAsset);
FormatPart(rdata, "Socks", rdata.userAppearance.SocksItem, rdata.userAppearance.SocksAsset);
FormatPart(rdata, "Jacket", rdata.userAppearance.JacketItem, rdata.userAppearance.JacketAsset);
FormatPart(rdata, "Gloves", rdata.userAppearance.GlovesItem, rdata.userAppearance.GlovesAsset);
FormatPart(rdata, "UnderShirt", rdata.userAppearance.UnderShirtItem, rdata.userAppearance.UnderShirtAsset);
FormatPart(rdata, "UnderPants", rdata.userAppearance.UnderPantsItem, rdata.userAppearance.UnderPantsAsset);
Hashtable attachments = rdata.userAppearance.GetAttachments();
if (attachments != null)
{
Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting attachments", MsgId);
rdata.writer.WriteStartElement("Attachments");
for (int i = 0; i < attachments.Count; i++)
{
Hashtable attachment = attachments[i] as Hashtable;
rdata.writer.WriteStartElement("Attachment");
rdata.writer.WriteAttributeString("AtPoint", i.ToString());
rdata.writer.WriteAttributeString("Item", (string) attachment["item"]);
rdata.writer.WriteAttributeString("Asset", (string) attachment["asset"]);
rdata.writer.WriteEndElement();
}
rdata.writer.WriteEndElement();
}
Primitive.TextureEntry texture = rdata.userAppearance.Texture;
if (texture != null && (texture.DefaultTexture != null || texture.FaceTextures != null))
{
Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting textures", MsgId);
rdata.writer.WriteStartElement("Texture");
if (texture.DefaultTexture != null)
{
Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting default texture", MsgId);
rdata.writer.WriteAttributeString("Default",
texture.DefaultTexture.TextureID.ToString());
}
if (texture.FaceTextures != null)
{
Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting face textures", MsgId);
for (int i=0; i
/// These are the inventory specific request/response state
/// extensions.
///
internal UUID uuid = UUID.Zero;
internal UserProfileData userProfile = null;
internal AvatarAppearance userAppearance = null;
internal AppearanceRequestData(OSHttpRequest request, OSHttpResponse response, string prefix)
: base(request, response, prefix)
{
}
}
#endregion Appearance RequestData extension
}
}