clockwerk-opensim – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 vero 1 /*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSim Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27  
28 using System;
29 using System.IO;
30 using System.Net;
31 using System.Text;
32 using System.Xml;
33 using System.Collections;
34 using System.Collections.Generic;
35 using System.Reflection;
36 using System.Threading;
37 using OpenMetaverse;
38 using log4net;
39 using Mono.Addins;
40 using Nini.Config;
41 using Nwc.XmlRpc;
42 using OpenSim.Framework;
43  
44 using OpenSim.Framework.Capabilities;
45 using OpenSim.Framework.Servers;
46 using OpenSim.Framework.Servers.HttpServer;
47 using OpenSim.Region.Framework.Interfaces;
48 using OpenSim.Region.Framework.Scenes;
49 using Caps = OpenSim.Framework.Capabilities.Caps;
50  
51 namespace OpenSim.Region.OptionalModules.Avatar.Voice.VivoxVoice
52 {
53 [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "VivoxVoiceModule")]
54 public class VivoxVoiceModule : ISharedRegionModule
55 {
56  
57 // channel distance model values
58 public const int CHAN_DIST_NONE = 0; // no attenuation
59 public const int CHAN_DIST_INVERSE = 1; // inverse distance attenuation
60 public const int CHAN_DIST_LINEAR = 2; // linear attenuation
61 public const int CHAN_DIST_EXPONENT = 3; // exponential attenuation
62 public const int CHAN_DIST_DEFAULT = CHAN_DIST_LINEAR;
63  
64 // channel type values
65 public static readonly string CHAN_TYPE_POSITIONAL = "positional";
66 public static readonly string CHAN_TYPE_CHANNEL = "channel";
67 public static readonly string CHAN_TYPE_DEFAULT = CHAN_TYPE_POSITIONAL;
68  
69 // channel mode values
70 public static readonly string CHAN_MODE_OPEN = "open";
71 public static readonly string CHAN_MODE_LECTURE = "lecture";
72 public static readonly string CHAN_MODE_PRESENTATION = "presentation";
73 public static readonly string CHAN_MODE_AUDITORIUM = "auditorium";
74 public static readonly string CHAN_MODE_DEFAULT = CHAN_MODE_OPEN;
75  
76 // unconstrained default values
77 public const double CHAN_ROLL_OFF_DEFAULT = 2.0; // rate of attenuation
78 public const double CHAN_ROLL_OFF_MIN = 1.0;
79 public const double CHAN_ROLL_OFF_MAX = 4.0;
80 public const int CHAN_MAX_RANGE_DEFAULT = 80; // distance at which channel is silent
81 public const int CHAN_MAX_RANGE_MIN = 0;
82 public const int CHAN_MAX_RANGE_MAX = 160;
83 public const int CHAN_CLAMPING_DISTANCE_DEFAULT = 10; // distance before attenuation applies
84 public const int CHAN_CLAMPING_DISTANCE_MIN = 0;
85 public const int CHAN_CLAMPING_DISTANCE_MAX = 160;
86  
87 // Infrastructure
88 private static readonly ILog m_log =
89 LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
90 private static readonly Object vlock = new Object();
91  
92 // Capability strings
93 private static readonly string m_parcelVoiceInfoRequestPath = "0107/";
94 private static readonly string m_provisionVoiceAccountRequestPath = "0108/";
95 //private static readonly string m_chatSessionRequestPath = "0109/";
96  
97 // Control info, e.g. vivox server, admin user, admin password
98 private static bool m_pluginEnabled = false;
99 private static bool m_adminConnected = false;
100  
101 private static string m_vivoxServer;
102 private static string m_vivoxSipUri;
103 private static string m_vivoxVoiceAccountApi;
104 private static string m_vivoxAdminUser;
105 private static string m_vivoxAdminPassword;
106 private static string m_authToken = String.Empty;
107  
108 private static int m_vivoxChannelDistanceModel;
109 private static double m_vivoxChannelRollOff;
110 private static int m_vivoxChannelMaximumRange;
111 private static string m_vivoxChannelMode;
112 private static string m_vivoxChannelType;
113 private static int m_vivoxChannelClampingDistance;
114  
115 private static Dictionary<string,string> m_parents = new Dictionary<string,string>();
116 private static bool m_dumpXml;
117  
118 private IConfig m_config;
119  
120 private object m_Lock;
121  
122 public void Initialise(IConfigSource config)
123 {
124  
125 m_config = config.Configs["VivoxVoice"];
126  
127 if (null == m_config)
128 return;
129  
130 if (!m_config.GetBoolean("enabled", false))
131 return;
132  
133 m_Lock = new object();
134  
135 try
136 {
137 // retrieve configuration variables
138 m_vivoxServer = m_config.GetString("vivox_server", String.Empty);
139 m_vivoxSipUri = m_config.GetString("vivox_sip_uri", String.Empty);
140 m_vivoxAdminUser = m_config.GetString("vivox_admin_user", String.Empty);
141 m_vivoxAdminPassword = m_config.GetString("vivox_admin_password", String.Empty);
142  
143 m_vivoxChannelDistanceModel = m_config.GetInt("vivox_channel_distance_model", CHAN_DIST_DEFAULT);
144 m_vivoxChannelRollOff = m_config.GetDouble("vivox_channel_roll_off", CHAN_ROLL_OFF_DEFAULT);
145 m_vivoxChannelMaximumRange = m_config.GetInt("vivox_channel_max_range", CHAN_MAX_RANGE_DEFAULT);
146 m_vivoxChannelMode = m_config.GetString("vivox_channel_mode", CHAN_MODE_DEFAULT).ToLower();
147 m_vivoxChannelType = m_config.GetString("vivox_channel_type", CHAN_TYPE_DEFAULT).ToLower();
148 m_vivoxChannelClampingDistance = m_config.GetInt("vivox_channel_clamping_distance",
149 CHAN_CLAMPING_DISTANCE_DEFAULT);
150 m_dumpXml = m_config.GetBoolean("dump_xml", false);
151  
152 // Validate against constraints and default if necessary
153 if (m_vivoxChannelRollOff < CHAN_ROLL_OFF_MIN || m_vivoxChannelRollOff > CHAN_ROLL_OFF_MAX)
154 {
155 m_log.WarnFormat("[VivoxVoice] Invalid value for roll off ({0}), reset to {1}.",
156 m_vivoxChannelRollOff, CHAN_ROLL_OFF_DEFAULT);
157 m_vivoxChannelRollOff = CHAN_ROLL_OFF_DEFAULT;
158 }
159  
160 if (m_vivoxChannelMaximumRange < CHAN_MAX_RANGE_MIN || m_vivoxChannelMaximumRange > CHAN_MAX_RANGE_MAX)
161 {
162 m_log.WarnFormat("[VivoxVoice] Invalid value for maximum range ({0}), reset to {1}.",
163 m_vivoxChannelMaximumRange, CHAN_MAX_RANGE_DEFAULT);
164 m_vivoxChannelMaximumRange = CHAN_MAX_RANGE_DEFAULT;
165 }
166  
167 if (m_vivoxChannelClampingDistance < CHAN_CLAMPING_DISTANCE_MIN ||
168 m_vivoxChannelClampingDistance > CHAN_CLAMPING_DISTANCE_MAX)
169 {
170 m_log.WarnFormat("[VivoxVoice] Invalid value for clamping distance ({0}), reset to {1}.",
171 m_vivoxChannelClampingDistance, CHAN_CLAMPING_DISTANCE_DEFAULT);
172 m_vivoxChannelClampingDistance = CHAN_CLAMPING_DISTANCE_DEFAULT;
173 }
174  
175 switch (m_vivoxChannelMode)
176 {
177 case "open" : break;
178 case "lecture" : break;
179 case "presentation" : break;
180 case "auditorium" : break;
181 default :
182 m_log.WarnFormat("[VivoxVoice] Invalid value for channel mode ({0}), reset to {1}.",
183 m_vivoxChannelMode, CHAN_MODE_DEFAULT);
184 m_vivoxChannelMode = CHAN_MODE_DEFAULT;
185 break;
186 }
187  
188 switch (m_vivoxChannelType)
189 {
190 case "positional" : break;
191 case "channel" : break;
192 default :
193 m_log.WarnFormat("[VivoxVoice] Invalid value for channel type ({0}), reset to {1}.",
194 m_vivoxChannelType, CHAN_TYPE_DEFAULT);
195 m_vivoxChannelType = CHAN_TYPE_DEFAULT;
196 break;
197 }
198  
199 m_vivoxVoiceAccountApi = String.Format("http://{0}/api2", m_vivoxServer);
200  
201 // Admin interface required values
202 if (String.IsNullOrEmpty(m_vivoxServer) ||
203 String.IsNullOrEmpty(m_vivoxSipUri) ||
204 String.IsNullOrEmpty(m_vivoxAdminUser) ||
205 String.IsNullOrEmpty(m_vivoxAdminPassword))
206 {
207 m_log.Error("[VivoxVoice] plugin mis-configured");
208 m_log.Info("[VivoxVoice] plugin disabled: incomplete configuration");
209 return;
210 }
211  
212 m_log.InfoFormat("[VivoxVoice] using vivox server {0}", m_vivoxServer);
213  
214 // Get admin rights and cleanup any residual channel definition
215  
216 DoAdminLogin();
217  
218 m_pluginEnabled = true;
219  
220 m_log.Info("[VivoxVoice] plugin enabled");
221 }
222 catch (Exception e)
223 {
224 m_log.ErrorFormat("[VivoxVoice] plugin initialization failed: {0}", e.Message);
225 m_log.DebugFormat("[VivoxVoice] plugin initialization failed: {0}", e.ToString());
226 return;
227 }
228 }
229  
230 public void AddRegion(Scene scene)
231 {
232 if (m_pluginEnabled)
233 {
234 lock (vlock)
235 {
236 string channelId;
237  
238 string sceneUUID = scene.RegionInfo.RegionID.ToString();
239 string sceneName = scene.RegionInfo.RegionName;
240  
241 // Make sure that all local channels are deleted.
242 // So we have to search for the children, and then do an
243 // iteration over the set of chidren identified.
244 // This assumes that there is just one directory per
245 // region.
246  
247 if (VivoxTryGetDirectory(sceneUUID + "D", out channelId))
248 {
249 m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}",
250 sceneName, sceneUUID, channelId);
251  
252 XmlElement children = VivoxListChildren(channelId);
253 string count;
254  
255 if (XmlFind(children, "response.level0.channel-search.count", out count))
256 {
257 int cnum = Convert.ToInt32(count);
258 for (int i = 0; i < cnum; i++)
259 {
260 string id;
261 if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id))
262 {
263 if (!IsOK(VivoxDeleteChannel(channelId, id)))
264 m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id);
265 }
266 }
267 }
268 }
269 else
270 {
271 if (!VivoxTryCreateDirectory(sceneUUID + "D", sceneName, out channelId))
272 {
273 m_log.WarnFormat("[VivoxVoice] Create failed <{0}:{1}:{2}>",
274 "*", sceneUUID, sceneName);
275 channelId = String.Empty;
276 }
277 }
278  
279 // Create a dictionary entry unconditionally. This eliminates the
280 // need to check for a parent in the core code. The end result is
281 // the same, if the parent table entry is an empty string, then
282 // region channels will be created as first-level channels.
283 lock (m_parents)
284 {
285 if (m_parents.ContainsKey(sceneUUID))
286 {
287 RemoveRegion(scene);
288 m_parents.Add(sceneUUID, channelId);
289 }
290 else
291 {
292 m_parents.Add(sceneUUID, channelId);
293 }
294 }
295 }
296  
297 // we need to capture scene in an anonymous method
298 // here as we need it later in the callbacks
299 scene.EventManager.OnRegisterCaps += delegate(UUID agentID, Caps caps)
300 {
301 OnRegisterCaps(scene, agentID, caps);
302 };
303 }
304 }
305  
306 public void RegionLoaded(Scene scene)
307 {
308 // Do nothing.
309 }
310  
311 public void RemoveRegion(Scene scene)
312 {
313 if (m_pluginEnabled)
314 {
315 lock (vlock)
316 {
317 string channelId;
318  
319 string sceneUUID = scene.RegionInfo.RegionID.ToString();
320 string sceneName = scene.RegionInfo.RegionName;
321  
322 // Make sure that all local channels are deleted.
323 // So we have to search for the children, and then do an
324 // iteration over the set of chidren identified.
325 // This assumes that there is just one directory per
326 // region.
327 if (VivoxTryGetDirectory(sceneUUID + "D", out channelId))
328 {
329 m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}",
330 sceneName, sceneUUID, channelId);
331  
332 XmlElement children = VivoxListChildren(channelId);
333 string count;
334  
335 if (XmlFind(children, "response.level0.channel-search.count", out count))
336 {
337 int cnum = Convert.ToInt32(count);
338 for (int i = 0; i < cnum; i++)
339 {
340 string id;
341 if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id))
342 {
343 if (!IsOK(VivoxDeleteChannel(channelId, id)))
344 m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id);
345 }
346 }
347 }
348 }
349  
350 if (!IsOK(VivoxDeleteChannel(null, channelId)))
351 m_log.WarnFormat("[VivoxVoice] Parent channel delete failed {0}:{1}:{2}", sceneName, sceneUUID, channelId);
352  
353 // Remove the channel umbrella entry
354  
355 lock (m_parents)
356 {
357 if (m_parents.ContainsKey(sceneUUID))
358 {
359 m_parents.Remove(sceneUUID);
360 }
361 }
362 }
363 }
364 }
365  
366 public void PostInitialise()
367 {
368 // Do nothing.
369 }
370  
371 public void Close()
372 {
373 if (m_pluginEnabled)
374 VivoxLogout();
375 }
376  
377 public Type ReplaceableInterface
378 {
379 get { return null; }
380 }
381  
382 public string Name
383 {
384 get { return "VivoxVoiceModule"; }
385 }
386  
387 public bool IsSharedModule
388 {
389 get { return true; }
390 }
391  
392 // <summary>
393 // OnRegisterCaps is invoked via the scene.EventManager
394 // everytime OpenSim hands out capabilities to a client
395 // (login, region crossing). We contribute two capabilities to
396 // the set of capabilities handed back to the client:
397 // ProvisionVoiceAccountRequest and ParcelVoiceInfoRequest.
398 //
399 // ProvisionVoiceAccountRequest allows the client to obtain
400 // the voice account credentials for the avatar it is
401 // controlling (e.g., user name, password, etc).
402 //
403 // ParcelVoiceInfoRequest is invoked whenever the client
404 // changes from one region or parcel to another.
405 //
406 // Note that OnRegisterCaps is called here via a closure
407 // delegate containing the scene of the respective region (see
408 // Initialise()).
409 // </summary>
410 public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps)
411 {
412 m_log.DebugFormat("[VivoxVoice] OnRegisterCaps: agentID {0} caps {1}", agentID, caps);
413  
414 string capsBase = "/CAPS/" + caps.CapsObjectPath;
415  
416 caps.RegisterHandler(
417 "ProvisionVoiceAccountRequest",
418 new RestStreamHandler(
419 "POST",
420 capsBase + m_provisionVoiceAccountRequestPath,
421 (request, path, param, httpRequest, httpResponse)
422 => ProvisionVoiceAccountRequest(scene, request, path, param, agentID, caps),
423 "ProvisionVoiceAccountRequest",
424 agentID.ToString()));
425  
426 caps.RegisterHandler(
427 "ParcelVoiceInfoRequest",
428 new RestStreamHandler(
429 "POST",
430 capsBase + m_parcelVoiceInfoRequestPath,
431 (request, path, param, httpRequest, httpResponse)
432 => ParcelVoiceInfoRequest(scene, request, path, param, agentID, caps),
433 "ParcelVoiceInfoRequest",
434 agentID.ToString()));
435  
436 //caps.RegisterHandler(
437 // "ChatSessionRequest",
438 // new RestStreamHandler(
439 // "POST",
440 // capsBase + m_chatSessionRequestPath,
441 // (request, path, param, httpRequest, httpResponse)
442 // => ChatSessionRequest(scene, request, path, param, agentID, caps),
443 // "ChatSessionRequest",
444 // agentID.ToString()));
445 }
446  
447 /// <summary>
448 /// Callback for a client request for Voice Account Details
449 /// </summary>
450 /// <param name="scene">current scene object of the client</param>
451 /// <param name="request"></param>
452 /// <param name="path"></param>
453 /// <param name="param"></param>
454 /// <param name="agentID"></param>
455 /// <param name="caps"></param>
456 /// <returns></returns>
457 public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param,
458 UUID agentID, Caps caps)
459 {
460 try
461 {
462 ScenePresence avatar = null;
463 string avatarName = null;
464  
465 if (scene == null)
466 throw new Exception("[VivoxVoice][PROVISIONVOICE]: Invalid scene");
467  
468 avatar = scene.GetScenePresence(agentID);
469 while (avatar == null)
470 {
471 Thread.Sleep(100);
472 avatar = scene.GetScenePresence(agentID);
473 }
474  
475 avatarName = avatar.Name;
476  
477 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: scene = {0}, agentID = {1}", scene, agentID);
478 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: request: {0}, path: {1}, param: {2}",
479 request, path, param);
480  
481 XmlElement resp;
482 bool retry = false;
483 string agentname = "x" + Convert.ToBase64String(agentID.GetBytes());
484 string password = new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16);
485 string code = String.Empty;
486  
487 agentname = agentname.Replace('+', '-').Replace('/', '_');
488  
489 do
490 {
491 resp = VivoxGetAccountInfo(agentname);
492  
493 if (XmlFind(resp, "response.level0.status", out code))
494 {
495 if (code != "OK")
496 {
497 if (XmlFind(resp, "response.level0.body.code", out code))
498 {
499 // If the request was recognized, then this should be set to something
500 switch (code)
501 {
502 case "201" : // Account expired
503 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : expired credentials",
504 avatarName);
505 m_adminConnected = false;
506 retry = DoAdminLogin();
507 break;
508  
509 case "202" : // Missing credentials
510 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : missing credentials",
511 avatarName);
512 break;
513  
514 case "212" : // Not authorized
515 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : not authorized",
516 avatarName);
517 break;
518  
519 case "300" : // Required parameter missing
520 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : parameter missing",
521 avatarName);
522 break;
523  
524 case "403" : // Account does not exist
525 resp = VivoxCreateAccount(agentname,password);
526 // Note: This REALLY MUST BE status. Create Account does not return code.
527 if (XmlFind(resp, "response.level0.status", out code))
528 {
529 switch (code)
530 {
531 case "201" : // Account expired
532 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : expired credentials",
533 avatarName);
534 m_adminConnected = false;
535 retry = DoAdminLogin();
536 break;
537  
538 case "202" : // Missing credentials
539 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : missing credentials",
540 avatarName);
541 break;
542  
543 case "212" : // Not authorized
544 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : not authorized",
545 avatarName);
546 break;
547  
548 case "300" : // Required parameter missing
549 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : parameter missing",
550 avatarName);
551 break;
552  
553 case "400" : // Create failed
554 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : create failed",
555 avatarName);
556 break;
557 }
558 }
559 break;
560  
561 case "404" : // Failed to retrieve account
562 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : retrieve failed");
563 // [AMW] Sleep and retry for a fixed period? Or just abandon?
564 break;
565 }
566 }
567 }
568 }
569 }
570 while (retry);
571  
572 if (code != "OK")
573 {
574 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: Get Account Request failed for \"{0}\"", avatarName);
575 throw new Exception("Unable to execute request");
576 }
577  
578 // Unconditionally change the password on each request
579 VivoxPassword(agentname, password);
580  
581 LLSDVoiceAccountResponse voiceAccountResponse =
582 new LLSDVoiceAccountResponse(agentname, password, m_vivoxSipUri, m_vivoxVoiceAccountApi);
583  
584 string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse);
585  
586 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r);
587  
588 return r;
589 }
590 catch (Exception e)
591 {
592 m_log.ErrorFormat("[VivoxVoice][PROVISIONVOICE]: : {0}, retry later", e.Message);
593 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: : {0} failed", e.ToString());
594 return "<llsd><undef /></llsd>";
595 }
596 }
597  
598 /// <summary>
599 /// Callback for a client request for ParcelVoiceInfo
600 /// </summary>
601 /// <param name="scene">current scene object of the client</param>
602 /// <param name="request"></param>
603 /// <param name="path"></param>
604 /// <param name="param"></param>
605 /// <param name="agentID"></param>
606 /// <param name="caps"></param>
607 /// <returns></returns>
608 public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param,
609 UUID agentID, Caps caps)
610 {
611 ScenePresence avatar = scene.GetScenePresence(agentID);
612 string avatarName = avatar.Name;
613  
614 // - check whether we have a region channel in our cache
615 // - if not:
616 // create it and cache it
617 // - send it to the client
618 // - send channel_uri: as "sip:regionID@m_sipDomain"
619 try
620 {
621 LLSDParcelVoiceInfoResponse parcelVoiceInfo;
622 string channel_uri;
623  
624 if (null == scene.LandChannel)
625 throw new Exception(String.Format("region \"{0}\": avatar \"{1}\": land data not yet available",
626 scene.RegionInfo.RegionName, avatarName));
627  
628 // get channel_uri: check first whether estate
629 // settings allow voice, then whether parcel allows
630 // voice, if all do retrieve or obtain the parcel
631 // voice channel
632 LandData land = scene.GetLandData(avatar.AbsolutePosition);
633  
634 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": request: {4}, path: {5}, param: {6}",
635 scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, request, path, param);
636 // m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: avatar \"{0}\": location: {1} {2} {3}",
637 // avatarName, avatar.AbsolutePosition.X, avatar.AbsolutePosition.Y, avatar.AbsolutePosition.Z);
638  
639 // TODO: EstateSettings don't seem to get propagated...
640 if (!scene.RegionInfo.EstateSettings.AllowVoice)
641 {
642 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": voice not enabled in estate settings",
643 scene.RegionInfo.RegionName);
644 channel_uri = String.Empty;
645 }
646  
647 if ((land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0)
648 {
649 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel",
650 scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName);
651 channel_uri = String.Empty;
652 }
653 else
654 {
655 channel_uri = RegionGetOrCreateChannel(scene, land);
656 }
657  
658 // fill in our response to the client
659 Hashtable creds = new Hashtable();
660 creds["channel_uri"] = channel_uri;
661  
662 parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds);
663 string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo);
664  
665 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}",
666 scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r);
667 return r;
668 }
669 catch (Exception e)
670 {
671 m_log.ErrorFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2}, retry later",
672 scene.RegionInfo.RegionName, avatarName, e.Message);
673 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2} failed",
674 scene.RegionInfo.RegionName, avatarName, e.ToString());
675  
676 return "<llsd><undef /></llsd>";
677 }
678 }
679  
680 /// <summary>
681 /// Callback for a client request for a private chat channel
682 /// </summary>
683 /// <param name="scene">current scene object of the client</param>
684 /// <param name="request"></param>
685 /// <param name="path"></param>
686 /// <param name="param"></param>
687 /// <param name="agentID"></param>
688 /// <param name="caps"></param>
689 /// <returns></returns>
690 public string ChatSessionRequest(Scene scene, string request, string path, string param,
691 UUID agentID, Caps caps)
692 {
693 ScenePresence avatar = scene.GetScenePresence(agentID);
694 string avatarName = avatar.Name;
695  
696 m_log.DebugFormat("[VivoxVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}",
697 avatarName, request, path, param);
698 return "<llsd>true</llsd>";
699 }
700  
701 private string RegionGetOrCreateChannel(Scene scene, LandData land)
702 {
703 string channelUri = null;
704 string channelId = null;
705  
706 string landUUID;
707 string landName;
708 string parentId;
709  
710 lock (m_parents)
711 parentId = m_parents[scene.RegionInfo.RegionID.ToString()];
712  
713 // Create parcel voice channel. If no parcel exists, then the voice channel ID is the same
714 // as the directory ID. Otherwise, it reflects the parcel's ID.
715 if (land.LocalID != 1 && (land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) == 0)
716 {
717 landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name);
718 landUUID = land.GlobalID.ToString();
719 m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}",
720 landName, land.LocalID, landUUID);
721 }
722 else
723 {
724 landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName);
725 landUUID = scene.RegionInfo.RegionID.ToString();
726 m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}",
727 landName, land.LocalID, landUUID);
728 }
729  
730 lock (vlock)
731 {
732 // Added by Adam to help debug channel not availible errors.
733 if (VivoxTryGetChannel(parentId, landUUID, out channelId, out channelUri))
734 m_log.DebugFormat("[VivoxVoice] Found existing channel at " + channelUri);
735 else if (VivoxTryCreateChannel(parentId, landUUID, landName, out channelUri))
736 m_log.DebugFormat("[VivoxVoice] Created new channel at " + channelUri);
737 else
738 throw new Exception("vivox channel uri not available");
739  
740 m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parent channel id {1}: retrieved parcel channel_uri {2} ",
741 landName, parentId, channelUri);
742 }
743  
744 return channelUri;
745 }
746  
747 private static readonly string m_vivoxLoginPath = "http://{0}/api2/viv_signin.php?userid={1}&pwd={2}";
748  
749 /// <summary>
750 /// Perform administrative login for Vivox.
751 /// Returns a hash table containing values returned from the request.
752 /// </summary>
753 private XmlElement VivoxLogin(string name, string password)
754 {
755 string requrl = String.Format(m_vivoxLoginPath, m_vivoxServer, name, password);
756 return VivoxCall(requrl, false);
757 }
758  
759 private static readonly string m_vivoxLogoutPath = "http://{0}/api2/viv_signout.php?auth_token={1}";
760  
761 /// <summary>
762 /// Perform administrative logout for Vivox.
763 /// </summary>
764 private XmlElement VivoxLogout()
765 {
766 string requrl = String.Format(m_vivoxLogoutPath, m_vivoxServer, m_authToken);
767 return VivoxCall(requrl, false);
768 }
769  
770 private static readonly string m_vivoxGetAccountPath = "http://{0}/api2/viv_get_acct.php?auth_token={1}&user_name={2}";
771  
772 /// <summary>
773 /// Retrieve account information for the specified user.
774 /// Returns a hash table containing values returned from the request.
775 /// </summary>
776 private XmlElement VivoxGetAccountInfo(string user)
777 {
778 string requrl = String.Format(m_vivoxGetAccountPath, m_vivoxServer, m_authToken, user);
779 return VivoxCall(requrl, true);
780 }
781  
782 private static readonly string m_vivoxNewAccountPath = "http://{0}/api2/viv_adm_acct_new.php?username={1}&pwd={2}&auth_token={3}";
783  
784 /// <summary>
785 /// Creates a new account.
786 /// For now we supply the minimum set of values, which
787 /// is user name and password. We *can* supply a lot more
788 /// demographic data.
789 /// </summary>
790 private XmlElement VivoxCreateAccount(string user, string password)
791 {
792 string requrl = String.Format(m_vivoxNewAccountPath, m_vivoxServer, user, password, m_authToken);
793 return VivoxCall(requrl, true);
794 }
795  
796 private static readonly string m_vivoxPasswordPath = "http://{0}/api2/viv_adm_password.php?user_name={1}&new_pwd={2}&auth_token={3}";
797  
798 /// <summary>
799 /// Change the user's password.
800 /// </summary>
801 private XmlElement VivoxPassword(string user, string password)
802 {
803 string requrl = String.Format(m_vivoxPasswordPath, m_vivoxServer, user, password, m_authToken);
804 return VivoxCall(requrl, true);
805 }
806  
807 private static readonly string m_vivoxChannelPath = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_name={2}&auth_token={3}";
808  
809 /// <summary>
810 /// Create a channel.
811 /// Once again, there a multitude of options possible. In the simplest case
812 /// we specify only the name and get a non-persistent cannel in return. Non
813 /// persistent means that the channel gets deleted if no-one uses it for
814 /// 5 hours. To accomodate future requirements, it may be a good idea to
815 /// initially create channels under the umbrella of a parent ID based upon
816 /// the region name. That way we have a context for side channels, if those
817 /// are required in a later phase.
818 ///
819 /// In this case the call handles parent and description as optional values.
820 /// </summary>
821 private bool VivoxTryCreateChannel(string parent, string channelId, string description, out string channelUri)
822 {
823 string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", channelId, m_authToken);
824  
825 if (!string.IsNullOrEmpty(parent))
826 {
827 requrl = String.Format("{0}&chan_parent={1}", requrl, parent);
828 }
829 if (!string.IsNullOrEmpty(description))
830 {
831 requrl = String.Format("{0}&chan_desc={1}", requrl, description);
832 }
833  
834 requrl = String.Format("{0}&chan_type={1}", requrl, m_vivoxChannelType);
835 requrl = String.Format("{0}&chan_mode={1}", requrl, m_vivoxChannelMode);
836 requrl = String.Format("{0}&chan_roll_off={1}", requrl, m_vivoxChannelRollOff);
837 requrl = String.Format("{0}&chan_dist_model={1}", requrl, m_vivoxChannelDistanceModel);
838 requrl = String.Format("{0}&chan_max_range={1}", requrl, m_vivoxChannelMaximumRange);
839 requrl = String.Format("{0}&chan_clamping_distance={1}", requrl, m_vivoxChannelClampingDistance);
840  
841 XmlElement resp = VivoxCall(requrl, true);
842 if (XmlFind(resp, "response.level0.body.chan_uri", out channelUri))
843 return true;
844  
845 channelUri = String.Empty;
846 return false;
847 }
848  
849 /// <summary>
850 /// Create a directory.
851 /// Create a channel with an unconditional type of "dir" (indicating directory).
852 /// This is used to create an arbitrary name tree for partitioning of the
853 /// channel name space.
854 /// The parent and description are optional values.
855 /// </summary>
856 private bool VivoxTryCreateDirectory(string dirId, string description, out string channelId)
857 {
858 string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", dirId, m_authToken);
859  
860 // if (parent != null && parent != String.Empty)
861 // {
862 // requrl = String.Format("{0}&chan_parent={1}", requrl, parent);
863 // }
864  
865 if (!string.IsNullOrEmpty(description))
866 {
867 requrl = String.Format("{0}&chan_desc={1}", requrl, description);
868 }
869 requrl = String.Format("{0}&chan_type={1}", requrl, "dir");
870  
871 XmlElement resp = VivoxCall(requrl, true);
872 if (IsOK(resp) && XmlFind(resp, "response.level0.body.chan_id", out channelId))
873 return true;
874  
875 channelId = String.Empty;
876 return false;
877 }
878  
879 private static readonly string m_vivoxChannelSearchPath = "http://{0}/api2/viv_chan_search.php?cond_channame={1}&auth_token={2}";
880  
881 /// <summary>
882 /// Retrieve a channel.
883 /// Once again, there a multitude of options possible. In the simplest case
884 /// we specify only the name and get a non-persistent cannel in return. Non
885 /// persistent means that the channel gets deleted if no-one uses it for
886 /// 5 hours. To accomodate future requirements, it may be a good idea to
887 /// initially create channels under the umbrella of a parent ID based upon
888 /// the region name. That way we have a context for side channels, if those
889 /// are required in a later phase.
890 /// In this case the call handles parent and description as optional values.
891 /// </summary>
892 private bool VivoxTryGetChannel(string channelParent, string channelName,
893 out string channelId, out string channelUri)
894 {
895 string count;
896  
897 string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, channelName, m_authToken);
898 XmlElement resp = VivoxCall(requrl, true);
899  
900 if (XmlFind(resp, "response.level0.channel-search.count", out count))
901 {
902 int channels = Convert.ToInt32(count);
903  
904 // Bug in Vivox Server r2978 where count returns 0
905 // Found by Adam
906 if (channels == 0)
907 {
908 for (int j=0;j<100;j++)
909 {
910 string tmpId;
911 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", j, out tmpId))
912 break;
913  
914 channels = j + 1;
915 }
916 }
917  
918 for (int i = 0; i < channels; i++)
919 {
920 string name;
921 string id;
922 string type;
923 string uri;
924 string parent;
925  
926 // skip if not a channel
927 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) ||
928 (type != "channel" && type != "positional_M"))
929 {
930 m_log.Debug("[VivoxVoice] Skipping Channel " + i + " as it's not a channel.");
931 continue;
932 }
933  
934 // skip if not the name we are looking for
935 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) ||
936 name != channelName)
937 {
938 m_log.Debug("[VivoxVoice] Skipping Channel " + i + " as it has no name.");
939 continue;
940 }
941  
942 // skip if parent does not match
943 if (channelParent != null && !XmlFind(resp, "response.level0.channel-search.channels.channels.level4.parent", i, out parent))
944 {
945 m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it's parent doesnt match");
946 continue;
947 }
948  
949 // skip if no channel id available
950 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id))
951 {
952 m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it has no channel ID");
953 continue;
954 }
955  
956 // skip if no channel uri available
957 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.uri", i, out uri))
958 {
959 m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it has no channel URI");
960 continue;
961 }
962  
963 channelId = id;
964 channelUri = uri;
965  
966 return true;
967 }
968 }
969 else
970 {
971 m_log.Debug("[VivoxVoice] No count element?");
972 }
973  
974 channelId = String.Empty;
975 channelUri = String.Empty;
976  
977 // Useful incase something goes wrong.
978 //m_log.Debug("[VivoxVoice] Could not find channel in XMLRESP: " + resp.InnerXml);
979  
980 return false;
981 }
982  
983 private bool VivoxTryGetDirectory(string directoryName, out string directoryId)
984 {
985 string count;
986  
987 string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, directoryName, m_authToken);
988 XmlElement resp = VivoxCall(requrl, true);
989  
990 if (XmlFind(resp, "response.level0.channel-search.count", out count))
991 {
992 int channels = Convert.ToInt32(count);
993 for (int i = 0; i < channels; i++)
994 {
995 string name;
996 string id;
997 string type;
998  
999 // skip if not a directory
1000 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) ||
1001 type != "dir")
1002 continue;
1003  
1004 // skip if not the name we are looking for
1005 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) ||
1006 name != directoryName)
1007 continue;
1008  
1009 // skip if no channel id available
1010 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id))
1011 continue;
1012  
1013 directoryId = id;
1014 return true;
1015 }
1016 }
1017  
1018 directoryId = String.Empty;
1019 return false;
1020 }
1021  
1022 // private static readonly string m_vivoxChannelById = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}";
1023  
1024 // private XmlElement VivoxGetChannelById(string parent, string channelid)
1025 // {
1026 // string requrl = String.Format(m_vivoxChannelById, m_vivoxServer, "get", channelid, m_authToken);
1027  
1028 // if (parent != null && parent != String.Empty)
1029 // return VivoxGetChild(parent, channelid);
1030 // else
1031 // return VivoxCall(requrl, true);
1032 // }
1033  
1034 private static readonly string m_vivoxChannelDel = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}";
1035  
1036 /// <summary>
1037 /// Delete a channel.
1038 /// Once again, there a multitude of options possible. In the simplest case
1039 /// we specify only the name and get a non-persistent cannel in return. Non
1040 /// persistent means that the channel gets deleted if no-one uses it for
1041 /// 5 hours. To accomodate future requirements, it may be a good idea to
1042 /// initially create channels under the umbrella of a parent ID based upon
1043 /// the region name. That way we have a context for side channels, if those
1044 /// are required in a later phase.
1045 /// In this case the call handles parent and description as optional values.
1046 /// </summary>
1047 private XmlElement VivoxDeleteChannel(string parent, string channelid)
1048 {
1049 string requrl = String.Format(m_vivoxChannelDel, m_vivoxServer, "delete", channelid, m_authToken);
1050 if (!string.IsNullOrEmpty(parent))
1051 {
1052 requrl = String.Format("{0}&chan_parent={1}", requrl, parent);
1053 }
1054 return VivoxCall(requrl, true);
1055 }
1056  
1057 private static readonly string m_vivoxChannelSearch = "http://{0}/api2/viv_chan_search.php?&cond_chanparent={1}&auth_token={2}";
1058  
1059 /// <summary>
1060 /// Return information on channels in the given directory
1061 /// </summary>
1062 private XmlElement VivoxListChildren(string channelid)
1063 {
1064 string requrl = String.Format(m_vivoxChannelSearch, m_vivoxServer, channelid, m_authToken);
1065 return VivoxCall(requrl, true);
1066 }
1067  
1068 // private XmlElement VivoxGetChild(string parent, string child)
1069 // {
1070  
1071 // XmlElement children = VivoxListChildren(parent);
1072 // string count;
1073  
1074 // if (XmlFind(children, "response.level0.channel-search.count", out count))
1075 // {
1076 // int cnum = Convert.ToInt32(count);
1077 // for (int i = 0; i < cnum; i++)
1078 // {
1079 // string name;
1080 // string id;
1081 // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.name", i, out name))
1082 // {
1083 // if (name == child)
1084 // {
1085 // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id))
1086 // {
1087 // return VivoxGetChannelById(null, id);
1088 // }
1089 // }
1090 // }
1091 // }
1092 // }
1093  
1094 // // One we *know* does not exist.
1095 // return VivoxGetChannel(null, Guid.NewGuid().ToString());
1096  
1097 // }
1098  
1099 /// <summary>
1100 /// This method handles the WEB side of making a request over the
1101 /// Vivox interface. The returned values are tansferred to a has
1102 /// table which is returned as the result.
1103 /// The outcome of the call can be determined by examining the
1104 /// status value in the hash table.
1105 /// </summary>
1106 private XmlElement VivoxCall(string requrl, bool admin)
1107 {
1108  
1109 XmlDocument doc = null;
1110  
1111 // If this is an admin call, and admin is not connected,
1112 // and the admin id cannot be connected, then fail.
1113 if (admin && !m_adminConnected && !DoAdminLogin())
1114 return null;
1115  
1116 doc = new XmlDocument();
1117  
1118 // Let's serialize all calls to Vivox. Most of these are driven by
1119 // the clients (CAPs), when the user arrives at the region. We don't
1120 // want to issue many simultaneous http requests to Vivox, because mono
1121 // doesn't like that
1122 lock (m_Lock)
1123 {
1124 try
1125 {
1126 // Otherwise prepare the request
1127 m_log.DebugFormat("[VivoxVoice] Sending request <{0}>", requrl);
1128  
1129 HttpWebRequest req = (HttpWebRequest)WebRequest.Create(requrl);
1130  
1131 // We are sending just parameters, no content
1132 req.ContentLength = 0;
1133  
1134 // Send request and retrieve the response
1135 using (HttpWebResponse rsp = (HttpWebResponse)req.GetResponse())
1136 using (Stream s = rsp.GetResponseStream())
1137 using (XmlTextReader rdr = new XmlTextReader(s))
1138 doc.Load(rdr);
1139 }
1140 catch (Exception e)
1141 {
1142 m_log.ErrorFormat("[VivoxVoice] Error in admin call : {0}", e.Message);
1143 }
1144 }
1145  
1146 // If we're debugging server responses, dump the whole
1147 // load now
1148 if (m_dumpXml) XmlScanl(doc.DocumentElement,0);
1149  
1150 return doc.DocumentElement;
1151 }
1152  
1153 /// <summary>
1154 /// Just say if it worked.
1155 /// </summary>
1156 private bool IsOK(XmlElement resp)
1157 {
1158 string status;
1159 XmlFind(resp, "response.level0.status", out status);
1160 return (status == "OK");
1161 }
1162  
1163 /// <summary>
1164 /// Login has been factored in this way because it gets called
1165 /// from several places in the module, and we want it to work
1166 /// the same way each time.
1167 /// </summary>
1168 private bool DoAdminLogin()
1169 {
1170 m_log.Debug("[VivoxVoice] Establishing admin connection");
1171  
1172 lock (vlock)
1173 {
1174 if (!m_adminConnected)
1175 {
1176 string status = "Unknown";
1177 XmlElement resp = null;
1178  
1179 resp = VivoxLogin(m_vivoxAdminUser, m_vivoxAdminPassword);
1180  
1181 if (XmlFind(resp, "response.level0.body.status", out status))
1182 {
1183 if (status == "Ok")
1184 {
1185 m_log.Info("[VivoxVoice] Admin connection established");
1186 if (XmlFind(resp, "response.level0.body.auth_token", out m_authToken))
1187 {
1188 if (m_dumpXml) m_log.DebugFormat("[VivoxVoice] Auth Token <{0}>",
1189 m_authToken);
1190 m_adminConnected = true;
1191 }
1192 }
1193 else
1194 {
1195 m_log.WarnFormat("[VivoxVoice] Admin connection failed, status = {0}",
1196 status);
1197 }
1198 }
1199 }
1200 }
1201  
1202 return m_adminConnected;
1203 }
1204  
1205 /// <summary>
1206 /// The XmlScan routine is provided to aid in the
1207 /// reverse engineering of incompletely
1208 /// documented packets returned by the Vivox
1209 /// voice server. It is only called if the
1210 /// m_dumpXml switch is set.
1211 /// </summary>
1212 private void XmlScanl(XmlElement e, int index)
1213 {
1214 if (e.HasChildNodes)
1215 {
1216 m_log.DebugFormat("<{0}>".PadLeft(index+5), e.Name);
1217 XmlNodeList children = e.ChildNodes;
1218 foreach (XmlNode node in children)
1219 switch (node.NodeType)
1220 {
1221 case XmlNodeType.Element :
1222 XmlScanl((XmlElement)node, index+1);
1223 break;
1224 case XmlNodeType.Text :
1225 m_log.DebugFormat("\"{0}\"".PadLeft(index+5), node.Value);
1226 break;
1227 default :
1228 break;
1229 }
1230 m_log.DebugFormat("</{0}>".PadLeft(index+6), e.Name);
1231 }
1232 else
1233 {
1234 m_log.DebugFormat("<{0}/>".PadLeft(index+6), e.Name);
1235 }
1236 }
1237  
1238 private static readonly char[] C_POINT = {'.'};
1239  
1240 /// <summary>
1241 /// The Find method is passed an element whose
1242 /// inner text is scanned in an attempt to match
1243 /// the name hierarchy passed in the 'tag' parameter.
1244 /// If the whole hierarchy is resolved, the InnerText
1245 /// value at that point is returned. Note that this
1246 /// may itself be a subhierarchy of the entire
1247 /// document. The function returns a boolean indicator
1248 /// of the search's success. The search is performed
1249 /// by the recursive Search method.
1250 /// </summary>
1251 private bool XmlFind(XmlElement root, string tag, int nth, out string result)
1252 {
1253 if (root == null || tag == null || tag == String.Empty)
1254 {
1255 result = String.Empty;
1256 return false;
1257 }
1258 return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result);
1259 }
1260  
1261 private bool XmlFind(XmlElement root, string tag, out string result)
1262 {
1263 int nth = 0;
1264 if (root == null || tag == null || tag == String.Empty)
1265 {
1266 result = String.Empty;
1267 return false;
1268 }
1269 return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result);
1270 }
1271  
1272 /// <summary>
1273 /// XmlSearch is initially called by XmlFind, and then
1274 /// recursively called by itself until the document
1275 /// supplied to XmlFind is either exhausted or the name hierarchy
1276 /// is matched.
1277 ///
1278 /// If the hierarchy is matched, the value is returned in
1279 /// result, and true returned as the function's
1280 /// value. Otherwise the result is set to the empty string and
1281 /// false is returned.
1282 /// </summary>
1283 private bool XmlSearch(XmlElement e, string[] tags, int index, ref int nth, out string result)
1284 {
1285 if (index == tags.Length || e.Name != tags[index])
1286 {
1287 result = String.Empty;
1288 return false;
1289 }
1290  
1291 if (tags.Length-index == 1)
1292 {
1293 if (nth == 0)
1294 {
1295 result = e.InnerText;
1296 return true;
1297 }
1298 else
1299 {
1300 nth--;
1301 result = String.Empty;
1302 return false;
1303 }
1304 }
1305  
1306 if (e.HasChildNodes)
1307 {
1308 XmlNodeList children = e.ChildNodes;
1309 foreach (XmlNode node in children)
1310 {
1311 switch (node.NodeType)
1312 {
1313 case XmlNodeType.Element :
1314 if (XmlSearch((XmlElement)node, tags, index+1, ref nth, out result))
1315 return true;
1316 break;
1317  
1318 default :
1319 break;
1320 }
1321 }
1322 }
1323  
1324 result = String.Empty;
1325 return false;
1326 }
1327 }
1328 }