();
+ }
+
+ removed_headers.Add(hdr);
+
+ if (headers != null)
+ {
+ headers.Remove(hdr);
+ }
+
+ }
+
+ // Should it prove necessary, we could always
+ // restore the header collection from a cloned
+ // copy, but for now we'll assume that that is
+ // not necessary.
+
+ private void BuildHeaders()
+ {
+ if (removed_headers != null)
+ {
+ foreach (string h in removed_headers)
+ {
+ Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, h);
+ response.Headers.Remove(h);
+ }
+ }
+ if (headers!= null)
+ {
+ for (int i = 0; i < headers.Count; i++)
+ {
+ Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>",
+ MsgId, headers.GetKey(i), headers.Get(i));
+ response.Headers.Add(headers.GetKey(i), headers.Get(i));
+ }
+ }
+ }
+
+ ///
+ /// Helper methods for deconstructing and reconstructing
+ /// URI path data.
+ ///
+
+ private void initUrl()
+ {
+
+ uri = request.Url;
+
+ if (query == null)
+ {
+ query = uri.Query;
+ }
+
+ // If the path has not been previously initialized,
+ // do so now.
+
+ if (path == null)
+ {
+ path = uri.AbsolutePath;
+ if (path.EndsWith(Rest.UrlPathSeparator))
+ path = path.Substring(0,path.Length-1);
+ path = Uri.UnescapeDataString(path);
+ }
+
+ // If we succeeded in getting a path, perform any
+ // additional pre-processing required.
+
+ if (path != null)
+ {
+ if (Rest.ExtendedEscape)
+ {
+ // Handle "+". Not a standard substitution, but
+ // common enough...
+ path = path.Replace(Rest.C_PLUS,Rest.C_SPACE);
+ }
+ pathNodes = path.Split(Rest.CA_PATHSEP);
+ }
+ else
+ {
+ pathNodes = EmptyPath;
+ }
+
+ // Request server context info
+
+ hostname = uri.Host;
+ port = uri.Port;
+
+ }
+
+ internal int initParameters(int prfxlen)
+ {
+
+ if (prfxlen < path.Length-1)
+ {
+ parameters = path.Substring(prfxlen+1).Split(Rest.CA_PATHSEP);
+ }
+ else
+ {
+ parameters = new string[0];
+ }
+
+ // Generate a debug list of the decoded parameters
+
+ if (Rest.DEBUG && prfxlen < path.Length-1)
+ {
+ Rest.Log.DebugFormat("{0} URI: Parameters: {1}", MsgId, path.Substring(prfxlen));
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ Rest.Log.DebugFormat("{0} Parameter[{1}]: {2}", MsgId, i, parameters[i]);
+ }
+ }
+
+ return parameters.Length;
+
+ }
+
+ internal string[] PathNodes
+ {
+ get
+ {
+ if (pathNodes == null)
+ {
+ initUrl();
+ }
+ return pathNodes;
+ }
+ }
+
+ internal string BuildUrl(int first, int last)
+ {
+
+ if (pathNodes == null)
+ {
+ initUrl();
+ }
+
+ if (first < 0)
+ {
+ first = first + pathNodes.Length;
+ }
+
+ if (last < 0)
+ {
+ last = last + pathNodes.Length;
+ if (last < 0)
+ {
+ return Rest.UrlPathSeparator;
+ }
+ }
+
+ sbuilder.Length = 0;
+ sbuilder.Append(Rest.UrlPathSeparator);
+
+ if (first <= last)
+ {
+ for (int i = first; i <= last; i++)
+ {
+ sbuilder.Append(pathNodes[i]);
+ sbuilder.Append(Rest.UrlPathSeparator);
+ }
+ }
+ else
+ {
+ for (int i = last; i >= first; i--)
+ {
+ sbuilder.Append(pathNodes[i]);
+ sbuilder.Append(Rest.UrlPathSeparator);
+ }
+ }
+
+ return sbuilder.ToString();
+
+ }
+
+ // Setup the XML writer for output
+
+ internal void initXmlWriter()
+ {
+ XmlWriterSettings settings = new XmlWriterSettings();
+ xmldata = new MemoryStream();
+ settings.Indent = true;
+ settings.IndentChars = " ";
+ settings.Encoding = encoding;
+ settings.CloseOutput = false;
+ settings.OmitXmlDeclaration = true;
+ settings.ConformanceLevel = ConformanceLevel.Fragment;
+ writer = XmlWriter.Create(xmldata, settings);
+ }
+
+ internal void initXmlReader()
+ {
+ XmlReaderSettings settings = new XmlReaderSettings();
+ settings.ConformanceLevel = ConformanceLevel.Fragment;
+ settings.IgnoreComments = true;
+ settings.IgnoreWhitespace = true;
+ settings.IgnoreProcessingInstructions = true;
+ settings.ValidationType = ValidationType.None;
+ // reader = XmlReader.Create(new StringReader(entity),settings);
+ reader = XmlReader.Create(request.InputStream,settings);
+ }
+
+ private void Flush()
+ {
+ byte[] dbuffer = new byte[8192];
+ while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0);
+ return;
+ }
+
+ // This allows us to make errors a bit more apparent in REST
+
+ internal void SendHtml(string text)
+ {
+ SendHtml("OpenSim REST Interface 1.0", text);
+ }
+
+ internal void SendHtml(string title, string text)
+ {
+
+ AddHeader(Rest.HttpHeaderContentType, "text/html");
+ sbuilder.Length = 0;
+
+ sbuilder.Append("");
+ sbuilder.Append("");
+ sbuilder.Append("");
+ sbuilder.Append(title);
+ sbuilder.Append("");
+ sbuilder.Append("");
+
+ sbuilder.Append("");
+ sbuilder.Append("
");
+ sbuilder.Append("");
+ sbuilder.Append(text);
+ sbuilder.Append("
");
+ sbuilder.Append("");
+ sbuilder.Append("");
+
+ html = sbuilder.ToString();
+
+ }
+ }
+}
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs
new file mode 100644
index 0000000000..e88c54d3d7
--- /dev/null
+++ b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs
@@ -0,0 +1,479 @@
+/*
+ * 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.Generic;
+using System.Reflection;
+using System.Text;
+using OpenSim.Framework;
+using OpenSim.Framework.Servers;
+using OpenSim.Framework.Communications;
+using OpenSim.Framework.Communications.Cache;
+using Nini.Config;
+
+namespace OpenSim.ApplicationPlugins.Rest.Inventory
+{
+
+ public class Rest
+ {
+
+ internal static readonly log4net.ILog Log =
+ log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+
+ internal static bool DEBUG = Log.IsDebugEnabled;
+
+ ///
+ /// These values have a single value for the whole
+ /// domain and lifetime of the plugin handler. We
+ /// make them static for ease of reference within
+ /// the assembly. These are initialized by the
+ /// RestHandler class during start-up.
+ ///
+
+ internal static RestHandler Plugin = null;
+ internal static OpenSimBase main = null;
+ internal static CommunicationsManager Comms = null;
+ internal static IInventoryServices InventoryServices = null;
+ internal static IUserService UserServices = null;
+ internal static AssetCache AssetServices = null;
+ internal static string Prefix = null;
+ internal static IConfig Config = null;
+ internal static string GodKey = null;
+ internal static bool Authenticate = true;
+ internal static bool Secure = true;
+ internal static bool ExtendedEscape = true;
+ internal static bool DumpAsset = false;
+ internal static string Realm = "REST";
+ internal static Dictionary Domains = new Dictionary();
+ internal static int CreationDate = (int) (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
+ internal static int DumpLineSize = 32; // Should be a multiple of 16 or (possibly) 4
+
+ internal static string MsgId
+ {
+ get { return Plugin.MsgId; }
+ }
+
+ internal static string RequestId
+ {
+ get { return Plugin.RequestId; }
+ }
+
+ internal static Encoding Encoding = Encoding.UTF8;
+
+ ///
+ /// Version control for REST implementation. This
+ /// refers to the overall infrastructure represented
+ /// by the following classes
+ /// RequestData
+ /// RequestInventoryPlugin
+ /// Rest
+ /// It does no describe implementation classes such as
+ /// RestInventoryServices, which may morph much more
+ /// often. Such classes ARE dependent upon this however
+ /// and should check it in their Initialize method.
+ ///
+
+ public static readonly float Version = 1.0F;
+ public const string Name = "REST 1.0";
+
+ ///
+ /// Currently defined HTTP methods.
+ /// Only GET and HEAD are required to be
+ /// supported by all servers. See Respond
+ /// to see how these are handled.
+ ///
+
+ // REST AGENT 1.0 interpretations
+ public const string GET = "get"; // information retrieval - server state unchanged
+ public const string HEAD = "head"; // same as get except only the headers are returned.
+ public const string POST = "post"; // Replace the URI designated resource with the entity.
+ public const string PUT = "put"; // Add the entity to the context represented by the URI
+ public const string DELETE = "delete"; // Remove the URI designated resource from the server.
+
+ public const string OPTIONS = "options"; //
+ public const string TRACE = "trace"; //
+ public const string CONNECT = "connect"; //
+
+ // Define this in one place...
+
+ public const string UrlPathSeparator = "/";
+ public const string UrlMethodSeparator = ":";
+
+ // Redirection qualifications
+
+ public const bool PERMANENT = false;
+ public const bool TEMPORARY = true;
+
+ // Constant arrays used by String.Split
+
+ public static readonly char C_SPACE = ' ';
+ public static readonly char C_SLASH = '/';
+ public static readonly char C_PATHSEP = '/';
+ public static readonly char C_COLON = ':';
+ public static readonly char C_PLUS = '+';
+ public static readonly char C_PERIOD = '.';
+ public static readonly char C_COMMA = ',';
+ public static readonly char C_DQUOTE = '"';
+
+ public static readonly string CS_SPACE = " ";
+ public static readonly string CS_SLASH = "/";
+ public static readonly string CS_PATHSEP = "/";
+ public static readonly string CS_COLON = ":";
+ public static readonly string CS_PLUS = "+";
+ public static readonly string CS_PERIOD = ".";
+ public static readonly string CS_COMMA = ",";
+ public static readonly string CS_DQUOTE = "\"";
+
+ public static readonly char[] CA_SPACE = { C_SPACE };
+ public static readonly char[] CA_SLASH = { C_SLASH };
+ public static readonly char[] CA_PATHSEP = { C_PATHSEP };
+ public static readonly char[] CA_COLON = { C_COLON };
+ public static readonly char[] CA_PERIOD = { C_PERIOD };
+ public static readonly char[] CA_PLUS = { C_PLUS };
+ public static readonly char[] CA_COMMA = { C_COMMA };
+ public static readonly char[] CA_DQUOTE = { C_DQUOTE };
+
+ // HTTP Code Values (in value order)
+
+ public const int HttpStatusCodeContinue = 100;
+ public const int HttpStatusCodeSwitchingProtocols = 101;
+
+ public const int HttpStatusCodeOK = 200;
+ public const int HttpStatusCodeCreated = 201;
+ public const int HttpStatusCodeAccepted = 202;
+ public const int HttpStatusCodeNonAuthoritative = 203;
+ public const int HttpStatusCodeNoContent = 204;
+ public const int HttpStatusCodeResetContent = 205;
+ public const int HttpStatusCodePartialContent = 206;
+
+ public const int HttpStatusCodeMultipleChoices = 300;
+ public const int HttpStatusCodePermanentRedirect = 301;
+ public const int HttpStatusCodeFound = 302;
+ public const int HttpStatusCodeSeeOther = 303;
+ public const int HttpStatusCodeNotModified = 304;
+ public const int HttpStatusCodeUseProxy = 305;
+ public const int HttpStatusCodeReserved306 = 306;
+ public const int HttpStatusCodeTemporaryRedirect = 307;
+
+ public const int HttpStatusCodeBadRequest = 400;
+ public const int HttpStatusCodeNotAuthorized = 401;
+ public const int HttpStatusCodePaymentRequired = 402;
+ public const int HttpStatusCodeForbidden = 403;
+ public const int HttpStatusCodeNotFound = 404;
+ public const int HttpStatusCodeMethodNotAllowed = 405;
+ public const int HttpStatusCodeNotAcceptable = 406;
+ public const int HttpStatusCodeProxyAuthenticate = 407;
+ public const int HttpStatusCodeTimeOut = 408;
+ public const int HttpStatusCodeConflict = 409;
+ public const int HttpStatusCodeGone = 410;
+ public const int HttpStatusCodeLengthRequired = 411;
+ public const int HttpStatusCodePreconditionFailed = 412;
+ public const int HttpStatusCodeEntityTooLarge = 413;
+ public const int HttpStatusCodeUriTooLarge = 414;
+ public const int HttpStatusCodeUnsupportedMedia = 415;
+ public const int HttpStatusCodeRangeNotSatsified = 416;
+ public const int HttpStatusCodeExpectationFailed = 417;
+
+ public const int HttpStatusCodeServerError = 500;
+ public const int HttpStatusCodeNotImplemented = 501;
+ public const int HttpStatusCodeBadGateway = 502;
+ public const int HttpStatusCodeServiceUnavailable = 503;
+ public const int HttpStatusCodeGatewayTimeout = 504;
+ public const int HttpStatusCodeHttpVersionError = 505;
+
+ // HTTP Status Descriptions (in status code order)
+
+ public const string HttpStatusDescContinue = "Continue Request"; // 100
+ public const string HttpStatusDescSwitchingProtocols = "Switching Protocols"; // 101
+
+ public const string HttpStatusDescOK = "OK";
+ public const string HttpStatusDescCreated = "CREATED";
+ public const string HttpStatusDescAccepted = "ACCEPTED";
+ public const string HttpStatusDescNonAuthoritative = "NON-AUTHORITATIVE INFORMATION";
+ public const string HttpStatusDescNoContent = "NO CONTENT";
+ public const string HttpStatusDescResetContent = "RESET CONTENT";
+ public const string HttpStatusDescPartialContent = "PARTIAL CONTENT";
+
+ public const string HttpStatusDescMultipleChoices = "MULTIPLE CHOICES";
+ public const string HttpStatusDescPermanentRedirect = "PERMANENT REDIRECT";
+ public const string HttpStatusDescFound = "FOUND";
+ public const string HttpStatusDescSeeOther = "SEE OTHER";
+ public const string HttpStatusDescNotModified = "NOT MODIFIED";
+ public const string HttpStatusDescUseProxy = "USE PROXY";
+ public const string HttpStatusDescReserved306 = "RESERVED CODE 306";
+ public const string HttpStatusDescTemporaryRedirect = "TEMPORARY REDIRECT";
+
+ public const string HttpStatusDescBadRequest = "BAD REQUEST";
+ public const string HttpStatusDescNotAuthorized = "NOT AUTHORIZED";
+ public const string HttpStatusDescPaymentRequired = "PAYMENT REQUIRED";
+ public const string HttpStatusDescForbidden = "FORBIDDEN";
+ public const string HttpStatusDescNotFound = "NOT FOUND";
+ public const string HttpStatusDescMethodNotAllowed = "METHOD NOT ALLOWED";
+ public const string HttpStatusDescNotAcceptable = "NOT ACCEPTABLE";
+ public const string HttpStatusDescProxyAuthenticate = "PROXY AUTHENTICATION REQUIRED";
+ public const string HttpStatusDescTimeOut = "TIMEOUT";
+ public const string HttpStatusDescConflict = "CONFLICT";
+ public const string HttpStatusDescGone = "GONE";
+ public const string HttpStatusDescLengthRequired = "LENGTH REQUIRED";
+ public const string HttpStatusDescPreconditionFailed = "PRECONDITION FAILED";
+ public const string HttpStatusDescEntityTooLarge = "ENTITY TOO LARGE";
+ public const string HttpStatusDescUriTooLarge = "URI TOO LARGE";
+ public const string HttpStatusDescUnsupportedMedia = "UNSUPPORTED MEDIA";
+ public const string HttpStatusDescRangeNotSatisfied = "RANGE NOT SATISFIED";
+ public const string HttpStatusDescExpectationFailed = "EXPECTATION FAILED";
+
+ public const string HttpStatusDescServerError = "SERVER ERROR";
+ public const string HttpStatusDescNotImplemented = "NOT IMPLEMENTED";
+ public const string HttpStatusDescBadGateway = "BAD GATEWAY";
+ public const string HttpStatusDescServiceUnavailable = "SERVICE UNAVAILABLE";
+ public const string HttpStatusDescGatewayTimeout = "GATEWAY TIMEOUT";
+ public const string HttpStatusDescHttpVersionError = "HTTP VERSION NOT SUPPORTED";
+
+ // HTTP Headers
+
+ public const string HttpHeaderAccept = "Accept";
+ public const string HttpHeaderAcceptCharset = "Accept-Charset";
+ public const string HttpHeaderAcceptEncoding = "Accept-Encoding";
+ public const string HttpHeaderAcceptLanguage = "Accept-Language";
+ public const string HttpHeaderAcceptRanges = "Accept-Ranges";
+ public const string HttpHeaderAge = "Age";
+ public const string HttpHeaderAllow = "Allow";
+ public const string HttpHeaderAuthorization = "Authorization";
+ public const string HttpHeaderCacheControl = "Cache-Control";
+ public const string HttpHeaderConnection = "Connection";
+ public const string HttpHeaderContentEncoding = "Content-Encoding";
+ public const string HttpHeaderContentLanguage = "Content-Language";
+ public const string HttpHeaderContentLength = "Content-Length";
+ public const string HttpHeaderContentLocation = "Content-Location";
+ public const string HttpHeaderContentMD5 = "Content-MD5";
+ public const string HttpHeaderContentRange = "Content-Range";
+ public const string HttpHeaderContentType = "Content-Type";
+ public const string HttpHeaderDate = "Date";
+ public const string HttpHeaderETag = "ETag";
+ public const string HttpHeaderExpect = "Expect";
+ public const string HttpHeaderExpires = "Expires";
+ public const string HttpHeaderFrom = "From";
+ public const string HttpHeaderHost = "Host";
+ public const string HttpHeaderIfMatch = "If-Match";
+ public const string HttpHeaderIfModifiedSince = "If-Modified-Since";
+ public const string HttpHeaderIfNoneMatch = "If-None-Match";
+ public const string HttpHeaderIfRange = "If-Range";
+ public const string HttpHeaderIfUnmodifiedSince = "If-Unmodified-Since";
+ public const string HttpHeaderLastModified = "Last-Modified";
+ public const string HttpHeaderLocation = "Location";
+ public const string HttpHeaderMaxForwards = "Max-Forwards";
+ public const string HttpHeaderPragma = "Pragma";
+ public const string HttpHeaderProxyAuthenticate = "Proxy-Authenticate";
+ public const string HttpHeaderProxyAuthorization = "Proxy-Authorization";
+ public const string HttpHeaderRange = "Range";
+ public const string HttpHeaderReferer = "Referer";
+ public const string HttpHeaderRetryAfter = "Retry-After";
+ public const string HttpHeaderServer = "Server";
+ public const string HttpHeaderTE = "TE";
+ public const string HttpHeaderTrailer = "Trailer";
+ public const string HttpHeaderTransferEncoding = "Transfer-Encoding";
+ public const string HttpHeaderUpgrade = "Upgrade";
+ public const string HttpHeaderUserAgent = "User-Agent";
+ public const string HttpHeaderVary = "Vary";
+ public const string HttpHeaderVia = "Via";
+ public const string HttpHeaderWarning = "Warning";
+ public const string HttpHeaderWWWAuthenticate = "WWW-Authenticate";
+
+ ///
+ /// Supported authentication schemes
+ ///
+
+ public const string AS_BASIC = "Basic";
+ public const string AS_DIGEST = "Digest";
+
+ /// Supported Digest algorithms
+
+ public const string Digest_MD5 = "MD5"; // assumedd efault if omitted
+ public const string Digest_MD5Sess = "MD5-sess";
+
+ public const string Qop_Auth = "auth";
+ public const string Qop_Int = "auth-int";
+
+ /// Utility routines
+
+ public static string StringToBase64(string str)
+ {
+ try
+ {
+ byte[] encData_byte = new byte[str.Length];
+ encData_byte = Encoding.UTF8.GetBytes(str);
+ return Convert.ToBase64String(encData_byte);
+ }
+ catch
+ {
+ return String.Empty;
+ }
+ }
+
+ public static string Base64ToString(string str)
+ {
+ UTF8Encoding encoder = new UTF8Encoding();
+ Decoder utf8Decode = encoder.GetDecoder();
+ try
+ {
+ byte[] todecode_byte = Convert.FromBase64String(str);
+ int charCount = utf8Decode.GetCharCount(todecode_byte, 0, todecode_byte.Length);
+ char[] decoded_char = new char[charCount];
+ utf8Decode.GetChars(todecode_byte, 0, todecode_byte.Length, decoded_char, 0);
+ return new String(decoded_char);
+ }
+ catch
+ {
+ return String.Empty;
+ }
+ }
+
+ private const string hvals = "0123456789abcdef";
+
+ public static int Hex2Int(string hex)
+ {
+ int val = 0;
+ int sum = 0;
+ string tmp = null;
+
+ if (hex != null)
+ {
+ tmp = hex.ToLower();
+ for (int i = 0; i < tmp.Length; i++)
+ {
+ val = hvals.IndexOf(tmp[i]);
+ if (val == -1)
+ break;
+ sum *= 16;
+ sum += val;
+ }
+ }
+
+ return sum;
+
+ }
+
+ public static string Int2Hex8(int val)
+ {
+ string res = String.Empty;
+ for (int i = 0; i < 8; i++)
+ {
+ res = (val % 16) + res;
+ val = val / 16;
+ }
+ return res;
+ }
+
+ public static string ToHex32(int val)
+ {
+ return String.Empty;
+ }
+
+ public static string ToHex32(string val)
+ {
+ return String.Empty;
+ }
+
+ // Nonce management
+
+ public static string NonceGenerator()
+ {
+ return StringToBase64(Guid.NewGuid().ToString());
+ }
+
+ // Dump he specified data stream;
+
+ public static void Dump(byte[] data)
+ {
+
+ char[] buffer = new char[Rest.DumpLineSize];
+ int cc = 0;
+
+ for (int i = 0; i < data.Length; i++)
+ {
+
+ if (i % Rest.DumpLineSize == 0) Console.Write("\n{0}: ",i.ToString("d8"));
+
+ if (i % 4 == 0) Console.Write(" ");
+// if (i%16 == 0) Console.Write(" ");
+
+ Console.Write("{0}",data[i].ToString("x2"));
+
+ if (data[i] < 127 && data[i] > 31)
+ buffer[i % Rest.DumpLineSize] = (char) data[i];
+ else
+ buffer[i % Rest.DumpLineSize] = '.';
+
+ cc++;
+
+ if (i != 0 && (i + 1) % Rest.DumpLineSize == 0)
+ {
+ Console.Write(" |"+(new String(buffer))+"|");
+ cc = 0;
+ }
+
+ }
+
+ // Finish off any incomplete line
+
+ if (cc != 0)
+ {
+ for (int i = cc ; i < Rest.DumpLineSize; i++)
+ {
+ if (i % 4 == 0) Console.Write(" ");
+ // if (i%16 == 0) Console.Write(" ");
+ Console.Write(" ");
+ buffer[i % Rest.DumpLineSize] = ' ';
+ }
+ Console.WriteLine(" |"+(new String(buffer))+"|");
+ }
+ else
+ {
+ Console.Write("\n");
+ }
+
+ }
+
+ }
+
+ // Local exception type
+
+ public class RestException : Exception
+ {
+
+ internal int statusCode;
+ internal string statusDesc;
+ internal string httpmethod;
+ internal string httppath;
+
+ public RestException(string msg) : base(msg)
+ {
+ }
+ }
+
+}
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs
new file mode 100644
index 0000000000..839e0f21ac
--- /dev/null
+++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs
@@ -0,0 +1,257 @@
+/*
+ * 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 libsecondlife;
+using Nini.Config;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Xml;
+using OpenSim.Framework;
+using OpenSim.Framework.Servers;
+using OpenSim.Framework.Communications;
+using OpenSim.Framework.Communications.Cache;
+
+namespace OpenSim.ApplicationPlugins.Rest.Inventory
+{
+
+ public class RestAssetServices : IRest
+ {
+
+ private string key = "assets";
+ private bool enabled = false;
+ private string qPrefix = "assets";
+
+ // A simple constructor is used to handle any once-only
+ // initialization of working classes.
+
+ public RestAssetServices(RestHandler p_rest)
+ {
+
+ Rest.Log.InfoFormat("{0} Asset services initializing", MsgId);
+ Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
+
+ // Integrate domain
+
+ if (!qPrefix.StartsWith(Rest.UrlPathSeparator))
+ {
+ qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix;
+ }
+
+ // Authentication domain
+
+ Rest.Domains.Add(key,Rest.Config.GetString("asset-domain",qPrefix));
+
+ // Register interface
+
+ Rest.Plugin.AddPathHandler(DoAsset, qPrefix, Allocate);
+
+ // Activate
+
+ enabled = true;
+
+ Rest.Log.InfoFormat("{0} Asset services initialization complete", MsgId);
+
+ }
+
+ // Post-construction, pre-enabled initialization opportunity
+ // Not currently exploited.
+
+ public void Initialize()
+ {
+ }
+
+ // Called by the plug-in to halt REST processing. Local processing is
+ // disabled, and control blocks until all current processing has
+ // completed. No new processing will be started
+
+ public void Close()
+ {
+ enabled = false;
+ Rest.Log.InfoFormat("{0} Asset services closing down", MsgId);
+ }
+
+ // Properties
+
+ internal string MsgId
+ {
+ get { return Rest.MsgId; }
+ }
+
+ #region Interface
+
+ private RequestData Allocate(OSHttpRequest request, OSHttpResponse response)
+ {
+ return (RequestData) new AssetRequestData(request, response, qPrefix);
+ }
+
+ // Asset Handler
+
+ private void DoAsset(RequestData rparm)
+ {
+
+ if (!enabled) return;
+
+ AssetRequestData rdata = (AssetRequestData) rparm;
+
+ Rest.Log.DebugFormat("{0} REST Asset handler ENTRY", MsgId);
+
+ // 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, Rest.HttpStatusDescNotAuthorized);
+ }
+ }
+ 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);
+ }
+
+ // Remove the prefix and what's left are the parameters. If we don't have
+ // the parameters we need, fail the request. Parameters do NOT include
+ // any supplied query values.
+
+ if (rdata.parameters.Length > 0)
+ {
+ switch (rdata.method)
+ {
+ case "get" :
+ DoGet(rdata);
+ break;
+ case "put" :
+ case "post" :
+ case "delete" :
+ default :
+ Rest.Log.WarnFormat("{0} Asset: Method not supported: {1}",
+ MsgId, rdata.method);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ break;
+ }
+ }
+ else
+ {
+ Rest.Log.WarnFormat("{0} Asset: No agent information provided", MsgId);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest);
+ }
+
+ Rest.Log.DebugFormat("{0} REST Asset handler EXIT", MsgId);
+
+ }
+
+ #endregion Interface
+
+ private void DoGet(AssetRequestData rdata)
+ {
+
+ bool istexture = false;
+
+ Rest.Log.DebugFormat("{0} REST Asset handler, Method = <{1}> ENTRY", MsgId, rdata.method);
+
+ // The only parameter we accept is an LLUUID for
+ // the asset
+
+ if (rdata.parameters.Length == 1)
+ {
+
+ LLUUID uuid = new LLUUID(rdata.parameters[0]);
+ AssetBase asset = Rest.AssetServices.GetAsset(uuid, istexture);
+
+ if (asset != null)
+ {
+
+ Rest.Log.DebugFormat("{0} Asset located <{1}>", MsgId, rdata.parameters[0]);
+
+ rdata.initXmlWriter();
+
+ rdata.writer.WriteStartElement(String.Empty,"Asset",String.Empty);
+
+ rdata.writer.WriteAttributeString("id", asset.ID.ToString());
+ rdata.writer.WriteAttributeString("name", asset.Name);
+ rdata.writer.WriteAttributeString("desc", asset.Description);
+ rdata.writer.WriteAttributeString("type", asset.Type.ToString());
+ rdata.writer.WriteAttributeString("invtype", asset.InvType.ToString());
+ rdata.writer.WriteAttributeString("local", asset.Local.ToString());
+ rdata.writer.WriteAttributeString("temporary", asset.Temporary.ToString());
+
+ rdata.writer.WriteBase64(asset.Data,0,asset.Data.Length);
+
+ rdata.writer.WriteFullEndElement();
+
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} Invalid parameters: <{1}>", MsgId, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeNotFound,
+ Rest.HttpStatusDescNotFound);
+ }
+ }
+
+ rdata.Complete();
+ rdata.Respond("Asset " + rdata.method + ": Normal completion");
+
+ }
+
+ internal class AssetRequestData : RequestData
+ {
+ internal AssetRequestData(OSHttpRequest request, OSHttpResponse response, string prefix)
+ : base(request, response, prefix)
+ {
+ }
+ }
+
+ }
+}
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs
new file mode 100644
index 0000000000..0a0bf3f44e
--- /dev/null
+++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs
@@ -0,0 +1,547 @@
+/*
+ * 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.Generic;
+using System.Reflection;
+using OpenSim.Framework;
+using OpenSim.Framework.Servers;
+using OpenSim.ApplicationPlugins.Rest;
+using Mono.Addins;
+
+[assembly : Addin]
+[assembly : AddinDependency("OpenSim", "0.5")]
+
+namespace OpenSim.ApplicationPlugins.Rest.Inventory
+{
+
+ [Extension("/OpenSim/Startup")]
+
+ public class RestHandler : RestPlugin, IHttpAgentHandler
+ {
+
+ #region local static state
+
+ ///
+ /// This static initializer scans the assembly for classes that
+ /// export the IRest interface and builds a list of them. These
+ /// are later activated by the handler. To add a new handler it
+ /// is only necessary to create a new services class that implements
+ /// the IRest interface, and recompile the handler. This gives
+ /// all of the build-time flexibility of a modular approach
+ /// while not introducing yet-another module loader. Note that
+ /// multiple assembles can still be built, each with its own set
+ /// of handlers.
+ ///
+
+ private static bool handlersLoaded = false;
+ private static List classes = new List();
+ private static List handlers = new List();
+ private static Type[] parms = new Type[1];
+ private static Object[] args = new Object[1];
+
+ static RestHandler()
+ {
+ Module[] mods = Assembly.GetExecutingAssembly().GetModules();
+ foreach (Module m in mods)
+ {
+ Type[] types = m.GetTypes();
+ foreach (Type t in types)
+ {
+ if (t.GetInterface("IRest") != null)
+ {
+ classes.Add(t);
+ }
+ }
+ }
+ }
+
+ #endregion local static state
+
+ #region local instance state
+
+ ///
+ /// The handler delegate is not noteworthy. The allocator allows
+ /// a given handler to optionally subclass the base RequestData
+ /// structure to carry any locally required per-request state
+ /// needed.
+ ///
+ internal delegate void RestMethodHandler(RequestData rdata);
+ internal delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response);
+
+ // Handler tables: both stream and REST are supported
+
+ internal Dictionary pathHandlers = new Dictionary();
+ internal Dictionary pathAllocators = new Dictionary();
+ internal Dictionary streamHandlers = new Dictionary();
+
+ ///
+ /// This routine loads all of the handlers discovered during
+ /// instance initialization. Each handler is responsible for
+ /// registering itself with this handler.
+ /// I was not able to make this code work in a constructor.
+ ///
+ private void LoadHandlers()
+ {
+ lock(handlers)
+ {
+ if (!handlersLoaded)
+ {
+ parms[0] = this.GetType();
+ args[0] = this;
+
+ ConstructorInfo ci;
+ Object ht;
+
+ foreach (Type t in classes)
+ {
+ ci = t.GetConstructor(parms);
+ ht = ci.Invoke(args);
+ handlers.Add((IRest)ht);
+ }
+ handlersLoaded = true;
+ }
+ }
+ }
+
+ #endregion local instance state
+
+ #region overriding properties
+
+ // Used to differentiate the message header.
+
+ public override string Name
+ {
+ get { return "HANDLER"; }
+ }
+
+ // Used to partition the configuration space.
+
+ public override string ConfigName
+ {
+ get { return "RestHandler"; }
+ }
+
+ // We have to rename these because we want
+ // to be able to share the values with other
+ // classes in our assembly and the base
+ // names are protected.
+
+ internal string MsgId
+ {
+ get { return base.MsgID; }
+ }
+
+ internal string RequestId
+ {
+ get { return base.RequestID; }
+ }
+
+ #endregion overriding properties
+
+ #region overriding methods
+
+ ///
+ /// This method is called by OpenSimMain immediately after loading the
+ /// plugin and after basic server setup, but before running any server commands.
+ ///
+ ///
+ /// Note that entries MUST be added to the active configuration files before
+ /// the plugin can be enabled.
+ ///
+ public override void Initialise(OpenSimBase openSim)
+ {
+ try
+ {
+
+ ///
+ /// This plugin will only be enabled if the broader
+ /// REST plugin mechanism is enabled.
+ ///
+
+ Rest.Log.InfoFormat("{0} Plugin is initializing", MsgID);
+
+ base.Initialise(openSim);
+
+ if (!IsEnabled)
+ {
+ Rest.Log.WarnFormat("{0} Plugins are disabled", MsgID);
+ return;
+ }
+
+ Rest.Log.InfoFormat("{0} Plugin will be enabled", MsgID);
+
+ ///
+ /// These are stored in static variables to make
+ /// them easy to reach from anywhere in the assembly.
+ ///
+
+ Rest.main = openSim;
+ Rest.Plugin = this;
+ Rest.Comms = App.CommunicationsManager;
+ Rest.UserServices = Rest.Comms.UserService;
+ Rest.InventoryServices = Rest.Comms.InventoryService;
+ Rest.AssetServices = Rest.Comms.AssetCache;
+ Rest.Config = Config;
+ Rest.Prefix = Prefix;
+ Rest.GodKey = GodKey;
+
+ Rest.Authenticate = Rest.Config.GetBoolean("authenticate",true);
+ Rest.Secure = Rest.Config.GetBoolean("secured",true);
+ Rest.ExtendedEscape = Rest.Config.GetBoolean("extended-escape",true);
+ Rest.Realm = Rest.Config.GetString("realm","OpenSim REST");
+ Rest.DumpAsset = Rest.Config.GetBoolean("dump-asset",false);
+ Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size",32);
+
+ Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId,
+ (Rest.Authenticate ? "" : "not "));
+
+ Rest.Log.InfoFormat("{0} Security is {1}enabled", MsgId,
+ (Rest.Authenticate ? "" : "not "));
+
+ Rest.Log.InfoFormat("{0} Extended URI escape processing is {1}enabled", MsgId,
+ (Rest.ExtendedEscape ? "" : "not "));
+
+ Rest.Log.InfoFormat("{0} Dumping of asset data is {1}enabled", MsgId,
+ (Rest.DumpAsset ? "" : "not "));
+
+ if (Rest.DumpAsset)
+ {
+ Rest.Log.InfoFormat("{0} Dump {1} bytes per line", MsgId,
+ Rest.DumpLineSize);
+ }
+
+ // Load all of the handlers present in the
+ // assembly
+
+ // In principle, as we're an application plug-in,
+ // most of what needs to be done could be done using
+ // static resources, however the Open Sim plug-in
+ // model makes this an instance, so that's what we
+ // need to be.
+ // There is only one Communications manager per
+ // server, and by inference, only one each of the
+ // user, asset, and inventory servers. So we can cache
+ // those using a static initializer.
+ // We move all of this processing off to another
+ // services class to minimize overlap between function
+ // and infrastructure.
+
+ LoadHandlers();
+
+ ///
+ /// The intention of a post construction initializer
+ /// is to allow for setup that is dependent upon other
+ /// activities outside of the agency. We don't currently
+ /// have any, but the design allows for it.
+ ///
+
+ foreach (IRest handler in handlers)
+ {
+ handler.Initialize();
+ }
+
+ ///
+ /// Now that everything is setup we can proceed and
+ /// add this agent to the HTTP server's handler list
+ ///
+
+ if (!AddAgentHandler(Rest.Name,this))
+ {
+ Rest.Log.ErrorFormat("{0} Unable to activate handler interface", MsgId);
+ foreach (IRest handler in handlers)
+ {
+ handler.Close();
+ }
+ }
+
+ }
+ catch (Exception e)
+ {
+ Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgID, e.Message);
+ }
+
+ }
+
+ ///
+ /// In the interests of efficiency, and because we cannot determine whether
+ /// or not this instance will actually be harvested, we clobber the only
+ /// anchoring reference to the working state for this plug-in. What the
+ /// call to close does is irrelevant to this class beyond knowing that it
+ /// can nullify the reference when it returns.
+ /// To make sure everything is copacetic we make sure the primary interface
+ /// is disabled by deleting the handler from the HTTP server tables.
+ ///
+ public override void Close()
+ {
+
+ Rest.Log.InfoFormat("{0} Plugin is terminating", MsgID);
+
+ try
+ {
+ RemoveAgentHandler(Rest.Name, this);
+ }
+ catch (KeyNotFoundException){}
+
+ foreach (IRest handler in handlers)
+ {
+ handler.Close();
+ }
+
+ }
+
+ #endregion overriding methods
+
+ #region interface methods
+
+ ///
+ /// This method is called by the server to match the client, it could
+ /// just return true if we only want one such handler. For now we
+ /// match any explicitly specified client.
+ ///
+ public bool Match(OSHttpRequest request, OSHttpResponse response)
+ {
+ string path = request.RawUrl;
+ foreach (string key in pathHandlers.Keys)
+ {
+ if (path.StartsWith(key))
+ {
+ return ( path.Length == key.Length ||
+ path.Substring(key.Length,1) == Rest.UrlPathSeparator);
+ }
+ }
+
+ path = String.Format("{0}{1}{2}", request.HttpMethod, Rest.UrlMethodSeparator, path);
+ foreach (string key in streamHandlers.Keys)
+ {
+ if (path.StartsWith(key))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Preconditions:
+ /// [1] request != null and is a valid request object
+ /// [2] response != null and is a valid response object
+ /// Behavior is undefined if preconditions are not satisfied.
+ ///
+ public bool Handle(OSHttpRequest request, OSHttpResponse response)
+ {
+ bool handled;
+ base.MsgID = base.RequestID;
+
+ if (Rest.DEBUG)
+ {
+ Rest.Log.DebugFormat("{0} ENTRY", MsgId);
+ Rest.Log.DebugFormat("{0} Agent: {1}", MsgId, request.UserAgent);
+ Rest.Log.DebugFormat("{0} Method: {1}", MsgId, request.HttpMethod);
+
+ for (int i = 0; i < request.Headers.Count; i++)
+ {
+ Rest.Log.DebugFormat("{0} Header [{1}] : <{2}> = <{3}>",
+ MsgId, i, request.Headers.GetKey(i), request.Headers.Get(i));
+ }
+ Rest.Log.DebugFormat("{0} URI: {1}", MsgId, request.RawUrl);
+ }
+
+ // If a path handler worked we're done, otherwise try any
+ // available stream handlers too.
+
+ try
+ {
+ handled = FindPathHandler(request, response) ||
+ FindStreamHandler(request, response);
+ }
+ catch (Exception e)
+ {
+ // A raw exception indicates that something we weren't expecting has
+ // happened. This should always reflect a shortcoming in the plugin,
+ // or a failure to satisfy the preconditions.
+ Rest.Log.ErrorFormat("{0} Plugin error: {1}", MsgId, e.Message);
+ handled = true;
+ }
+
+ Rest.Log.DebugFormat("{0} EXIT", MsgId);
+
+ return handled;
+
+ }
+
+ #endregion interface methods
+
+ ///
+ /// If there is a stream handler registered that can handle the
+ /// request, then fine. If the request is not matched, do
+ /// nothing.
+ ///
+
+ private bool FindStreamHandler(OSHttpRequest request, OSHttpResponse response)
+ {
+ RequestData rdata = new RequestData(request, response, String.Empty);
+
+ string bestMatch = null;
+ string path = String.Format("{0}:{1}", rdata.method, rdata.path);
+
+ Rest.Log.DebugFormat("{0} Checking for stream handler for <{1}>", MsgId, path);
+
+ foreach (string pattern in streamHandlers.Keys)
+ {
+ if (path.StartsWith(pattern))
+ {
+ if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length)
+ {
+ bestMatch = pattern;
+ }
+ }
+ }
+
+ // Handle using the best match available
+
+ if (!String.IsNullOrEmpty(bestMatch))
+ {
+ Rest.Log.DebugFormat("{0} Stream-based handler matched with <{1}>", MsgId, bestMatch);
+ RestStreamHandler handler = streamHandlers[bestMatch];
+ rdata.buffer = handler.Handle(rdata.path, rdata.request.InputStream, rdata.request, rdata.response);
+ rdata.AddHeader(rdata.response.ContentType,handler.ContentType);
+ rdata.Respond("FindStreamHandler Completion");
+ }
+
+ return rdata.handled;
+
+ }
+
+ // Preserves the original handler's semantics
+
+ public new void AddStreamHandler(string httpMethod, string path, RestMethod method)
+ {
+
+ if (!IsEnabled)
+ {
+ return;
+ }
+
+ if (!path.StartsWith(Rest.Prefix))
+ {
+ path = String.Format("{0}{1}", Rest.Prefix, path);
+ }
+
+ path = String.Format("{0}{1}{2}", httpMethod, Rest.UrlMethodSeparator, path);
+
+ // Conditionally add to the list
+
+ if (!streamHandlers.ContainsKey(path))
+ {
+ streamHandlers.Add(path, new RestStreamHandler(httpMethod, path, method));
+ Rest.Log.DebugFormat("{0} Added handler for {1}", MsgID, path);
+ }
+ else
+ {
+ Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgID, path);
+ }
+
+ }
+
+
+ internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response)
+ {
+
+ RequestData rdata = null;
+ string bestMatch = null;
+
+ if (!IsEnabled)
+ {
+ return false;
+ }
+
+ // Conditionally add to the list
+
+ Rest.Log.DebugFormat("{0} Checking for path handler for <{1}>", MsgId, request.RawUrl);
+
+ foreach (string pattern in pathHandlers.Keys)
+ {
+ if (request.RawUrl.StartsWith(pattern))
+ {
+ if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length)
+ {
+ bestMatch = pattern;
+ }
+ }
+ }
+
+ if (!String.IsNullOrEmpty(bestMatch))
+ {
+
+ rdata = pathAllocators[bestMatch](request, response);
+
+ Rest.Log.DebugFormat("{0} Path based REST handler matched with <{1}>", MsgId, bestMatch);
+
+ try
+ {
+ pathHandlers[bestMatch](rdata);
+ }
+
+ // A plugin generated error indicates a request-related error
+ // that has been handled by the plugin.
+
+ catch (RestException r)
+ {
+ Rest.Log.WarnFormat("{0} Request failed: {1}", MsgId, r.Message);
+ }
+
+ }
+
+ return (rdata == null) ? false : rdata.handled;
+
+ }
+
+ internal void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra)
+ {
+ if (pathHandlers.ContainsKey(path))
+ {
+ Rest.Log.DebugFormat("{0} Replacing handler for <${1}>", MsgId, path);
+ pathHandlers.Remove(path);
+ }
+
+ if (pathAllocators.ContainsKey(path))
+ {
+ Rest.Log.DebugFormat("{0} Replacing allocator for <${1}>", MsgId, path);
+ pathAllocators.Remove(path);
+ }
+
+ Rest.Log.DebugFormat("{0} Adding path handler for {1}", MsgId, path);
+
+ pathHandlers.Add(path, mh);
+ pathAllocators.Add(path, ra);
+
+ }
+ }
+}
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs
new file mode 100644
index 0000000000..0fc10f9a5b
--- /dev/null
+++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs
@@ -0,0 +1,1993 @@
+/*
+ * 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.Generic;
+using System.IO;
+using System.Threading;
+using System.Xml;
+using OpenSim.Framework;
+using OpenSim.Framework.Servers;
+using OpenSim.Framework.Communications;
+using OpenSim.Framework.Communications.Cache;
+using libsecondlife;
+using Nini.Config;
+
+namespace OpenSim.ApplicationPlugins.Rest.Inventory
+{
+
+ public class RestInventoryServices : IRest
+ {
+
+ private string key = "inventory";
+ private bool enabled = false;
+ private string qPrefix = "inventory";
+
+ // A simple constructor is used to handle any once-only
+ // initialization of working classes.
+
+ public RestInventoryServices(RestHandler p_rest)
+ {
+
+ Rest.Log.InfoFormat("{0} Inventory services initializing", MsgId);
+ Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
+
+ // Update to reflect the full prefix if not absolute
+
+ if (!qPrefix.StartsWith(Rest.UrlPathSeparator))
+ {
+ qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix;
+ }
+
+ // Authentication domain
+
+ Rest.Domains.Add(key, Rest.Config.GetString("inventory-domain",qPrefix));
+
+ // Register interface
+
+ Rest.Plugin.AddPathHandler(DoInventory,qPrefix,Allocate);
+
+ // Activate
+
+ enabled = true;
+
+ Rest.Log.InfoFormat("{0} Inventory services initialization complete", MsgId);
+
+ }
+
+ // Post-construction, pre-enabled initialization opportunity
+ // Not currently exploited.
+
+ public void Initialize()
+ {
+ }
+
+ // Called by the plug-in to halt REST processing. Local processing is
+ // disabled, and control blocks until all current processing has
+ // completed. No new processing will be started
+
+ public void Close()
+ {
+ enabled = false;
+ Rest.Log.InfoFormat("{0} Inventory services closing down", MsgId);
+ }
+
+ // Convenient properties
+
+ internal string MsgId
+ {
+ get { return Rest.MsgId; }
+ }
+
+ #region Interface
+
+ private RequestData Allocate(OSHttpRequest request, OSHttpResponse response)
+ {
+ return (RequestData) new InventoryRequestData(request, response, qPrefix);
+ }
+
+ ///
+ /// This method is registered with the handler when this class is
+ /// initialized. It is called whenever the URI includes this handler's
+ /// prefix string.
+ /// It handles all aspects of inventory REST processing.
+ ///
+
+ private void DoInventory(RequestData hdata)
+ {
+
+ InventoryRequestData rdata = (InventoryRequestData) hdata;
+
+ Rest.Log.DebugFormat("{0} DoInventory ENTRY", MsgId);
+
+ // We're disabled
+ 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, Rest.HttpStatusDescNotAuthorized);
+ }
+ }
+ 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're authorized
+ //
+ // The requestor may have specified an LLUUID or
+ // a conjoined FirstNameLastName string. We'll
+ // try both. If we fail with the first, UUID,
+ // attempt, then we need two nodes to construct
+ // a valid avatar name.
+
+ // Do we have at least a user agent name?
+
+ if (rdata.parameters.Length < 1)
+ {
+ Rest.Log.WarnFormat("{0} Inventory: No user agent identifier specified", MsgId);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest);
+ }
+
+ // The next parameter MUST be the agent identification, either an LLUUID
+ // or a space-separated First-name Last-Name specification.
+
+ try
+ {
+ rdata.uuid = new LLUUID(rdata.parameters[0]);
+ Rest.Log.DebugFormat("{0} LLUUID supplied", MsgId);
+ rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid);
+ }
+ catch
+ {
+ string[] names = rdata.parameters[0].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.DebugFormat("{0} A Valid UUID or both first and last names must be specified", MsgId);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest);
+ }
+ }
+
+ if (rdata.userProfile != null)
+ {
+ Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}",
+ MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} No profile for {1}", MsgId, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound);
+ }
+
+ // If we get to here, then we have successfully obtained an inventory
+ // for the specified user.
+
+ rdata.uuid = rdata.userProfile.ID;
+
+ if (Rest.InventoryServices.HasInventoryForUser(rdata.uuid))
+ {
+
+ rdata.root = Rest.InventoryServices.RequestRootFolder(rdata.uuid);
+
+ Rest.Log.DebugFormat("{0} Inventory Root retrieved for {1} {2}",
+ MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
+
+ Rest.InventoryServices.RequestInventoryForUser(rdata.uuid, rdata.GetUserInventory);
+
+ Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}",
+ MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
+
+ lock(rdata)
+ {
+ if (!rdata.HaveInventory)
+ {
+ Monitor.Wait(rdata);
+ }
+ }
+
+ if (rdata.root == null)
+ {
+ Rest.Log.DebugFormat("{0} Inventory is not available [1] for agent {1} {2}",
+ MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
+ rdata.Fail(Rest.HttpStatusCodeServerError,Rest.HttpStatusDescServerError);
+ }
+
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} Inventory is not available for agent [3] {1} {2}",
+ MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
+ rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound);
+ }
+
+ // If we get here, then we have successfully retrieved the user's information
+ // and inventory information is now available locally.
+
+ 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 : // Add new information
+ DoPut(rdata);
+ break;
+
+ case Rest.POST : // Update (replace)
+ DoPost(rdata);
+ break;
+
+ case Rest.DELETE : // Delete information
+ DoDelete(rdata);
+ break;
+
+ default :
+ Rest.Log.DebugFormat("{0} Method {1} not supported for {2}",
+ MsgId, rdata.method, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed,
+ Rest.HttpStatusDescMethodNotAllowed);
+ break;
+ }
+
+ }
+
+ #endregion Interface
+
+ #region method-specific processing
+
+ ///
+ /// This method implements GET processing for inventory.
+ /// Any remaining parameters are used to locate the
+ /// corresponding subtree based upon node name.
+ ///
+
+ private void DoGet(InventoryRequestData rdata)
+ {
+
+ rdata.initXmlWriter();
+
+ rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty);
+
+ if (rdata.parameters.Length == 1)
+ {
+ formatInventory(rdata, rdata.root, String.Empty);
+ }
+ else
+ {
+ traverseInventory(rdata, rdata.root, 1);
+ }
+
+ rdata.writer.WriteFullEndElement();
+
+ rdata.Complete();
+ rdata.Respond("Inventory " + rdata.method + ": Normal completion");
+
+ }
+
+ ///
+ /// In the case of the inventory, and probably much else
+ /// the distinction between PUT and POST is not always
+ /// easy to discern. Adding a directory can be viewed as
+ /// an addition, or as a modification to the inventory as
+ /// a whole.
+ ///
+ /// The best distinction may be the relationship between
+ /// the entity and the URI. If we view POST as an update,
+ /// then the enity represents a replacement for the
+ /// element named by the URI. If the operation is PUT,
+ /// then the URI describes the context into which the
+ /// entity will be added.
+ ///
+ /// As an example, suppose the URI contains:
+ /// /admin/inventory/Clothing
+ /// Suppose the entity represents a Folder, called
+ /// "Clothes".
+ ///
+ /// A POST request will result in the replacement of
+ /// "Clothing" by "Clothes". Whereas a PUT request
+ /// would add Clothes as a sub-directory of Clothing.
+ ///
+ /// This is the model followed by this implementation.
+ ///
+
+ ///
+ /// PUT adds new information to the inventory at the
+ /// context identified by the URI.
+ ///
+
+ private void DoPut(InventoryRequestData rdata)
+ {
+
+ // Resolve the context node specified in the URI. Entity
+ // data will be ADDED beneath this node.
+
+ Object InventoryNode = getInventoryNode(rdata, rdata.root, 1);
+
+ // Processing depends upon the type of inventory node
+ // identified in the URI. This is the CONTEXT for the
+ // change. We either got a context or we threw an
+ // exception.
+
+ // It follows that we can only add information if the URI
+ // has identified a folder. So only folder is supported
+ // in this case.
+
+ if (typeof(InventoryFolderBase) == InventoryNode.GetType() ||
+ typeof(InventoryFolderImpl) == InventoryNode.GetType())
+ {
+
+ // Cast the context node appropriately.
+
+ InventoryFolderBase context = (InventoryFolderBase) InventoryNode;
+
+ Rest.Log.DebugFormat("{0} {1}: Resource(s) will be added to folder {2}",
+ MsgId, rdata.method, rdata.path);
+
+ // Reconstitute inventory sub-tree from the XML supplied in the entity.
+ // This is a stand-alone inventory subtree, not yet integrated into the
+ // existing tree.
+
+ XmlInventoryCollection entity = ReconstituteEntity(rdata);
+
+ // Inlined assest included in entity. If anything fails,
+ // return failure to requestor.
+
+ if (entity.Assets.Count > 0)
+ {
+
+ Rest.Log.DebugFormat("{0} Adding {1} assets to server",
+ MsgId, entity.Assets.Count);
+
+ foreach (AssetBase asset in entity.Assets)
+ {
+ Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}",
+ MsgId, asset.ID, asset.Type, asset.Name);
+ Rest.AssetServices.AddAsset(asset);
+ if (Rest.DumpAsset)
+ {
+ Rest.Dump(asset.Data);
+ }
+ }
+
+ }
+
+ // Modify the context using the collection of folders and items
+ // returned in the XmlInventoryCollection.
+
+ foreach (InventoryFolderBase folder in entity.Folders)
+ {
+
+ InventoryFolderBase found = null;
+
+ // If the parentID is zero, then this is going
+ // into the root identified by the URI. The requestor
+ // may have already set the parent ID correctly, in which
+ // case we don't have to do it here.
+
+ if (folder.ParentID == LLUUID.Zero)
+ {
+ folder.ParentID = context.ID;
+ }
+
+ // Search the existing inventory for an existing entry. If
+ // we have once, we need to decide if it has really changed.
+ // It could just be present as (unnecessary) context, and we
+ // don't want to waste time updating the database in that
+ // case, OR, it could be being moved from another location
+ // in which case an update is most certainly necessary.
+
+ found = null;
+
+ foreach (InventoryFolderBase xf in rdata.folders)
+ {
+ // Compare identifying attribute
+ if (xf.ID == folder.ID)
+ {
+ found = xf;
+ }
+ }
+
+ if (found != null && FolderHasChanged(folder,found))
+ {
+ Rest.Log.DebugFormat("{0} Updating existing folder", MsgId);
+ Rest.InventoryServices.MoveFolder(folder);
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} Adding new folder", MsgId);
+ Rest.InventoryServices.AddFolder(folder);
+ }
+
+ }
+
+ // Now we repeat a similar process for the items included
+ // in the entity.
+
+ foreach (InventoryItemBase item in entity.Items)
+ {
+
+ InventoryItemBase found = null;
+
+ // If the parentID is zero, then this is going
+ // directly into the root identified by the URI.
+
+ if (item.Folder == LLUUID.Zero)
+ {
+ item.Folder = context.ID;
+ }
+
+ // Determine whether this is a new item or a
+ // replacement definition.
+
+ foreach (InventoryItemBase xi in rdata.items)
+ {
+ // Compare identifying attribute
+ if (xi.ID == item.ID)
+ {
+ found = xi;
+ }
+ }
+
+ if (found != null && ItemHasChanged(item, found))
+ {
+ Rest.Log.DebugFormat("{0} Updating item {1} {2} {3} {4} {5}",
+ MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name);
+ Rest.InventoryServices.UpdateItem(item);
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} Adding item {1} {2} {3} {4} {5}",
+ MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name);
+ Rest.InventoryServices.AddItem(item);
+ }
+
+ }
+
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} {1}: Resource {2} is not a valid context: {3}",
+ MsgId, rdata.method, rdata.path, InventoryNode.GetType());
+ rdata.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ rdata.Complete();
+ rdata.Respond("Inventory " + rdata.method + ": Normal completion");
+
+ }
+
+ ///
+ /// POST updates the URI-identified element in the inventory. This
+ /// is actually far more flexible than it might at first sound. For
+ /// POST the URI serves two purposes:
+ /// [1] It identifies the user whose inventory is to be
+ /// processed.
+ /// [2] It optionally specifies a subtree of the inventory
+ /// that is to be used to resolve an relative subtree
+ /// specifications in the entity. If nothing is specified
+ /// then the whole inventory is implied.
+ /// Please note that the subtree specified by the URI is only relevant
+ /// to an entity containing a URI relative specification, i.e. one or
+ /// more elements do not specify parent folder information. These
+ /// elements will be implicitly referenced within the context identified
+ /// by the URI.
+ /// If an element in the entity specifies an explicit parent folder, then
+ /// that parent is effective, regardless of nay value specified in the
+ /// URI. If the parent does not exist, then the element, and any dependent
+ /// elements, are ignored. This case is actually detected and handled
+ /// during the reconstitution process.
+ ///
+
+ private void DoPost(InventoryRequestData rdata)
+ {
+
+ int count = 0;
+
+ // Resolve the inventory node that is to be modified.
+
+ Object InventoryNode = getInventoryNode(rdata, rdata.root, 1);
+
+ // As long as we have a context, then we have something
+ // meaningful to do, unlike PUT. So reconstitute the
+ // subtree before doing anything else. Note that we
+ // etiher got a context or we threw an exception.
+
+ XmlInventoryCollection entity = ReconstituteEntity(rdata);
+
+ // Incorporate any inlined assets first
+
+ if (entity.Assets.Count != 0)
+ {
+ foreach (AssetBase asset in entity.Assets)
+ {
+ // Asset was validated during the collection
+ // process
+ Rest.AssetServices.AddAsset(asset);
+ }
+ }
+
+ ///
+ /// URI specifies a folder to be updated.
+ ///
+ ///
+ /// The root node in the entity must have the same
+ /// UUID as the node identified by the URI. The
+ /// parentID if different indicates that the updated
+ /// folder is actually being moved too.
+ ///
+
+ if (typeof(InventoryFolderBase) == InventoryNode.GetType() ||
+ typeof(InventoryFolderImpl) == InventoryNode.GetType())
+ {
+
+ InventoryFolderBase uri = (InventoryFolderBase) InventoryNode;
+ InventoryFolderBase xml = null;
+
+ // Scan the set of folders in the entity collection for an
+ // entry that macthes the context folder. It is assumed that
+ // the only reliable indicator of this is a zero UUID ( using
+ // implicit context), or the parent's UUID matches that of the
+ // URI designated node (explicit context). We don't allow
+ // ambiguity in this case because this is POST and we are
+ // supposed to be modifying a specific node.
+ // We assign any element IDs required as an economy; we don't
+ // want to iterate over the fodler set again if it can be
+ // helped.
+
+ foreach (InventoryFolderBase folder in entity.Folders)
+ {
+ if (folder.ParentID == uri.ParentID ||
+ folder.ParentID == LLUUID.Zero)
+ {
+ folder.ParentID = uri.ParentID;
+ xml = folder;
+ count++;
+ }
+ if (xml.ID == LLUUID.Zero)
+ {
+ xml.ID = LLUUID.Random();
+ }
+ }
+
+ // More than one entry is ambiguous
+
+ if (count > 1)
+ {
+ Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous",
+ MsgId, rdata.method, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ // Exactly one entry means we ARE replacing the node
+ // identified by the URI. So we delete the old folder
+ // by moving it to the trash and then purging it.
+ // We then add all of the folders and items we
+ // included in the entity. The subtree has been
+ // modified.
+
+ if (count == 1)
+ {
+
+ InventoryFolderBase TrashCan = GetTrashCan(rdata);
+
+ uri.ParentID = TrashCan.ID;
+ Rest.InventoryServices.MoveFolder(uri);
+ Rest.InventoryServices.PurgeFolder(TrashCan);
+
+ }
+
+ // Now, regardelss of what they represent, we
+ // integrate all of the elements in the entity.
+
+ foreach (InventoryFolderBase f in entity.Folders)
+ {
+ Rest.InventoryServices.MoveFolder(f);
+ }
+
+ foreach (InventoryItemBase it in entity.Items)
+ {
+ Rest.InventoryServices.AddItem(it);
+ }
+
+ }
+
+ ///
+ /// URI specifies an item to be updated
+ ///
+ ///
+ /// The entity must contain a single item node to be
+ /// updated. ID and Folder ID must be correct.
+ ///
+
+ else
+ {
+
+ InventoryItemBase uri = (InventoryItemBase) InventoryNode;
+ InventoryItemBase xml = null;
+
+ if (entity.Folders.Count != 0)
+ {
+ Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>",
+ MsgId, rdata.method, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ if (entity.Items.Count > 1)
+ {
+ Rest.Log.DebugFormat("{0} {1}: Entity contains too many items <{2}>",
+ MsgId, rdata.method, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ xml = entity.Items[0];
+
+ if (xml.ID == LLUUID.Zero)
+ {
+ xml.ID = LLUUID.Random();
+ }
+
+ // If the folder reference has changed, then this item is
+ // being moved. Otherwise we'll just delete the old, and
+ // add in the new.
+
+ // Delete the old item
+
+ Rest.InventoryServices.DeleteItem(uri);
+
+ // Add the new item to the inventory
+
+ Rest.InventoryServices.AddItem(xml);
+
+ }
+
+ rdata.Complete();
+ rdata.Respond("Inventory " + rdata.method + ": Normal completion");
+
+ }
+
+ ///
+ /// Arguably the most damaging REST interface. It deletes the inventory
+ /// item or folder identified by the URI.
+ ///
+ /// We only process if the URI identified node appears to exist
+ /// We do not test for success because we either get a context,
+ /// or an exception is thrown.
+ ///
+ /// Folders are deleted by moving them to another folder and then
+ /// purging that folder. We'll do that by creating a temporary
+ /// sub-folder in the TrashCan and purging that folder's
+ /// contents. If we can't can it, we don't delete it...
+ /// So, if no trashcan is available, the request does nothing.
+ /// Items are summarily deleted.
+ ///
+ /// In the interests of safety, a delete request should normally
+ /// be performed using UUID, as a name might identify several
+ /// elements.
+ ///
+
+ private void DoDelete(InventoryRequestData rdata)
+ {
+
+ Object InventoryNode = getInventoryNode(rdata, rdata.root, 1);
+
+ if (typeof(InventoryFolderBase) == InventoryNode.GetType() ||
+ typeof(InventoryFolderImpl) == InventoryNode.GetType())
+ {
+
+ InventoryFolderBase TrashCan = GetTrashCan(rdata);
+
+ InventoryFolderBase folder = (InventoryFolderBase) InventoryNode;
+ Rest.Log.DebugFormat("{0} {1}: Folder {2} will be deleted",
+ MsgId, rdata.method, rdata.path);
+ folder.ParentID = TrashCan.ID;
+ Rest.InventoryServices.MoveFolder(folder);
+ Rest.InventoryServices.PurgeFolder(TrashCan);
+
+ }
+
+ // Deleting items is much more straight forward.
+
+ else
+ {
+ InventoryItemBase item = (InventoryItemBase) InventoryNode;
+ Rest.Log.DebugFormat("{0} {1}: Item {2} will be deleted",
+ MsgId, rdata.method, rdata.path);
+ Rest.InventoryServices.DeleteItem(item);
+ }
+
+ rdata.Complete();
+ rdata.Respond("Inventory " + rdata.method + ": Normal completion");
+
+ }
+
+#endregion method-specific processing
+
+ ///
+ /// This method is called to obtain the OpenSim inventory object identified
+ /// by the supplied URI. This may be either an Item or a Folder, so a suitably
+ /// ambiguous return type is employed (Object). This method recurses as
+ /// necessary to process the designated hierarchy.
+ ///
+ /// If we reach the end of the URI then we return the contextural folder to
+ /// our caller.
+ ///
+ /// If we are not yet at the end of the URI we attempt to find a child folder
+ /// and if we succeed we recurse.
+ ///
+ /// If this is the last node, then we look to see if this is an item. If it is,
+ /// we return that item.
+ ///
+ /// Otherwise we fail the request on the ground of an invalid URI.
+ ///
+ ///
+ /// This mechanism cannot detect the case where duplicate subtrees satisfy a
+ /// request. In such a case the 1st element gets processed. If this is a
+ /// problem, then UUID should be used to identify the end-node. This is basic
+ /// premise of normal inventory processing. The name is an informational, and
+ /// not a defining, attribute.
+ ///
+ ///
+ ///
+
+ private Object getInventoryNode(InventoryRequestData rdata, InventoryFolderBase folder, int pi)
+ {
+
+ Rest.Log.DebugFormat("{0} Searching folder {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi);
+
+ // We have just run off the end of the parameter sequence
+
+ if (pi >= rdata.parameters.Length)
+ {
+ return folder;
+ }
+
+ // More names in the sequence, look for a folder that might
+ // get us there.
+
+ if (rdata.folders != null)
+ foreach (InventoryFolderBase f in rdata.folders)
+ {
+ // Look for the present node in the directory list
+ if (f.ParentID == folder.ID &&
+ (f.Name == rdata.parameters[pi] ||
+ f.ID.ToString() == rdata.parameters[pi]))
+ {
+ return getInventoryNode(rdata, f, pi+1);
+ }
+ }
+
+ // No folders that match. Perhaps this parameter identifies an item? If
+ // it does, then it MUST also be the last name in the sequence.
+
+ if (pi == rdata.parameters.Length-1)
+ {
+ if (rdata.items != null)
+ {
+ int k = 0;
+ InventoryItemBase li = null;
+ foreach (InventoryItemBase i in rdata.items)
+ {
+ if (i.Folder == folder.ID &&
+ (i.Name == rdata.parameters[pi] ||
+ i.ID.ToString() == rdata.parameters[pi]))
+ {
+ li = i;
+ k++;
+ }
+ }
+ if (k == 1)
+ {
+ return li;
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous",
+ MsgId, rdata.method, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound);
+ }
+ }
+ }
+
+ // No, so abandon the request
+
+ Rest.Log.DebugFormat("{0} {1}: Resource {2} not found",
+ MsgId, rdata.method, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound);
+
+ return null; /* Never reached */
+
+ }
+
+ ///
+ /// This routine traverse the inventory's structure until the end-point identified
+ /// in the URI is reached, the remainder of the inventory (if any) is then formatted
+ /// and returned to the requestor.
+ ///
+ /// Note that this method is only interested in those folder that match elements of
+ /// the URI supplied by the requestor, so once a match is fund, the processing does
+ /// not need to consider any further elements.
+ ///
+ /// Only the last element in the URI should identify an item.
+ ///
+
+ private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi)
+ {
+
+ Rest.Log.DebugFormat("{0} Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi);
+
+ if (rdata.folders != null)
+ {
+ foreach (InventoryFolderBase f in rdata.folders)
+ {
+ if (f.ParentID == folder.ID &&
+ (f.Name == rdata.parameters[pi] ||
+ f.ID.ToString() == rdata.parameters[pi]))
+ {
+ if (pi < rdata.parameters.Length-1)
+ {
+ traverseInventory(rdata, f, pi+1);
+ }
+ else
+ {
+ formatInventory(rdata, f, String.Empty);
+ }
+ return;
+ }
+ }
+ }
+
+ if (pi == rdata.parameters.Length-1)
+ {
+ if (rdata.items != null)
+ {
+ foreach (InventoryItemBase i in rdata.items)
+ {
+ if (i.Folder == folder.ID &&
+ (i.Name == rdata.parameters[pi] ||
+ i.ID.ToString() == rdata.parameters[pi]))
+ {
+ // Fetching an Item has a special significance. In this
+ // case we also want to fetch the associated asset.
+ // To make it interesting, we'll d this via redirection.
+ string asseturl = "http://" + rdata.hostname + ":" + rdata.port +
+ "/admin/assets" + Rest.UrlPathSeparator + i.AssetID.ToString();
+ rdata.Redirect(asseturl,Rest.PERMANENT);
+ Rest.Log.DebugFormat("{0} Never Reached");
+ }
+ }
+ }
+ }
+
+ Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>",
+ MsgId, rdata.path);
+ rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound);
+
+ }
+
+ ///
+ /// This method generates XML that describes an instance of InventoryFolderBase.
+ /// It recurses as necessary to reflect a folder hierarchy, and calls formatItem
+ /// to generate XML for any items encountered along the way.
+ /// The indentation parameter is solely for the benefit of trace record
+ /// formatting.
+ ///
+
+ private void formatInventory(InventoryRequestData rdata, InventoryFolderBase folder, string indent)
+ {
+
+ if (Rest.DEBUG)
+ {
+ Rest.Log.DebugFormat("{0} Folder : {1} {2} {3}", MsgId, folder.ID, indent, folder.Name);
+ indent += "\t";
+ }
+
+ // Start folder item
+
+ rdata.writer.WriteStartElement(String.Empty,"Folder",String.Empty);
+ rdata.writer.WriteAttributeString("name",String.Empty,folder.Name);
+ rdata.writer.WriteAttributeString("uuid",String.Empty,folder.ID.ToString());
+ rdata.writer.WriteAttributeString("owner",String.Empty,folder.Owner.ToString());
+ rdata.writer.WriteAttributeString("type",String.Empty,folder.Type.ToString());
+ rdata.writer.WriteAttributeString("version",String.Empty,folder.Version.ToString());
+
+ if (rdata.folders != null)
+ {
+ foreach (InventoryFolderBase f in rdata.folders)
+ {
+ if (f.ParentID == folder.ID)
+ {
+ formatInventory(rdata, f, indent);
+ }
+ }
+ }
+
+ if (rdata.items != null)
+ {
+ foreach (InventoryItemBase i in rdata.items)
+ {
+ if (i.Folder == folder.ID)
+ {
+ formatItem(rdata, i, indent);
+ }
+ }
+ }
+
+ // End folder item
+
+ rdata.writer.WriteEndElement();
+
+ }
+
+ ///
+ /// This method generates XML that describes an instance of InventoryItemBase.
+ ///
+
+ private void formatItem(InventoryRequestData rdata, InventoryItemBase i, string indent)
+ {
+
+ Rest.Log.DebugFormat("{0} Item : {1} {2} {3}", MsgId, i.ID, indent, i.Name);
+
+ rdata.writer.WriteStartElement(String.Empty,"Item",String.Empty);
+
+ rdata.writer.WriteAttributeString("name",String.Empty,i.Name);
+ rdata.writer.WriteAttributeString("desc",String.Empty,i.Description);
+ rdata.writer.WriteAttributeString("uuid",String.Empty,i.ID.ToString());
+ rdata.writer.WriteAttributeString("owner",String.Empty,i.Owner.ToString());
+ rdata.writer.WriteAttributeString("creator",String.Empty,i.Creator.ToString());
+ rdata.writer.WriteAttributeString("creationdate",String.Empty,i.CreationDate.ToString());
+ rdata.writer.WriteAttributeString("type",String.Empty,i.InvType.ToString());
+ rdata.writer.WriteAttributeString("assettype",String.Empty,i.AssetType.ToString());
+ rdata.writer.WriteAttributeString("groupowned",String.Empty,i.GroupOwned.ToString());
+ rdata.writer.WriteAttributeString("groupid",String.Empty,i.GroupID.ToString());
+ rdata.writer.WriteAttributeString("saletype",String.Empty,i.SaleType.ToString());
+ rdata.writer.WriteAttributeString("saleprice",String.Empty,i.SalePrice.ToString());
+ rdata.writer.WriteAttributeString("flags",String.Empty,i.Flags.ToString("X"));
+
+ rdata.writer.WriteStartElement(String.Empty,"Permissions",String.Empty);
+ rdata.writer.WriteAttributeString("current",String.Empty,i.CurrentPermissions.ToString("X"));
+ rdata.writer.WriteAttributeString("next",String.Empty,i.NextPermissions.ToString("X"));
+ rdata.writer.WriteAttributeString("everyone",String.Empty,i.EveryOnePermissions.ToString("X"));
+ rdata.writer.WriteAttributeString("base",String.Empty,i.BasePermissions.ToString("X"));
+ rdata.writer.WriteEndElement();
+
+ rdata.writer.WriteElementString("Asset",i.AssetID.ToString());
+
+ rdata.writer.WriteEndElement();
+
+ }
+
+ ///
+ /// This method creates a "trashcan" folder to support folder and item
+ /// deletions by this interface. The xisting trash folder is found and
+ /// this folder is created within it. It is called "tmp" to indicate to
+ /// the client that it is OK to delete this folder. The REST interface
+ /// will recreate the folder on an as-required basis.
+ /// If the trash can cannot be created, then by implication the request
+ /// that required it cannot be completed, and it fails accordingly.
+ ///
+
+ private InventoryFolderBase GetTrashCan(InventoryRequestData rdata)
+ {
+
+ InventoryFolderBase TrashCan = null;
+
+ foreach (InventoryFolderBase f in rdata.folders)
+ {
+ if (f.Name == "Trash")
+ {
+ foreach (InventoryFolderBase t in rdata.folders)
+ {
+ if (t.Name == "tmp")
+ {
+ TrashCan = t;
+ }
+ }
+ if (TrashCan == null)
+ {
+ TrashCan = new InventoryFolderBase();
+ TrashCan.Name = "tmp";
+ TrashCan.ID = LLUUID.Random();
+ TrashCan.Version = 1;
+ TrashCan.Type = (short) AssetType.TrashFolder;
+ TrashCan.ParentID = f.ID;
+ Rest.InventoryServices.AddFolder(TrashCan);
+ }
+ }
+ }
+
+ if (TrashCan == null)
+ {
+ Rest.Log.DebugFormat("{0} No Trash Can available", MsgId);
+ rdata.Fail(Rest.HttpStatusCodeServerError,
+ Rest.HttpStatusDescServerError);
+ }
+
+ return TrashCan;
+
+ }
+
+ ///
+ /// Make sure that an unchanged folder is not unnecessarily
+ /// processed.
+ ///
+
+ private bool FolderHasChanged(InventoryFolderBase newf, InventoryFolderBase oldf)
+ {
+ return ( newf.Name != oldf.Name
+ || newf.ParentID != oldf.ParentID
+ || newf.Owner != oldf.Owner
+ || newf.Type != oldf.Type
+ || newf.Version != oldf.Version
+ );
+ }
+
+ ///
+ /// Make sure that an unchanged item is not unnecessarily
+ /// processed.
+ ///
+
+ private bool ItemHasChanged(InventoryItemBase newf, InventoryItemBase oldf)
+ {
+ return ( newf.Name != oldf.Name
+ || newf.Folder != oldf.Description
+ || newf.Description != oldf.Description
+ || newf.Owner != oldf.Owner
+ || newf.Creator != oldf.Creator
+ || newf.AssetID != oldf.AssetID
+ || newf.GroupID != oldf.GroupID
+ || newf.GroupOwned != oldf.GroupOwned
+ || newf.InvType != oldf.InvType
+ || newf.AssetType != oldf.AssetType
+ );
+ }
+
+ ///
+ /// This method is called by PUT and POST to create an XmlInventoryCollection
+ /// instance that reflects the content of the entity supplied on the request.
+ /// Any elements in the completed collection whose UUID is zero, are
+ /// considered to be located relative to the end-point identified int he
+ /// URI. In this way, an entire sub-tree can be conveyed in a single REST
+ /// PUT or POST request.
+ ///
+ /// A new instance of XmlInventoryCollection is created and, if the request
+ /// has an entity, it is more completely initialized. thus, if no entity was
+ /// provided the collection is valid, but empty.
+ ///
+ /// The entity is then scanned and each tag is processed to produce the
+ /// appropriate inventory elements. At the end f the scan, teh XmlInventoryCollection
+ /// will reflect the subtree described by the entity.
+ ///
+ /// This is a very flexible mechanism, the entity may contain arbitrary,
+ /// discontiguous tree fragments, or may contain single element. The caller is
+ /// responsible for integrating this collection (and ensuring that any
+ /// missing parent IDs are resolved).
+ ///
+
+ internal XmlInventoryCollection ReconstituteEntity(InventoryRequestData rdata)
+ {
+
+ Rest.Log.DebugFormat("{0} Reconstituting entity", MsgId);
+
+ XmlInventoryCollection ic = new XmlInventoryCollection();
+
+ if (rdata.request.HasEntityBody)
+ {
+
+ Rest.Log.DebugFormat("{0} Entity present", MsgId);
+
+ ic.init(rdata);
+
+ try
+ {
+ while (ic.xml.Read())
+ {
+ switch (ic.xml.NodeType)
+ {
+ case XmlNodeType.Element :
+ Rest.Log.DebugFormat("{0} StartElement: <{1}>",
+ MsgId, ic.xml.Name);
+ switch (ic.xml.Name)
+ {
+ case "Folder" :
+ Rest.Log.DebugFormat("{0} Processing {1} element",
+ MsgId, ic.xml.Name);
+ CollectFolder(ic);
+ break;
+ case "Item" :
+ Rest.Log.DebugFormat("{0} Processing {1} element",
+ MsgId, ic.xml.Name);
+ CollectItem(ic);
+ break;
+ case "Asset" :
+ Rest.Log.DebugFormat("{0} Processing {1} element",
+ MsgId, ic.xml.Name);
+ CollectAsset(ic);
+ break;
+ case "Permissions" :
+ Rest.Log.DebugFormat("{0} Processing {1} element",
+ MsgId, ic.xml.Name);
+ CollectPermissions(ic);
+ break;
+ default :
+ Rest.Log.DebugFormat("{0} Ignoring {1} element",
+ MsgId, ic.xml.Name);
+ break;
+ }
+ // This stinks, but the ReadElement call above not only reads
+ // the imbedded data, but also consumes the end tag for Asset
+ // and moves the element pointer on to the containing Item's
+ // element-end, however, if there was a permissions element
+ // following, it would get us to the start of that..
+ if (ic.xml.NodeType == XmlNodeType.EndElement &&
+ ic.xml.Name == "Item")
+ {
+ Validate(ic);
+ }
+ break;
+ case XmlNodeType.EndElement :
+ switch (ic.xml.Name)
+ {
+ case "Folder" :
+ Rest.Log.DebugFormat("{0} Completing {1} element",
+ MsgId, ic.xml.Name);
+ ic.Pop();
+ break;
+ case "Item" :
+ Rest.Log.DebugFormat("{0} Completing {1} element",
+ MsgId, ic.xml.Name);
+ Validate(ic);
+ break;
+ case "Asset" :
+ Rest.Log.DebugFormat("{0} Completing {1} element",
+ MsgId, ic.xml.Name);
+ break;
+ case "Permissions" :
+ Rest.Log.DebugFormat("{0} Completing {1} element",
+ MsgId, ic.xml.Name);
+ break;
+ default :
+ Rest.Log.DebugFormat("{0} Ignoring {1} element",
+ MsgId, ic.xml.Name);
+ break;
+ }
+ break;
+ default :
+ Rest.Log.DebugFormat("{0} [0] Ignoring: <{1}>:<2>",
+ MsgId, ic.xml.NodeType, ic.xml.Value);
+ break;
+ }
+ }
+ }
+ catch (XmlException e)
+ {
+ Rest.Log.WarnFormat("{0} XML parsing error: {1}", MsgId, e.Message);
+ throw e;
+ }
+ catch (Exception e)
+ {
+ Rest.Log.WarnFormat("{0} Unexpected XML parsing error: {1}", MsgId, e.Message);
+ throw e;
+ }
+
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} Entity absent", MsgId);
+ }
+
+ if (Rest.DEBUG)
+ {
+ Rest.Log.DebugFormat("{0} Reconstituted entity", MsgId);
+ Rest.Log.DebugFormat("{0} {1} assets", MsgId, ic.Assets.Count);
+ Rest.Log.DebugFormat("{0} {1} folder", MsgId, ic.Folders.Count);
+ Rest.Log.DebugFormat("{0} {1} items", MsgId, ic.Items.Count);
+ }
+
+ return ic;
+
+ }
+
+ ///
+ /// This method creates an inventory Folder from the
+ /// information supplied in the request's entity.
+ /// A folder instance is created and initialized to reflect
+ /// default values. These values are then overridden
+ /// by information supplied in the entity.
+ /// If context was not explicitly provided, then the
+ /// appropriate ID values are determined.
+ ///
+
+ private void CollectFolder(XmlInventoryCollection ic)
+ {
+
+ Rest.Log.DebugFormat("{0} Interpret folder element", MsgId);
+
+ InventoryFolderBase result = new InventoryFolderBase();
+
+ // Default values
+
+ result.Name = String.Empty;
+ result.ID = LLUUID.Zero;
+ result.Owner = ic.UserID;
+ result.ParentID = LLUUID.Zero; // Context
+ result.Type = (short) AssetType.Folder;
+ result.Version = 1;
+
+ if (ic.xml.HasAttributes)
+ {
+ for (int i = 0; i < ic.xml.AttributeCount; i++)
+ {
+ ic.xml.MoveToAttribute(i);
+ switch (ic.xml.Name)
+ {
+ case "name" :
+ result.Name = ic.xml.Value;
+ break;
+ case "uuid" :
+ result.ID = new LLUUID(ic.xml.Value);
+ break;
+ case "parent" :
+ result.ParentID = new LLUUID(ic.xml.Value);
+ break;
+ case "owner" :
+ result.Owner = new LLUUID(ic.xml.Value);
+ break;
+ case "type" :
+ result.Type = Int16.Parse(ic.xml.Value);
+ break;
+ case "version" :
+ result.Version = UInt16.Parse(ic.xml.Value);
+ break;
+ default :
+ Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}",
+ MsgId, ic.xml.Name, ic.xml.Value);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ break;
+ }
+ }
+ }
+
+ ic.xml.MoveToElement();
+
+ // The client is relying upon the reconstitution process
+ // to determine the parent's UUID based upon context. This
+ // is necessary where a new folder may have been
+ // introduced.
+
+ if (result.ParentID == LLUUID.Zero)
+ {
+ result.ParentID = ic.Parent();
+ }
+ else
+ {
+
+ bool found = false;
+
+ foreach (InventoryFolderBase parent in ic.rdata.folders)
+ {
+ if ( parent.ID == result.ParentID )
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}",
+ MsgId, ic.Item.Folder, result.ID);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ }
+
+ // This is a new folder, so no existing UUID is available
+ // or appropriate
+
+ if (result.ID == LLUUID.Zero)
+ {
+ result.ID = LLUUID.Random();
+ }
+
+ // Treat this as a new context. Any other information is
+ // obsolete as a consequence.
+
+ ic.Push(result);
+
+ }
+
+ ///
+ /// This method is called to handle the construction of an Item
+ /// instance from the supplied request entity. It is called
+ /// whenever an Item start tag is detected.
+ /// An instance of an Item is created and initialized to default
+ /// values. These values are then overridden from values supplied
+ /// as attributes to the Item element.
+ /// This item is then stored in the XmlInventoryCollection and
+ /// will be verified by Validate.
+ /// All context is reset whenever the effective folder changes
+ /// or an item is successfully validated.
+ ///
+
+ private void CollectItem(XmlInventoryCollection ic)
+ {
+
+ Rest.Log.DebugFormat("{0} Interpret item element", MsgId);
+
+ InventoryItemBase result = new InventoryItemBase();
+
+ result.Name = String.Empty;
+ result.Description = String.Empty;
+ result.ID = LLUUID.Zero;
+ result.Folder = LLUUID.Zero;
+ result.Owner = ic.UserID;
+ result.Creator = ic.UserID;
+ result.AssetID = LLUUID.Zero;
+ result.GroupID = LLUUID.Zero;
+ result.GroupOwned = false;
+ result.InvType = (int) InventoryType.Unknown;
+ result.AssetType = (int) AssetType.Unknown;
+
+ if (ic.xml.HasAttributes)
+ {
+ for (int i = 0; i < ic.xml.AttributeCount; i++)
+ {
+
+ ic.xml.MoveToAttribute(i);
+
+ switch (ic.xml.Name)
+ {
+ case "name" :
+ result.Name = ic.xml.Value;
+ break;
+ case "desc" :
+ result.Description = ic.xml.Value;
+ break;
+ case "uuid" :
+ result.ID = new LLUUID(ic.xml.Value);
+ break;
+ case "folder" :
+ result.Folder = new LLUUID(ic.xml.Value);
+ break;
+ case "owner" :
+ result.Owner = new LLUUID(ic.xml.Value);
+ break;
+ case "invtype" :
+ result.InvType = Int32.Parse(ic.xml.Value);
+ break;
+ case "creator" :
+ result.Creator = new LLUUID(ic.xml.Value);
+ break;
+ case "assettype" :
+ result.AssetType = Int32.Parse(ic.xml.Value);
+ break;
+ case "groupowned" :
+ result.GroupOwned = Boolean.Parse(ic.xml.Value);
+ break;
+ case "groupid" :
+ result.GroupID = new LLUUID(ic.xml.Value);
+ break;
+ case "flags" :
+ result.Flags = UInt32.Parse(ic.xml.Value);
+ break;
+ case "creationdate" :
+ result.CreationDate = Int32.Parse(ic.xml.Value);
+ break;
+ case "saletype" :
+ result.SaleType = Byte.Parse(ic.xml.Value);
+ break;
+ case "saleprice" :
+ result.SalePrice = Int32.Parse(ic.xml.Value);
+ break;
+
+ default :
+ Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}",
+ MsgId, ic.xml.Name, ic.xml.Value);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ break;
+ }
+ }
+ }
+
+ ic.xml.MoveToElement();
+
+ ic.Push(result);
+
+ }
+
+ ///
+ /// This method assembles an asset instance from the
+ /// information supplied in the request's entity. It is
+ /// called as a result of detecting a start tag for a
+ /// type of Asset.
+ /// The information is collected locally, and an asset
+ /// instance is created only if the basic XML parsing
+ /// completes successfully.
+ /// Default values for all parts of the asset are
+ /// established before overriding them from the supplied
+ /// XML.
+ /// If an asset has inline=true as an attribute, then
+ /// the element contains the data representing the
+ /// asset. This is saved as the data component.
+ /// inline=false means that the element's payload is
+ /// simply the UUID of the asset referenced by the
+ /// item being constructed.
+ /// An asset, if created is stored in the
+ /// XmlInventoryCollection
+ ///
+
+ private void CollectAsset(XmlInventoryCollection ic)
+ {
+
+ Rest.Log.DebugFormat("{0} Interpret asset element", MsgId);
+
+ AssetBase asset = null;
+
+ string name = String.Empty;
+ string desc = String.Empty;
+ sbyte type = (sbyte) AssetType.Unknown;
+ sbyte itype = (sbyte) AssetType.Unknown;
+ bool temp = false;
+ bool local = false;
+
+ // This is not a persistent attribute
+ bool inline = true;
+
+ LLUUID uuid = LLUUID.Zero;
+
+ // Attribute is optional
+ if (ic.xml.HasAttributes)
+ {
+ for (int i = 0; i < ic.xml.AttributeCount; i++)
+ {
+ ic.xml.MoveToAttribute(i);
+ switch (ic.xml.Name)
+ {
+
+ case "name" :
+ name = ic.xml.Value;
+ break;
+
+ case "type" :
+ type = SByte.Parse(ic.xml.Value);
+ break;
+
+ case "description" :
+ desc = ic.xml.Value;
+ break;
+
+ case "temporary" :
+ temp = Boolean.Parse(ic.xml.Value);
+ break;
+
+ case "invtype" :
+ itype = SByte.Parse(ic.xml.Value);
+ break;
+
+ case "uuid" :
+ uuid = new LLUUID(ic.xml.Value);
+ break;
+
+ case "inline" :
+ inline = Boolean.Parse(ic.xml.Value);
+ break;
+
+ case "local" :
+ local = Boolean.Parse(ic.xml.Value);
+ break;
+
+ default :
+ Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}",
+ MsgId, ic.xml.Name, ic.xml.Value);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ break;
+ }
+ }
+ }
+
+ ic.xml.MoveToElement();
+
+ // If this is a reference to an existing asset, just store the
+ // asset ID into the item.
+
+ if (!inline)
+ {
+ if (ic.Item != null)
+ {
+ ic.Item.AssetID = new LLUUID(ic.xml.ReadElementContentAsString());
+ Rest.Log.DebugFormat("{0} Asset ID supplied: {1}", MsgId, ic.Item.AssetID);
+ }
+ else
+ {
+ Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+ }
+
+ // Otherwise, generate an asset ID, store that into the item, and
+ // create an entry in the asset list for the inlined asset. But
+ // only if the size is non-zero.
+
+ else
+ {
+
+ string b64string = null;
+
+ // Generate a UUID of none were given, and generally none should
+ // be. Ever.
+
+ if (uuid == LLUUID.Zero)
+ {
+ uuid = LLUUID.Random();
+ }
+
+ // Create AssetBase entity to hold the inlined asset
+
+ asset = new AssetBase(uuid, name);
+
+ asset.Description = desc;
+ asset.Type = type; // type == 0 == texture
+ asset.InvType = itype;
+ asset.Local = local;
+ asset.Temporary = temp;
+
+ b64string = ic.xml.ReadElementContentAsString();
+
+ Rest.Log.DebugFormat("{0} Data length is {1}", MsgId, b64string.Length);
+ Rest.Log.DebugFormat("{0} Data content starts with: \n\t<{1}>", MsgId,
+ b64string.Substring(0, b64string.Length > 132 ? 132 : b64string.Length));
+
+ asset.Data = Convert.FromBase64String(b64string);
+
+ // Ensure the asset always has some kind of data component
+
+ if (asset.Data == null)
+ {
+ asset.Data = new byte[1];
+ }
+
+ // If this is in the context of an item, establish
+ // a link with the item in context.
+
+ if (ic.Item != null && ic.Item.AssetID == LLUUID.Zero)
+ {
+ ic.Item.AssetID = uuid;
+ }
+
+ }
+
+ ic.Push(asset);
+
+ }
+
+ ///
+ /// Store any permissions information provided by the request.
+ /// This overrides the default permissions set when the
+ /// XmlInventoryCollection object was created.
+ ///
+
+ private void CollectPermissions(XmlInventoryCollection ic)
+ {
+
+ if (ic.xml.HasAttributes)
+ {
+ for (int i = 0; i < ic.xml.AttributeCount; i++)
+ {
+ ic.xml.MoveToAttribute(i);
+ switch (ic.xml.Name)
+ {
+ case "current" :
+ ic.CurrentPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
+ break;
+ case "next" :
+ ic.NextPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
+ break;
+ case "everyone" :
+ ic.EveryOnePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
+ break;
+ case "base" :
+ ic.BasePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
+ break;
+ default :
+ Rest.Log.DebugFormat("{0} Permissions: invalid attribute {1}:{2}",
+ MsgId,ic.xml.Name, ic.xml.Value);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ break;
+ }
+ }
+ }
+
+ ic.xml.MoveToElement();
+
+ }
+
+ ///
+ /// This method is called whenever an Item has been successfully
+ /// reconstituted from the request's entity.
+ /// It uses the information curren tin the XmlInventoryCollection
+ /// to complete the item's specification, including any implied
+ /// context and asset associations.
+ /// It fails the request if any necessary item or asset information
+ /// is missing.
+ ///
+
+ private void Validate(XmlInventoryCollection ic)
+ {
+
+ // There really should be an item present if we've
+ // called validate. So fail if there is not.
+
+ if (ic.Item == null)
+ {
+ Rest.Log.ErrorFormat("{0} Unable to parse request", MsgId);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ // Every item is required to have a name (via REST anyway)
+
+ if (ic.Item.Name == String.Empty)
+ {
+ Rest.Log.ErrorFormat("{0} An item name MUST be specified", MsgId);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ // An item MUST have an asset ID. AssetID should never be zero
+ // here. It should always get set from the information stored
+ // when the Asset element was processed.
+
+ if (ic.Item.AssetID == LLUUID.Zero)
+ {
+
+ Rest.Log.ErrorFormat("{0} Unable to complete request", MsgId);
+ Rest.Log.InfoFormat("{0} Asset information is missing", MsgId);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+
+ }
+
+ // If the item is new, then assign it an ID
+
+ if (ic.Item.ID == LLUUID.Zero)
+ {
+ ic.Item.ID = LLUUID.Random();
+ }
+
+ // If the context is being implied, obtain the current
+ // folder item's ID. If it was specified explicitly, make
+ // sure that theparent folder exists.
+
+ if (ic.Item.Folder == LLUUID.Zero)
+ {
+ ic.Item.Folder = ic.Parent();
+ }
+ else
+ {
+
+ bool found = false;
+
+ foreach (InventoryFolderBase parent in ic.rdata.folders)
+ {
+ if ( parent.ID == ic.Item.Folder )
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}",
+ MsgId, ic.Item.Folder, ic.Item.ID);
+ ic.Fail(Rest.HttpStatusCodeBadRequest,
+ Rest.HttpStatusDescBadRequest);
+ }
+
+ }
+
+ // If this is an inline asset being constructed in the context
+ // of a new Item, then use the itm's name here too.
+
+ if (ic.Asset != null)
+ {
+ if (ic.Asset.Name == String.Empty)
+ ic.Asset.Name = ic.Item.Name;
+ if (ic.Asset.Description == String.Empty)
+ ic.Asset.Description = ic.Item.Description;
+ }
+
+ // Assign permissions
+
+ ic.Item.CurrentPermissions = ic.CurrentPermissions;
+ ic.Item.EveryOnePermissions = ic.EveryOnePermissions;
+ ic.Item.BasePermissions = ic.BasePermissions;
+ ic.Item.NextPermissions = ic.NextPermissions;
+
+ // If no type was specified for this item, we can attempt to
+ // infer something from the file type maybe. This is NOT as
+ // good as having type be specified in the XML.
+
+ if (ic.Item.AssetType == (int) AssetType.Unknown ||
+ ic.Item.InvType == (int) AssetType.Unknown)
+ {
+
+ Rest.Log.DebugFormat("{0} Attempting to infer item type", MsgId);
+
+ string[] parts = ic.Item.Name.Split(Rest.CA_PERIOD);
+
+ if (Rest.DEBUG)
+ {
+ for (int i = 0; i < parts.Length; i++)
+ {
+ Rest.Log.DebugFormat("{0} Name part {1} : {2}",
+ MsgId, i, parts[i]);
+ }
+ }
+
+ // If the associated item name is multi-part, then maybe
+ // the last part will indicate the item type - if we're
+ // lucky.
+
+ if (parts.Length > 1)
+ {
+ Rest.Log.DebugFormat("{0} File type is {1}",
+ MsgId, parts[parts.Length - 1]);
+ switch (parts[parts.Length - 1])
+ {
+ case "jpeg2000" :
+ case "jpeg-2000" :
+ case "jpg2000" :
+ case "jpg-2000" :
+ Rest.Log.DebugFormat("{0} Type {1} inferred",
+ MsgId, parts[parts.Length-1]);
+ if (ic.Item.AssetType == (int) AssetType.Unknown)
+ ic.Item.AssetType = (int) AssetType.ImageJPEG;
+ if (ic.Item.InvType == (int) AssetType.Unknown)
+ ic.Item.InvType = (int) AssetType.ImageJPEG;
+ break;
+ case "jpg" :
+ case "jpeg" :
+ Rest.Log.DebugFormat("{0} Type {1} inferred",
+ MsgId, parts[parts.Length - 1]);
+ if (ic.Item.AssetType == (int) AssetType.Unknown)
+ ic.Item.AssetType = (int) AssetType.ImageJPEG;
+ if (ic.Item.InvType == (int) AssetType.Unknown)
+ ic.Item.InvType = (int) AssetType.ImageJPEG;
+ break;
+ default :
+ Rest.Log.DebugFormat("{0} Type was not inferred", MsgId);
+ break;
+ }
+ }
+ }
+
+ ic.reset();
+
+ }
+
+ #region Inventory RequestData extension
+
+ internal class InventoryRequestData : RequestData
+ {
+
+ ///
+ /// These are the inventory specific request/response state
+ /// extensions.
+ ///
+
+ internal bool HaveInventory = false;
+ internal ICollection folders = null;
+ internal ICollection items = null;
+ internal UserProfileData userProfile = null;
+ internal InventoryFolderBase root = null;
+
+ internal InventoryRequestData(OSHttpRequest request, OSHttpResponse response, string prefix)
+ : base(request, response, prefix)
+ {
+ }
+
+ ///
+ /// This is the callback method required by inventory services. The
+ /// requestor issues an inventory request and then blocks until this
+ /// method signals the monitor.
+ ///
+
+ internal void GetUserInventory(ICollection folders, ICollection items)
+ {
+ Rest.Log.DebugFormat("{0} Asynchronously updating inventory data", MsgId);
+ this.folders = folders;
+ this.items = items;
+ this.HaveInventory = true;
+ lock(this)
+ {
+ Monitor.Pulse(this);
+ }
+ }
+
+ }
+
+ #endregion Inventory RequestData extension
+
+ ///
+ /// This class is used to record and manage the hierarchy
+ /// constructed from the entity supplied in the request for
+ /// PUT and POST.
+ ///
+
+ internal class XmlInventoryCollection : InventoryCollection
+ {
+
+ internal InventoryRequestData rdata;
+ private Stack stk;
+
+ internal List Assets;
+
+ internal InventoryItemBase Item;
+ internal AssetBase Asset;
+ internal XmlReader xml;
+
+ internal /*static*/ const uint DefaultCurrent = 0x7FFFFFFF;
+ internal /*static*/ const uint DefaultNext = 0x82000;
+ internal /*static*/ const uint DefaultBase = 0x7FFFFFFF;
+ internal /*static*/ const uint DefaultEveryOne = 0x0;
+
+ internal uint CurrentPermissions = 0x00;
+ internal uint NextPermissions = 0x00;
+ internal uint BasePermissions = 0x00;
+ internal uint EveryOnePermissions = 0x00;
+
+ internal XmlInventoryCollection()
+ {
+ Folders = new List();
+ Items = new List();
+ Assets = new List();
+ }
+
+ internal void init(InventoryRequestData p_rdata)
+ {
+ rdata = p_rdata;
+ UserID = rdata.uuid;
+ stk = new Stack();
+ rdata.initXmlReader();
+ xml = rdata.reader;
+ initPermissions();
+ }
+
+ internal void initPermissions()
+ {
+ CurrentPermissions = DefaultCurrent;
+ NextPermissions = DefaultNext;
+ BasePermissions = DefaultBase;
+ EveryOnePermissions = DefaultEveryOne;
+ }
+
+ internal LLUUID Parent()
+ {
+ if (stk.Count != 0)
+ {
+ return stk.Peek().ID;
+ }
+ else
+ {
+ return LLUUID.Zero;
+ }
+ }
+
+ internal void Push(InventoryFolderBase folder)
+ {
+ stk.Push(folder);
+ Folders.Add(folder);
+ reset();
+ }
+
+ internal void Push(InventoryItemBase item)
+ {
+ Item = item;
+ Items.Add(item);
+ }
+
+ internal void Push(AssetBase asset)
+ {
+ Asset = asset;
+ Assets.Add(asset);
+ }
+
+ internal void Pop()
+ {
+ stk.Pop();
+ reset();
+ }
+
+ internal void reset()
+ {
+ Item = null;
+ Asset = null;
+ initPermissions();
+ }
+
+ internal void Fail(int code, string desc)
+ {
+ rdata.Fail(code, desc);
+ }
+
+ }
+ }
+}
diff --git a/prebuild.xml b/prebuild.xml
index fdc9eeec3e..29c8a23a26 100644
--- a/prebuild.xml
+++ b/prebuild.xml
@@ -1198,6 +1198,44 @@
+
+
+
+
+ ../../../../bin/
+
+
+
+
+ ../../../../bin/
+
+
+
+ ../../../../bin/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+