From: Alan Webb <alan_webb@us.ibm.com>
cleanups and assorted fixes to REST inventory, asset, and appearance services.0.6.0-stable
parent
1fc6872f20
commit
12042cdc2b
|
@ -42,11 +42,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
public delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response, string path);
|
||||
|
||||
/// <summary>
|
||||
/// This interface represents the boundary between the general purpose
|
||||
/// REST plugin handling, and the functionally specific handlers. The
|
||||
/// handler knows only to initialize and terminate all such handlers
|
||||
/// that it finds. Implementing this interface identifies the class as
|
||||
/// a REST handler implementation.
|
||||
/// This interface exports the generic plugin-handling services
|
||||
/// available to each loaded REST services module (IRest implementation)
|
||||
/// </summary>
|
||||
|
||||
internal interface IRestHandler
|
||||
|
|
|
@ -167,7 +167,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
//
|
||||
|
||||
internal bool authenticated = false;
|
||||
internal string scheme = null;
|
||||
internal string scheme = Rest.Scheme;
|
||||
internal string realm = Rest.Realm;
|
||||
internal string domain = null;
|
||||
internal string nonce = null;
|
||||
|
@ -292,6 +292,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
sbuilder.Length = 0;
|
||||
|
||||
encoding = request.ContentEncoding;
|
||||
|
||||
if (encoding == null)
|
||||
{
|
||||
encoding = Rest.Encoding;
|
||||
|
@ -448,9 +449,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
if (realm != null)
|
||||
{
|
||||
sbuilder.Append(" realm=\"");
|
||||
sbuilder.Append(" realm=");
|
||||
sbuilder.Append(Rest.CS_DQUOTE);
|
||||
sbuilder.Append(realm);
|
||||
sbuilder.Append("\"");
|
||||
sbuilder.Append(Rest.CS_DQUOTE);
|
||||
}
|
||||
AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString());
|
||||
}
|
||||
|
@ -677,7 +679,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
UserProfileData udata = Rest.UserServices.GetUserProfile(first, last);
|
||||
|
||||
// If we don;t recognize the user id, perhaps it is god?
|
||||
// If we don't recognize the user id, perhaps it is god?
|
||||
|
||||
if (udata == null)
|
||||
return pass == Rest.GodKey;
|
||||
|
@ -800,6 +802,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
if (!authparms.ContainsKey("cnonce"))
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId);
|
||||
Fail(Rest.HttpStatusCodeBadRequest);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -808,6 +811,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
if (!authparms.TryGetValue("nc", out nck) || nck == null)
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId);
|
||||
Fail(Rest.HttpStatusCodeBadRequest);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -820,6 +824,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
if (Rest.Hex2Int(ncl) >= Rest.Hex2Int(nck))
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId);
|
||||
Fail(Rest.HttpStatusCodeBadRequest);
|
||||
break;
|
||||
}
|
||||
cntable[nonce] = nck;
|
||||
|
@ -840,11 +845,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
if (authparms.ContainsKey("cnonce"))
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce", MsgId);
|
||||
Fail(Rest.HttpStatusCodeBadRequest);
|
||||
break;
|
||||
}
|
||||
if (authparms.ContainsKey("nc"))
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce counter[2]", MsgId);
|
||||
Fail(Rest.HttpStatusCodeBadRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -857,6 +864,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
while (false);
|
||||
|
||||
}
|
||||
else
|
||||
Fail(Rest.HttpStatusCodeBadRequest);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,21 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
internal static bool DEBUG = Log.IsDebugEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Supported authentication schemes
|
||||
/// </summary>
|
||||
|
||||
public const string AS_BASIC = "Basic"; // simple user/password verification
|
||||
public const string AS_DIGEST = "Digest"; // password safe authentication
|
||||
|
||||
/// Supported Digest algorithms
|
||||
|
||||
public const string Digest_MD5 = "MD5"; // assumed default if omitted
|
||||
public const string Digest_MD5Sess = "MD5-sess"; // session-span - not good for REST?
|
||||
|
||||
public const string Qop_Auth = "auth"; // authentication only
|
||||
public const string Qop_Int = "auth-int"; // TODO
|
||||
|
||||
/// <summary>
|
||||
/// These values have a single value for the whole
|
||||
/// domain and lifetime of the plugin handler. We
|
||||
|
@ -67,9 +82,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
internal static bool Secure = true;
|
||||
internal static bool ExtendedEscape = true;
|
||||
internal static bool DumpAsset = false;
|
||||
internal static bool Fill = true;
|
||||
internal static bool Fill = false;
|
||||
internal static bool FlushEnabled = true;
|
||||
internal static string Realm = "REST";
|
||||
internal static string Realm = "OpenSim REST";
|
||||
internal static string Scheme = AS_BASIC;
|
||||
internal static int DumpLineSize = 32; // Should be a multiple of 16 or (possibly) 4
|
||||
|
||||
/// <summary>
|
||||
|
@ -383,21 +399,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
public const string HttpHeaderWarning = "Warning";
|
||||
public const string HttpHeaderWWWAuthenticate = "WWW-Authenticate";
|
||||
|
||||
/// <summary>
|
||||
/// Supported authentication schemes
|
||||
/// </summary>
|
||||
|
||||
public const string AS_BASIC = "Basic";
|
||||
public const string AS_DIGEST = "Digest";
|
||||
|
||||
/// Supported Digest algorithms
|
||||
|
||||
public const string Digest_MD5 = "MD5"; // assumed default 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)
|
||||
|
|
|
@ -63,6 +63,16 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
Rest.Log.InfoFormat("{0} User appearance services initializing", MsgId);
|
||||
Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
|
||||
|
||||
// This is better than a null reference.
|
||||
|
||||
if (Rest.AvatarServices == null)
|
||||
throw new Exception(String.Format("{0} OpenSim inventory services are not available",
|
||||
MsgId));
|
||||
|
||||
if (Rest.UserServices == null)
|
||||
throw new Exception(String.Format("{0} OpenSim user profile services are not available",
|
||||
MsgId));
|
||||
|
||||
// If a relative path was specified for the handler's domain,
|
||||
// add the standard prefix to make it absolute, e.g. /admin
|
||||
|
||||
|
@ -170,9 +180,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
try
|
||||
{
|
||||
// digest scheme seems borked: disable it for the time
|
||||
// being
|
||||
rdata.scheme = Rest.AS_BASIC;
|
||||
if (!rdata.IsAuthenticated)
|
||||
{
|
||||
rdata.Fail(Rest.HttpStatusCodeNotAuthorized,String.Format("user \"{0}\" could not be authenticated", rdata.userName));
|
||||
|
@ -731,7 +738,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
if (asset != UUID.Zero)
|
||||
{
|
||||
rdata.writer.WriteAttributeString("Item",asset.ToString());
|
||||
rdata.writer.WriteAttributeString("Asset",asset.ToString());
|
||||
}
|
||||
rdata.writer.WriteEndElement();
|
||||
}
|
||||
|
|
|
@ -52,6 +52,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
Rest.Log.InfoFormat("{0} Asset services initializing", MsgId);
|
||||
Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
|
||||
|
||||
// This is better than a null reference.
|
||||
|
||||
if (Rest.AssetServices == null)
|
||||
throw new Exception(String.Format("{0} OpenSim asset services are not available",
|
||||
MsgId));
|
||||
|
||||
// If the handler specifies a relative path for its domain
|
||||
// then we must add the standard absolute prefix, e.g. /admin
|
||||
|
||||
|
@ -130,9 +136,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
try
|
||||
{
|
||||
// digest scheme seems borked: disable it for the time
|
||||
// being
|
||||
rdata.scheme = Rest.AS_BASIC;
|
||||
if (!rdata.IsAuthenticated)
|
||||
{
|
||||
rdata.Fail(Rest.HttpStatusCodeNotAuthorized, String.Format("user \"{0}\" could not be authenticated"));
|
||||
|
|
|
@ -239,14 +239,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
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.Fill = Rest.Config.GetBoolean("path-fill",true);
|
||||
Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size",32);
|
||||
Rest.FlushEnabled = Rest.Config.GetBoolean("flush-on-error",true);
|
||||
Rest.Authenticate = Rest.Config.GetBoolean("authenticate", Rest.Authenticate);
|
||||
Rest.Scheme = Rest.Config.GetString("auth-scheme", Rest.Scheme);
|
||||
Rest.Secure = Rest.Config.GetBoolean("secured", Rest.Secure);
|
||||
Rest.ExtendedEscape = Rest.Config.GetBoolean("extended-escape", Rest.ExtendedEscape);
|
||||
Rest.Realm = Rest.Config.GetString("realm", Rest.Realm);
|
||||
Rest.DumpAsset = Rest.Config.GetBoolean("dump-asset", Rest.DumpAsset);
|
||||
Rest.Fill = Rest.Config.GetBoolean("path-fill", Rest.Fill);
|
||||
Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size", Rest.DumpLineSize);
|
||||
Rest.FlushEnabled = Rest.Config.GetBoolean("flush-on-error", Rest.FlushEnabled);
|
||||
|
||||
// Note: Odd spacing is required in the following strings
|
||||
|
||||
Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId,
|
||||
(Rest.Authenticate ? "" : "not "));
|
||||
|
@ -374,13 +377,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
string path = request.RawUrl.ToLower();
|
||||
|
||||
Rest.Log.DebugFormat("{0} Match ENTRY", MsgId);
|
||||
// Rest.Log.DebugFormat("{0} Match ENTRY", MsgId);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string key in pathHandlers.Keys)
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Match testing {1} against agent prefix <{2}>", MsgId, path, key);
|
||||
// Rest.Log.DebugFormat("{0} Match testing {1} against agent prefix <{2}>", MsgId, path, key);
|
||||
|
||||
// Note that Match will not necessarily find the handler that will
|
||||
// actually be used - it does no test for the "closest" fit. It
|
||||
|
@ -388,7 +391,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
if (path.StartsWith(key))
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key);
|
||||
// Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key);
|
||||
|
||||
// This apparently odd evaluation is needed to prevent a match
|
||||
// on anything other than a URI token boundary. Otherwise we
|
||||
|
@ -404,7 +407,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
foreach (string key in streamHandlers.Keys)
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Match testing {1} against stream prefix <{2}>", MsgId, path, key);
|
||||
// Rest.Log.DebugFormat("{0} Match testing {1} against stream prefix <{2}>", MsgId, path, key);
|
||||
|
||||
// Note that Match will not necessarily find the handler that will
|
||||
// actually be used - it does no test for the "closest" fit. It
|
||||
|
@ -412,7 +415,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
if (path.StartsWith(key))
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key);
|
||||
// Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key);
|
||||
|
||||
// This apparently odd evaluation is needed to prevent a match
|
||||
// on anything other than a URI token boundary. Otherwise we
|
||||
|
@ -434,7 +437,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
/// <summary>
|
||||
/// This is called by the HTTP server once the handler has indicated
|
||||
/// that t is able to handle the request.
|
||||
/// that it is able to handle the request.
|
||||
/// Preconditions:
|
||||
/// [1] request != null and is a valid request object
|
||||
/// [2] response != null and is a valid response object
|
||||
|
@ -474,7 +477,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
{
|
||||
// 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.
|
||||
// or a failure to satisfy the preconditions. It should not reflect
|
||||
// an error in the request itself. Under such circumstances the state
|
||||
// of the request cannot be determined and we are obliged to mark it
|
||||
// as 'handled'.
|
||||
|
||||
Rest.Log.ErrorFormat("{0} Plugin error: {1}", MsgId, e.Message);
|
||||
handled = true;
|
||||
}
|
||||
|
@ -497,7 +504,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
{
|
||||
RequestData rdata = new RequestData(request, response, String.Empty);
|
||||
|
||||
string bestMatch = null;
|
||||
string bestMatch = String.Empty;
|
||||
string path = String.Format("{0}:{1}", rdata.method, rdata.path).ToLower();
|
||||
|
||||
Rest.Log.DebugFormat("{0} Checking for stream handler for <{1}>", MsgId, path);
|
||||
|
@ -511,7 +518,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
{
|
||||
if (path.StartsWith(pattern))
|
||||
{
|
||||
if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length)
|
||||
if (pattern.Length > bestMatch.Length)
|
||||
{
|
||||
bestMatch = pattern;
|
||||
}
|
||||
|
@ -520,7 +527,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
// Handle using the best match available
|
||||
|
||||
if (!String.IsNullOrEmpty(bestMatch))
|
||||
if (bestMatch.Length > 0)
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Stream-based handler matched with <{1}>", MsgId, bestMatch);
|
||||
RestStreamHandler handler = streamHandlers[bestMatch];
|
||||
|
|
|
@ -31,6 +31,7 @@ using System.IO;
|
|||
using System.Threading;
|
||||
using System.Xml;
|
||||
using System.Drawing;
|
||||
using System.Timers;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Framework.Servers;
|
||||
using OpenSim.Framework.Communications;
|
||||
|
@ -61,6 +62,20 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
Rest.Log.InfoFormat("{0} Inventory services initializing", MsgId);
|
||||
Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
|
||||
|
||||
// This is better than a null reference.
|
||||
|
||||
if (Rest.InventoryServices == null)
|
||||
throw new Exception(String.Format("{0} OpenSim inventory services are not available",
|
||||
MsgId));
|
||||
|
||||
if (Rest.UserServices == null)
|
||||
throw new Exception(String.Format("{0} OpenSim user services are not available",
|
||||
MsgId));
|
||||
|
||||
if (Rest.AssetServices == null)
|
||||
throw new Exception(String.Format("{0} OpenSim asset services are not available",
|
||||
MsgId));
|
||||
|
||||
// If a relative path was specified for the handler's domain,
|
||||
// add the standard prefix to make it absolute, e.g. /admin
|
||||
|
||||
|
@ -167,9 +182,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
|
||||
try
|
||||
{
|
||||
// digest scheme seems borked: disable it for the time
|
||||
// being
|
||||
rdata.scheme = Rest.AS_BASIC;
|
||||
if (!rdata.IsAuthenticated)
|
||||
{
|
||||
rdata.Fail(Rest.HttpStatusCodeNotAuthorized,String.Format("user \"{0}\" could not be authenticated", rdata.userName));
|
||||
|
@ -283,14 +295,24 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}",
|
||||
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
|
||||
|
||||
|
||||
lock (rdata)
|
||||
{
|
||||
if (!rdata.HaveInventory)
|
||||
{
|
||||
rdata.startWD(1000);
|
||||
rdata.timeout = false;
|
||||
Monitor.Wait(rdata);
|
||||
}
|
||||
}
|
||||
|
||||
if (rdata.timeout)
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Inventory not available for {1} {2}. No response from service.",
|
||||
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
|
||||
rdata.Fail(Rest.HttpStatusCodeServerError, "inventory server not responding");
|
||||
}
|
||||
|
||||
if (rdata.root == null)
|
||||
{
|
||||
Rest.Log.WarnFormat("{0} Inventory is not available [1] for agent {1} {2}",
|
||||
|
@ -2145,12 +2167,50 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
internal ICollection<InventoryItemBase> items = null;
|
||||
internal UserProfileData userProfile = null;
|
||||
internal InventoryFolderBase root = null;
|
||||
internal bool timeout = false;
|
||||
internal System.Timers.Timer watchDog = new System.Timers.Timer();
|
||||
|
||||
internal InventoryRequestData(OSHttpRequest request, OSHttpResponse response, string prefix)
|
||||
: base(request, response, prefix)
|
||||
{
|
||||
}
|
||||
|
||||
internal void startWD(double interval)
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Setting watchdog", MsgId);
|
||||
watchDog.Elapsed += new ElapsedEventHandler(OnTimeOut);
|
||||
watchDog.Interval = interval;
|
||||
watchDog.AutoReset = false;
|
||||
watchDog.Enabled = true;
|
||||
watchDog.Start();
|
||||
}
|
||||
|
||||
internal void stopWD()
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Reset watchdog", MsgId);
|
||||
watchDog.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the callback method required by the inventory watchdog. The
|
||||
/// requestor issues an inventory request and then blocks until the
|
||||
/// request completes, or this method signals the monitor.
|
||||
/// </summary>
|
||||
|
||||
private void OnTimeOut(object sender, ElapsedEventArgs args)
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Asynchronous inventory update timed-out", MsgId);
|
||||
// InventoryRequestData rdata = (InventoryRequestData) sender;
|
||||
lock (this)
|
||||
{
|
||||
this.folders = null;
|
||||
this.items = null;
|
||||
this.HaveInventory = false;
|
||||
this.timeout = true;
|
||||
Monitor.Pulse(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the callback method required by inventory services. The
|
||||
/// requestor issues an inventory request and then blocks until this
|
||||
|
@ -2160,11 +2220,16 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory
|
|||
internal void GetUserInventory(ICollection<InventoryFolderImpl> folders, ICollection<InventoryItemBase> items)
|
||||
{
|
||||
Rest.Log.DebugFormat("{0} Asynchronously updating inventory data", MsgId);
|
||||
lock (this)
|
||||
{
|
||||
if (watchDog.Enabled)
|
||||
{
|
||||
this.stopWD();
|
||||
}
|
||||
this.folders = folders;
|
||||
this.items = items;
|
||||
this.HaveInventory = true;
|
||||
lock (this)
|
||||
{
|
||||
this.timeout = false;
|
||||
Monitor.Pulse(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,9 +178,15 @@ namespace OpenSim.Framework.Communications.Cache
|
|||
if (libraryFolders.ContainsKey(item.Folder))
|
||||
{
|
||||
InventoryFolderImpl parentFolder = libraryFolders[item.Folder];
|
||||
|
||||
try
|
||||
{
|
||||
parentFolder.Items.Add(item.ID, item);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
m_log.WarnFormat("[LIBRARY INVENTORY] Item {1} [{0}] not added, duplicate item", item.ID, item.Name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.WarnFormat(
|
||||
|
|
Loading…
Reference in New Issue