From eeb5381bbbf58190824d047d845569564368c708 Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Thu, 18 Sep 2008 15:50:52 +0000 Subject: and this actually adds the appearance code itself (and not just the check-in message) --- .../Rest/Inventory/RestAppearanceServices.cs | 826 +++++++++++++++++++++ OpenSim/ApplicationPlugins/Rest/rest.xsd | 276 +++++++ 2 files changed, 1102 insertions(+) create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/RestAppearanceServices.cs create mode 100644 OpenSim/ApplicationPlugins/Rest/rest.xsd (limited to 'OpenSim/ApplicationPlugins') diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAppearanceServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAppearanceServices.cs new file mode 100644 index 0000000..bc706f0 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAppearanceServices.cs @@ -0,0 +1,826 @@ +/* + * 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 + { + 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,"appearance data not found"); + } + + 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; + + AvatarAppearance old = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID); + rdata.userAppearance = new AvatarAppearance(); + + rdata.userAppearance.Owner = old.Owner; + + if (GetUserAppearance(rdata)) + { + modified = rdata.userAppearance != null; + created = !modified; + Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); + } + + 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 (GetUserAppearance(rdata)) + { + modified = rdata.userAppearance != null; + created = !modified; + Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); + } + + 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; + } + 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 FormatUserAppearance(AppearanceRequestData rdata) + { + + rdata.writer.WriteStartElement("Appearance"); + rdata.writer.WriteAttributeString("Height", rdata.userAppearance.AvatarHeight.ToString()); + + rdata.writer.WriteStartElement("Body"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.BodyItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.BodyAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Skin"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.SkinItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.SkinAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Hair"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.HairItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.HairAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Eyes"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.EyesItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.EyesAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Shirt"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.ShirtItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.ShirtAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Pants"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.PantsItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.PantsAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Shoes"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.ShoesItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.ShoesAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Socks"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.SocksItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.SocksAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Jacket"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.JacketItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.JacketAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Gloves"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.GlovesItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.GlovesAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("UnderShirt"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.UnderShirtItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.UnderShirtAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("UnderPants"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.UnderPantsItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.UnderPantsAsset.ToString()); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteStartElement("Skirt"); + rdata.writer.WriteAttributeString("Item",rdata.userAppearance.SkirtItem.ToString()); + rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.SkirtAsset.ToString()); + rdata.writer.WriteEndElement(); + + Hashtable attachments = rdata.userAppearance.GetAttachments(); + + rdata.writer.WriteStartElement("Attachments"); + + 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 + + } +} diff --git a/OpenSim/ApplicationPlugins/Rest/rest.xsd b/OpenSim/ApplicationPlugins/Rest/rest.xsd new file mode 100644 index 0000000..4dc0ae4 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/rest.xsd @@ -0,0 +1,276 @@ + + + + + Open Simulator Export/Import XML schema + August 2008 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.1