aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs
diff options
context:
space:
mode:
authorDr Scofield2008-07-02 09:02:30 +0000
committerDr Scofield2008-07-02 09:02:30 +0000
commitd40bea4a8e09be1f8e87cf41405aaa60fa8826cb (patch)
tree91ef9356d8b284ac6fa5f0d588fedebe723b69ad /OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs
parentMantis#1643. Thank you Melanie for a patch that: (diff)
downloadopensim-SC_OLD-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.zip
opensim-SC_OLD-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.gz
opensim-SC_OLD-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.bz2
opensim-SC_OLD-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.xz
From: Alan M Webb <awebb@vnet.ibm.com>
This adds REST services for inventory access. It also allows inventory uploads.
Diffstat (limited to 'OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs')
-rw-r--r--OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs1993
1 files changed, 1993 insertions, 0 deletions
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs
new file mode 100644
index 0000000..0fc10f9
--- /dev/null
+++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs
@@ -0,0 +1,1993 @@
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
29using System;
30using System.Collections.Generic;
31using System.IO;
32using System.Threading;
33using System.Xml;
34using OpenSim.Framework;
35using OpenSim.Framework.Servers;
36using OpenSim.Framework.Communications;
37using OpenSim.Framework.Communications.Cache;
38using libsecondlife;
39using Nini.Config;
40
41namespace OpenSim.ApplicationPlugins.Rest.Inventory
42{
43
44 public class RestInventoryServices : IRest
45 {
46
47 private string key = "inventory";
48 private bool enabled = false;
49 private string qPrefix = "inventory";
50
51 // A simple constructor is used to handle any once-only
52 // initialization of working classes.
53
54 public RestInventoryServices(RestHandler p_rest)
55 {
56
57 Rest.Log.InfoFormat("{0} Inventory services initializing", MsgId);
58 Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version);
59
60 // Update to reflect the full prefix if not absolute
61
62 if (!qPrefix.StartsWith(Rest.UrlPathSeparator))
63 {
64 qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix;
65 }
66
67 // Authentication domain
68
69 Rest.Domains.Add(key, Rest.Config.GetString("inventory-domain",qPrefix));
70
71 // Register interface
72
73 Rest.Plugin.AddPathHandler(DoInventory,qPrefix,Allocate);
74
75 // Activate
76
77 enabled = true;
78
79 Rest.Log.InfoFormat("{0} Inventory services initialization complete", MsgId);
80
81 }
82
83 // Post-construction, pre-enabled initialization opportunity
84 // Not currently exploited.
85
86 public void Initialize()
87 {
88 }
89
90 // Called by the plug-in to halt REST processing. Local processing is
91 // disabled, and control blocks until all current processing has
92 // completed. No new processing will be started
93
94 public void Close()
95 {
96 enabled = false;
97 Rest.Log.InfoFormat("{0} Inventory services closing down", MsgId);
98 }
99
100 // Convenient properties
101
102 internal string MsgId
103 {
104 get { return Rest.MsgId; }
105 }
106
107 #region Interface
108
109 private RequestData Allocate(OSHttpRequest request, OSHttpResponse response)
110 {
111 return (RequestData) new InventoryRequestData(request, response, qPrefix);
112 }
113
114 /// <summary>
115 /// This method is registered with the handler when this class is
116 /// initialized. It is called whenever the URI includes this handler's
117 /// prefix string.
118 /// It handles all aspects of inventory REST processing.
119 /// </summary>
120
121 private void DoInventory(RequestData hdata)
122 {
123
124 InventoryRequestData rdata = (InventoryRequestData) hdata;
125
126 Rest.Log.DebugFormat("{0} DoInventory ENTRY", MsgId);
127
128 // We're disabled
129 if (!enabled)
130 {
131 return;
132 }
133
134 // Now that we know this is a serious attempt to
135 // access inventory data, we should find out who
136 // is asking, and make sure they are authorized
137 // to do so. We need to validate the caller's
138 // identity before revealing anything about the
139 // status quo. Authenticate throws an exception
140 // via Fail if no identity information is present.
141 //
142 // With the present HTTP server we can't use the
143 // builtin authentication mechanisms because they
144 // would be enforced for all in-bound requests.
145 // Instead we look at the headers ourselves and
146 // handle authentication directly.
147
148 try
149 {
150 if (!rdata.IsAuthenticated)
151 {
152 rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized);
153 }
154 }
155 catch (RestException e)
156 {
157 if (e.statusCode == Rest.HttpStatusCodeNotAuthorized)
158 {
159 Rest.Log.WarnFormat("{0} User not authenticated", MsgId);
160 Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization"));
161 }
162 else
163 {
164 Rest.Log.ErrorFormat("{0} User authentication failed", MsgId);
165 Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization"));
166 }
167 throw (e);
168 }
169
170 Rest.Log.DebugFormat("{0} Authenticated {1}", MsgId, rdata.userName);
171
172 // We can only get here if we're authorized
173 //
174 // The requestor may have specified an LLUUID or
175 // a conjoined FirstNameLastName string. We'll
176 // try both. If we fail with the first, UUID,
177 // attempt, then we need two nodes to construct
178 // a valid avatar name.
179
180 // Do we have at least a user agent name?
181
182 if (rdata.parameters.Length < 1)
183 {
184 Rest.Log.WarnFormat("{0} Inventory: No user agent identifier specified", MsgId);
185 rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest);
186 }
187
188 // The next parameter MUST be the agent identification, either an LLUUID
189 // or a space-separated First-name Last-Name specification.
190
191 try
192 {
193 rdata.uuid = new LLUUID(rdata.parameters[0]);
194 Rest.Log.DebugFormat("{0} LLUUID supplied", MsgId);
195 rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid);
196 }
197 catch
198 {
199 string[] names = rdata.parameters[0].Split(Rest.CA_SPACE);
200 if (names.Length == 2)
201 {
202 Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId);
203 rdata.userProfile = Rest.UserServices.GetUserProfile(names[0],names[1]);
204 }
205 else
206 {
207 Rest.Log.DebugFormat("{0} A Valid UUID or both first and last names must be specified", MsgId);
208 rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest);
209 }
210 }
211
212 if (rdata.userProfile != null)
213 {
214 Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}",
215 MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
216 }
217 else
218 {
219 Rest.Log.DebugFormat("{0} No profile for {1}", MsgId, rdata.path);
220 rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound);
221 }
222
223 // If we get to here, then we have successfully obtained an inventory
224 // for the specified user.
225
226 rdata.uuid = rdata.userProfile.ID;
227
228 if (Rest.InventoryServices.HasInventoryForUser(rdata.uuid))
229 {
230
231 rdata.root = Rest.InventoryServices.RequestRootFolder(rdata.uuid);
232
233 Rest.Log.DebugFormat("{0} Inventory Root retrieved for {1} {2}",
234 MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
235
236 Rest.InventoryServices.RequestInventoryForUser(rdata.uuid, rdata.GetUserInventory);
237
238 Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}",
239 MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
240
241 lock(rdata)
242 {
243 if (!rdata.HaveInventory)
244 {
245 Monitor.Wait(rdata);
246 }
247 }
248
249 if (rdata.root == null)
250 {
251 Rest.Log.DebugFormat("{0} Inventory is not available [1] for agent {1} {2}",
252 MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
253 rdata.Fail(Rest.HttpStatusCodeServerError,Rest.HttpStatusDescServerError);
254 }
255
256 }
257 else
258 {
259 Rest.Log.DebugFormat("{0} Inventory is not available for agent [3] {1} {2}",
260 MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
261 rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound);
262 }
263
264 // If we get here, then we have successfully retrieved the user's information
265 // and inventory information is now available locally.
266
267 switch (rdata.method)
268 {
269
270 case Rest.HEAD : // Do the processing, set the status code, suppress entity
271 DoGet(rdata);
272 rdata.buffer = null;
273 break;
274
275 case Rest.GET : // Do the processing, set the status code, return entity
276 DoGet(rdata);
277 break;
278
279 case Rest.PUT : // Add new information
280 DoPut(rdata);
281 break;
282
283 case Rest.POST : // Update (replace)
284 DoPost(rdata);
285 break;
286
287 case Rest.DELETE : // Delete information
288 DoDelete(rdata);
289 break;
290
291 default :
292 Rest.Log.DebugFormat("{0} Method {1} not supported for {2}",
293 MsgId, rdata.method, rdata.path);
294 rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed,
295 Rest.HttpStatusDescMethodNotAllowed);
296 break;
297 }
298
299 }
300
301 #endregion Interface
302
303 #region method-specific processing
304
305 /// <summary>
306 /// This method implements GET processing for inventory.
307 /// Any remaining parameters are used to locate the
308 /// corresponding subtree based upon node name.
309 /// </summary>
310
311 private void DoGet(InventoryRequestData rdata)
312 {
313
314 rdata.initXmlWriter();
315
316 rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty);
317
318 if (rdata.parameters.Length == 1)
319 {
320 formatInventory(rdata, rdata.root, String.Empty);
321 }
322 else
323 {
324 traverseInventory(rdata, rdata.root, 1);
325 }
326
327 rdata.writer.WriteFullEndElement();
328
329 rdata.Complete();
330 rdata.Respond("Inventory " + rdata.method + ": Normal completion");
331
332 }
333
334 /// <summary>
335 /// In the case of the inventory, and probably much else
336 /// the distinction between PUT and POST is not always
337 /// easy to discern. Adding a directory can be viewed as
338 /// an addition, or as a modification to the inventory as
339 /// a whole.
340 ///
341 /// The best distinction may be the relationship between
342 /// the entity and the URI. If we view POST as an update,
343 /// then the enity represents a replacement for the
344 /// element named by the URI. If the operation is PUT,
345 /// then the URI describes the context into which the
346 /// entity will be added.
347 ///
348 /// As an example, suppose the URI contains:
349 /// /admin/inventory/Clothing
350 /// Suppose the entity represents a Folder, called
351 /// "Clothes".
352 ///
353 /// A POST request will result in the replacement of
354 /// "Clothing" by "Clothes". Whereas a PUT request
355 /// would add Clothes as a sub-directory of Clothing.
356 ///
357 /// This is the model followed by this implementation.
358 /// </summary>
359
360 /// <summary>
361 /// PUT adds new information to the inventory at the
362 /// context identified by the URI.
363 /// </summary>
364
365 private void DoPut(InventoryRequestData rdata)
366 {
367
368 // Resolve the context node specified in the URI. Entity
369 // data will be ADDED beneath this node.
370
371 Object InventoryNode = getInventoryNode(rdata, rdata.root, 1);
372
373 // Processing depends upon the type of inventory node
374 // identified in the URI. This is the CONTEXT for the
375 // change. We either got a context or we threw an
376 // exception.
377
378 // It follows that we can only add information if the URI
379 // has identified a folder. So only folder is supported
380 // in this case.
381
382 if (typeof(InventoryFolderBase) == InventoryNode.GetType() ||
383 typeof(InventoryFolderImpl) == InventoryNode.GetType())
384 {
385
386 // Cast the context node appropriately.
387
388 InventoryFolderBase context = (InventoryFolderBase) InventoryNode;
389
390 Rest.Log.DebugFormat("{0} {1}: Resource(s) will be added to folder {2}",
391 MsgId, rdata.method, rdata.path);
392
393 // Reconstitute inventory sub-tree from the XML supplied in the entity.
394 // This is a stand-alone inventory subtree, not yet integrated into the
395 // existing tree.
396
397 XmlInventoryCollection entity = ReconstituteEntity(rdata);
398
399 // Inlined assest included in entity. If anything fails,
400 // return failure to requestor.
401
402 if (entity.Assets.Count > 0)
403 {
404
405 Rest.Log.DebugFormat("{0} Adding {1} assets to server",
406 MsgId, entity.Assets.Count);
407
408 foreach (AssetBase asset in entity.Assets)
409 {
410 Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}",
411 MsgId, asset.ID, asset.Type, asset.Name);
412 Rest.AssetServices.AddAsset(asset);
413 if (Rest.DumpAsset)
414 {
415 Rest.Dump(asset.Data);
416 }
417 }
418
419 }
420
421 // Modify the context using the collection of folders and items
422 // returned in the XmlInventoryCollection.
423
424 foreach (InventoryFolderBase folder in entity.Folders)
425 {
426
427 InventoryFolderBase found = null;
428
429 // If the parentID is zero, then this is going
430 // into the root identified by the URI. The requestor
431 // may have already set the parent ID correctly, in which
432 // case we don't have to do it here.
433
434 if (folder.ParentID == LLUUID.Zero)
435 {
436 folder.ParentID = context.ID;
437 }
438
439 // Search the existing inventory for an existing entry. If
440 // we have once, we need to decide if it has really changed.
441 // It could just be present as (unnecessary) context, and we
442 // don't want to waste time updating the database in that
443 // case, OR, it could be being moved from another location
444 // in which case an update is most certainly necessary.
445
446 found = null;
447
448 foreach (InventoryFolderBase xf in rdata.folders)
449 {
450 // Compare identifying attribute
451 if (xf.ID == folder.ID)
452 {
453 found = xf;
454 }
455 }
456
457 if (found != null && FolderHasChanged(folder,found))
458 {
459 Rest.Log.DebugFormat("{0} Updating existing folder", MsgId);
460 Rest.InventoryServices.MoveFolder(folder);
461 }
462 else
463 {
464 Rest.Log.DebugFormat("{0} Adding new folder", MsgId);
465 Rest.InventoryServices.AddFolder(folder);
466 }
467
468 }
469
470 // Now we repeat a similar process for the items included
471 // in the entity.
472
473 foreach (InventoryItemBase item in entity.Items)
474 {
475
476 InventoryItemBase found = null;
477
478 // If the parentID is zero, then this is going
479 // directly into the root identified by the URI.
480
481 if (item.Folder == LLUUID.Zero)
482 {
483 item.Folder = context.ID;
484 }
485
486 // Determine whether this is a new item or a
487 // replacement definition.
488
489 foreach (InventoryItemBase xi in rdata.items)
490 {
491 // Compare identifying attribute
492 if (xi.ID == item.ID)
493 {
494 found = xi;
495 }
496 }
497
498 if (found != null && ItemHasChanged(item, found))
499 {
500 Rest.Log.DebugFormat("{0} Updating item {1} {2} {3} {4} {5}",
501 MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name);
502 Rest.InventoryServices.UpdateItem(item);
503 }
504 else
505 {
506 Rest.Log.DebugFormat("{0} Adding item {1} {2} {3} {4} {5}",
507 MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name);
508 Rest.InventoryServices.AddItem(item);
509 }
510
511 }
512
513 }
514 else
515 {
516 Rest.Log.DebugFormat("{0} {1}: Resource {2} is not a valid context: {3}",
517 MsgId, rdata.method, rdata.path, InventoryNode.GetType());
518 rdata.Fail(Rest.HttpStatusCodeBadRequest,
519 Rest.HttpStatusDescBadRequest);
520 }
521
522 rdata.Complete();
523 rdata.Respond("Inventory " + rdata.method + ": Normal completion");
524
525 }
526
527 /// <summary>
528 /// POST updates the URI-identified element in the inventory. This
529 /// is actually far more flexible than it might at first sound. For
530 /// POST the URI serves two purposes:
531 /// [1] It identifies the user whose inventory is to be
532 /// processed.
533 /// [2] It optionally specifies a subtree of the inventory
534 /// that is to be used to resolve an relative subtree
535 /// specifications in the entity. If nothing is specified
536 /// then the whole inventory is implied.
537 /// Please note that the subtree specified by the URI is only relevant
538 /// to an entity containing a URI relative specification, i.e. one or
539 /// more elements do not specify parent folder information. These
540 /// elements will be implicitly referenced within the context identified
541 /// by the URI.
542 /// If an element in the entity specifies an explicit parent folder, then
543 /// that parent is effective, regardless of nay value specified in the
544 /// URI. If the parent does not exist, then the element, and any dependent
545 /// elements, are ignored. This case is actually detected and handled
546 /// during the reconstitution process.
547 /// </summary>
548
549 private void DoPost(InventoryRequestData rdata)
550 {
551
552 int count = 0;
553
554 // Resolve the inventory node that is to be modified.
555
556 Object InventoryNode = getInventoryNode(rdata, rdata.root, 1);
557
558 // As long as we have a context, then we have something
559 // meaningful to do, unlike PUT. So reconstitute the
560 // subtree before doing anything else. Note that we
561 // etiher got a context or we threw an exception.
562
563 XmlInventoryCollection entity = ReconstituteEntity(rdata);
564
565 // Incorporate any inlined assets first
566
567 if (entity.Assets.Count != 0)
568 {
569 foreach (AssetBase asset in entity.Assets)
570 {
571 // Asset was validated during the collection
572 // process
573 Rest.AssetServices.AddAsset(asset);
574 }
575 }
576
577 /// <summary>
578 /// URI specifies a folder to be updated.
579 /// </summary>
580 /// <remarks>
581 /// The root node in the entity must have the same
582 /// UUID as the node identified by the URI. The
583 /// parentID if different indicates that the updated
584 /// folder is actually being moved too.
585 /// </remarks>
586
587 if (typeof(InventoryFolderBase) == InventoryNode.GetType() ||
588 typeof(InventoryFolderImpl) == InventoryNode.GetType())
589 {
590
591 InventoryFolderBase uri = (InventoryFolderBase) InventoryNode;
592 InventoryFolderBase xml = null;
593
594 // Scan the set of folders in the entity collection for an
595 // entry that macthes the context folder. It is assumed that
596 // the only reliable indicator of this is a zero UUID ( using
597 // implicit context), or the parent's UUID matches that of the
598 // URI designated node (explicit context). We don't allow
599 // ambiguity in this case because this is POST and we are
600 // supposed to be modifying a specific node.
601 // We assign any element IDs required as an economy; we don't
602 // want to iterate over the fodler set again if it can be
603 // helped.
604
605 foreach (InventoryFolderBase folder in entity.Folders)
606 {
607 if (folder.ParentID == uri.ParentID ||
608 folder.ParentID == LLUUID.Zero)
609 {
610 folder.ParentID = uri.ParentID;
611 xml = folder;
612 count++;
613 }
614 if (xml.ID == LLUUID.Zero)
615 {
616 xml.ID = LLUUID.Random();
617 }
618 }
619
620 // More than one entry is ambiguous
621
622 if (count > 1)
623 {
624 Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous",
625 MsgId, rdata.method, rdata.path);
626 rdata.Fail(Rest.HttpStatusCodeBadRequest,
627 Rest.HttpStatusDescBadRequest);
628 }
629
630 // Exactly one entry means we ARE replacing the node
631 // identified by the URI. So we delete the old folder
632 // by moving it to the trash and then purging it.
633 // We then add all of the folders and items we
634 // included in the entity. The subtree has been
635 // modified.
636
637 if (count == 1)
638 {
639
640 InventoryFolderBase TrashCan = GetTrashCan(rdata);
641
642 uri.ParentID = TrashCan.ID;
643 Rest.InventoryServices.MoveFolder(uri);
644 Rest.InventoryServices.PurgeFolder(TrashCan);
645
646 }
647
648 // Now, regardelss of what they represent, we
649 // integrate all of the elements in the entity.
650
651 foreach (InventoryFolderBase f in entity.Folders)
652 {
653 Rest.InventoryServices.MoveFolder(f);
654 }
655
656 foreach (InventoryItemBase it in entity.Items)
657 {
658 Rest.InventoryServices.AddItem(it);
659 }
660
661 }
662
663 /// <summary>
664 /// URI specifies an item to be updated
665 /// </summary>
666 /// <remarks>
667 /// The entity must contain a single item node to be
668 /// updated. ID and Folder ID must be correct.
669 /// </remarks>
670
671 else
672 {
673
674 InventoryItemBase uri = (InventoryItemBase) InventoryNode;
675 InventoryItemBase xml = null;
676
677 if (entity.Folders.Count != 0)
678 {
679 Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>",
680 MsgId, rdata.method, rdata.path);
681 rdata.Fail(Rest.HttpStatusCodeBadRequest,
682 Rest.HttpStatusDescBadRequest);
683 }
684
685 if (entity.Items.Count > 1)
686 {
687 Rest.Log.DebugFormat("{0} {1}: Entity contains too many items <{2}>",
688 MsgId, rdata.method, rdata.path);
689 rdata.Fail(Rest.HttpStatusCodeBadRequest,
690 Rest.HttpStatusDescBadRequest);
691 }
692
693 xml = entity.Items[0];
694
695 if (xml.ID == LLUUID.Zero)
696 {
697 xml.ID = LLUUID.Random();
698 }
699
700 // If the folder reference has changed, then this item is
701 // being moved. Otherwise we'll just delete the old, and
702 // add in the new.
703
704 // Delete the old item
705
706 Rest.InventoryServices.DeleteItem(uri);
707
708 // Add the new item to the inventory
709
710 Rest.InventoryServices.AddItem(xml);
711
712 }
713
714 rdata.Complete();
715 rdata.Respond("Inventory " + rdata.method + ": Normal completion");
716
717 }
718
719 /// <summary>
720 /// Arguably the most damaging REST interface. It deletes the inventory
721 /// item or folder identified by the URI.
722 ///
723 /// We only process if the URI identified node appears to exist
724 /// We do not test for success because we either get a context,
725 /// or an exception is thrown.
726 ///
727 /// Folders are deleted by moving them to another folder and then
728 /// purging that folder. We'll do that by creating a temporary
729 /// sub-folder in the TrashCan and purging that folder's
730 /// contents. If we can't can it, we don't delete it...
731 /// So, if no trashcan is available, the request does nothing.
732 /// Items are summarily deleted.
733 ///
734 /// In the interests of safety, a delete request should normally
735 /// be performed using UUID, as a name might identify several
736 /// elements.
737 /// </summary>
738
739 private void DoDelete(InventoryRequestData rdata)
740 {
741
742 Object InventoryNode = getInventoryNode(rdata, rdata.root, 1);
743
744 if (typeof(InventoryFolderBase) == InventoryNode.GetType() ||
745 typeof(InventoryFolderImpl) == InventoryNode.GetType())
746 {
747
748 InventoryFolderBase TrashCan = GetTrashCan(rdata);
749
750 InventoryFolderBase folder = (InventoryFolderBase) InventoryNode;
751 Rest.Log.DebugFormat("{0} {1}: Folder {2} will be deleted",
752 MsgId, rdata.method, rdata.path);
753 folder.ParentID = TrashCan.ID;
754 Rest.InventoryServices.MoveFolder(folder);
755 Rest.InventoryServices.PurgeFolder(TrashCan);
756
757 }
758
759 // Deleting items is much more straight forward.
760
761 else
762 {
763 InventoryItemBase item = (InventoryItemBase) InventoryNode;
764 Rest.Log.DebugFormat("{0} {1}: Item {2} will be deleted",
765 MsgId, rdata.method, rdata.path);
766 Rest.InventoryServices.DeleteItem(item);
767 }
768
769 rdata.Complete();
770 rdata.Respond("Inventory " + rdata.method + ": Normal completion");
771
772 }
773
774#endregion method-specific processing
775
776 /// <summary>
777 /// This method is called to obtain the OpenSim inventory object identified
778 /// by the supplied URI. This may be either an Item or a Folder, so a suitably
779 /// ambiguous return type is employed (Object). This method recurses as
780 /// necessary to process the designated hierarchy.
781 ///
782 /// If we reach the end of the URI then we return the contextural folder to
783 /// our caller.
784 ///
785 /// If we are not yet at the end of the URI we attempt to find a child folder
786 /// and if we succeed we recurse.
787 ///
788 /// If this is the last node, then we look to see if this is an item. If it is,
789 /// we return that item.
790 ///
791 /// Otherwise we fail the request on the ground of an invalid URI.
792 ///
793 /// <note>
794 /// This mechanism cannot detect the case where duplicate subtrees satisfy a
795 /// request. In such a case the 1st element gets processed. If this is a
796 /// problem, then UUID should be used to identify the end-node. This is basic
797 /// premise of normal inventory processing. The name is an informational, and
798 /// not a defining, attribute.
799 /// </note>
800 ///
801 /// </summary>
802
803 private Object getInventoryNode(InventoryRequestData rdata, InventoryFolderBase folder, int pi)
804 {
805
806 Rest.Log.DebugFormat("{0} Searching folder {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi);
807
808 // We have just run off the end of the parameter sequence
809
810 if (pi >= rdata.parameters.Length)
811 {
812 return folder;
813 }
814
815 // More names in the sequence, look for a folder that might
816 // get us there.
817
818 if (rdata.folders != null)
819 foreach (InventoryFolderBase f in rdata.folders)
820 {
821 // Look for the present node in the directory list
822 if (f.ParentID == folder.ID &&
823 (f.Name == rdata.parameters[pi] ||
824 f.ID.ToString() == rdata.parameters[pi]))
825 {
826 return getInventoryNode(rdata, f, pi+1);
827 }
828 }
829
830 // No folders that match. Perhaps this parameter identifies an item? If
831 // it does, then it MUST also be the last name in the sequence.
832
833 if (pi == rdata.parameters.Length-1)
834 {
835 if (rdata.items != null)
836 {
837 int k = 0;
838 InventoryItemBase li = null;
839 foreach (InventoryItemBase i in rdata.items)
840 {
841 if (i.Folder == folder.ID &&
842 (i.Name == rdata.parameters[pi] ||
843 i.ID.ToString() == rdata.parameters[pi]))
844 {
845 li = i;
846 k++;
847 }
848 }
849 if (k == 1)
850 {
851 return li;
852 }
853 else
854 {
855 Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous",
856 MsgId, rdata.method, rdata.path);
857 rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound);
858 }
859 }
860 }
861
862 // No, so abandon the request
863
864 Rest.Log.DebugFormat("{0} {1}: Resource {2} not found",
865 MsgId, rdata.method, rdata.path);
866 rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound);
867
868 return null; /* Never reached */
869
870 }
871
872 /// <summary>
873 /// This routine traverse the inventory's structure until the end-point identified
874 /// in the URI is reached, the remainder of the inventory (if any) is then formatted
875 /// and returned to the requestor.
876 ///
877 /// Note that this method is only interested in those folder that match elements of
878 /// the URI supplied by the requestor, so once a match is fund, the processing does
879 /// not need to consider any further elements.
880 ///
881 /// Only the last element in the URI should identify an item.
882 /// </summary>
883
884 private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi)
885 {
886
887 Rest.Log.DebugFormat("{0} Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi);
888
889 if (rdata.folders != null)
890 {
891 foreach (InventoryFolderBase f in rdata.folders)
892 {
893 if (f.ParentID == folder.ID &&
894 (f.Name == rdata.parameters[pi] ||
895 f.ID.ToString() == rdata.parameters[pi]))
896 {
897 if (pi < rdata.parameters.Length-1)
898 {
899 traverseInventory(rdata, f, pi+1);
900 }
901 else
902 {
903 formatInventory(rdata, f, String.Empty);
904 }
905 return;
906 }
907 }
908 }
909
910 if (pi == rdata.parameters.Length-1)
911 {
912 if (rdata.items != null)
913 {
914 foreach (InventoryItemBase i in rdata.items)
915 {
916 if (i.Folder == folder.ID &&
917 (i.Name == rdata.parameters[pi] ||
918 i.ID.ToString() == rdata.parameters[pi]))
919 {
920 // Fetching an Item has a special significance. In this
921 // case we also want to fetch the associated asset.
922 // To make it interesting, we'll d this via redirection.
923 string asseturl = "http://" + rdata.hostname + ":" + rdata.port +
924 "/admin/assets" + Rest.UrlPathSeparator + i.AssetID.ToString();
925 rdata.Redirect(asseturl,Rest.PERMANENT);
926 Rest.Log.DebugFormat("{0} Never Reached");
927 }
928 }
929 }
930 }
931
932 Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>",
933 MsgId, rdata.path);
934 rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound);
935
936 }
937
938 /// <summary>
939 /// This method generates XML that describes an instance of InventoryFolderBase.
940 /// It recurses as necessary to reflect a folder hierarchy, and calls formatItem
941 /// to generate XML for any items encountered along the way.
942 /// The indentation parameter is solely for the benefit of trace record
943 /// formatting.
944 /// </summary>
945
946 private void formatInventory(InventoryRequestData rdata, InventoryFolderBase folder, string indent)
947 {
948
949 if (Rest.DEBUG)
950 {
951 Rest.Log.DebugFormat("{0} Folder : {1} {2} {3}", MsgId, folder.ID, indent, folder.Name);
952 indent += "\t";
953 }
954
955 // Start folder item
956
957 rdata.writer.WriteStartElement(String.Empty,"Folder",String.Empty);
958 rdata.writer.WriteAttributeString("name",String.Empty,folder.Name);
959 rdata.writer.WriteAttributeString("uuid",String.Empty,folder.ID.ToString());
960 rdata.writer.WriteAttributeString("owner",String.Empty,folder.Owner.ToString());
961 rdata.writer.WriteAttributeString("type",String.Empty,folder.Type.ToString());
962 rdata.writer.WriteAttributeString("version",String.Empty,folder.Version.ToString());
963
964 if (rdata.folders != null)
965 {
966 foreach (InventoryFolderBase f in rdata.folders)
967 {
968 if (f.ParentID == folder.ID)
969 {
970 formatInventory(rdata, f, indent);
971 }
972 }
973 }
974
975 if (rdata.items != null)
976 {
977 foreach (InventoryItemBase i in rdata.items)
978 {
979 if (i.Folder == folder.ID)
980 {
981 formatItem(rdata, i, indent);
982 }
983 }
984 }
985
986 // End folder item
987
988 rdata.writer.WriteEndElement();
989
990 }
991
992 /// <summary>
993 /// This method generates XML that describes an instance of InventoryItemBase.
994 /// </summary>
995
996 private void formatItem(InventoryRequestData rdata, InventoryItemBase i, string indent)
997 {
998
999 Rest.Log.DebugFormat("{0} Item : {1} {2} {3}", MsgId, i.ID, indent, i.Name);
1000
1001 rdata.writer.WriteStartElement(String.Empty,"Item",String.Empty);
1002
1003 rdata.writer.WriteAttributeString("name",String.Empty,i.Name);
1004 rdata.writer.WriteAttributeString("desc",String.Empty,i.Description);
1005 rdata.writer.WriteAttributeString("uuid",String.Empty,i.ID.ToString());
1006 rdata.writer.WriteAttributeString("owner",String.Empty,i.Owner.ToString());
1007 rdata.writer.WriteAttributeString("creator",String.Empty,i.Creator.ToString());
1008 rdata.writer.WriteAttributeString("creationdate",String.Empty,i.CreationDate.ToString());
1009 rdata.writer.WriteAttributeString("type",String.Empty,i.InvType.ToString());
1010 rdata.writer.WriteAttributeString("assettype",String.Empty,i.AssetType.ToString());
1011 rdata.writer.WriteAttributeString("groupowned",String.Empty,i.GroupOwned.ToString());
1012 rdata.writer.WriteAttributeString("groupid",String.Empty,i.GroupID.ToString());
1013 rdata.writer.WriteAttributeString("saletype",String.Empty,i.SaleType.ToString());
1014 rdata.writer.WriteAttributeString("saleprice",String.Empty,i.SalePrice.ToString());
1015 rdata.writer.WriteAttributeString("flags",String.Empty,i.Flags.ToString("X"));
1016
1017 rdata.writer.WriteStartElement(String.Empty,"Permissions",String.Empty);
1018 rdata.writer.WriteAttributeString("current",String.Empty,i.CurrentPermissions.ToString("X"));
1019 rdata.writer.WriteAttributeString("next",String.Empty,i.NextPermissions.ToString("X"));
1020 rdata.writer.WriteAttributeString("everyone",String.Empty,i.EveryOnePermissions.ToString("X"));
1021 rdata.writer.WriteAttributeString("base",String.Empty,i.BasePermissions.ToString("X"));
1022 rdata.writer.WriteEndElement();
1023
1024 rdata.writer.WriteElementString("Asset",i.AssetID.ToString());
1025
1026 rdata.writer.WriteEndElement();
1027
1028 }
1029
1030 /// <summary>
1031 /// This method creates a "trashcan" folder to support folder and item
1032 /// deletions by this interface. The xisting trash folder is found and
1033 /// this folder is created within it. It is called "tmp" to indicate to
1034 /// the client that it is OK to delete this folder. The REST interface
1035 /// will recreate the folder on an as-required basis.
1036 /// If the trash can cannot be created, then by implication the request
1037 /// that required it cannot be completed, and it fails accordingly.
1038 /// </summary>
1039
1040 private InventoryFolderBase GetTrashCan(InventoryRequestData rdata)
1041 {
1042
1043 InventoryFolderBase TrashCan = null;
1044
1045 foreach (InventoryFolderBase f in rdata.folders)
1046 {
1047 if (f.Name == "Trash")
1048 {
1049 foreach (InventoryFolderBase t in rdata.folders)
1050 {
1051 if (t.Name == "tmp")
1052 {
1053 TrashCan = t;
1054 }
1055 }
1056 if (TrashCan == null)
1057 {
1058 TrashCan = new InventoryFolderBase();
1059 TrashCan.Name = "tmp";
1060 TrashCan.ID = LLUUID.Random();
1061 TrashCan.Version = 1;
1062 TrashCan.Type = (short) AssetType.TrashFolder;
1063 TrashCan.ParentID = f.ID;
1064 Rest.InventoryServices.AddFolder(TrashCan);
1065 }
1066 }
1067 }
1068
1069 if (TrashCan == null)
1070 {
1071 Rest.Log.DebugFormat("{0} No Trash Can available", MsgId);
1072 rdata.Fail(Rest.HttpStatusCodeServerError,
1073 Rest.HttpStatusDescServerError);
1074 }
1075
1076 return TrashCan;
1077
1078 }
1079
1080 /// <summary>
1081 /// Make sure that an unchanged folder is not unnecessarily
1082 /// processed.
1083 /// </summary>
1084
1085 private bool FolderHasChanged(InventoryFolderBase newf, InventoryFolderBase oldf)
1086 {
1087 return ( newf.Name != oldf.Name
1088 || newf.ParentID != oldf.ParentID
1089 || newf.Owner != oldf.Owner
1090 || newf.Type != oldf.Type
1091 || newf.Version != oldf.Version
1092 );
1093 }
1094
1095 /// <summary>
1096 /// Make sure that an unchanged item is not unnecessarily
1097 /// processed.
1098 /// </summary>
1099
1100 private bool ItemHasChanged(InventoryItemBase newf, InventoryItemBase oldf)
1101 {
1102 return ( newf.Name != oldf.Name
1103 || newf.Folder != oldf.Description
1104 || newf.Description != oldf.Description
1105 || newf.Owner != oldf.Owner
1106 || newf.Creator != oldf.Creator
1107 || newf.AssetID != oldf.AssetID
1108 || newf.GroupID != oldf.GroupID
1109 || newf.GroupOwned != oldf.GroupOwned
1110 || newf.InvType != oldf.InvType
1111 || newf.AssetType != oldf.AssetType
1112 );
1113 }
1114
1115 /// <summary>
1116 /// This method is called by PUT and POST to create an XmlInventoryCollection
1117 /// instance that reflects the content of the entity supplied on the request.
1118 /// Any elements in the completed collection whose UUID is zero, are
1119 /// considered to be located relative to the end-point identified int he
1120 /// URI. In this way, an entire sub-tree can be conveyed in a single REST
1121 /// PUT or POST request.
1122 ///
1123 /// A new instance of XmlInventoryCollection is created and, if the request
1124 /// has an entity, it is more completely initialized. thus, if no entity was
1125 /// provided the collection is valid, but empty.
1126 ///
1127 /// The entity is then scanned and each tag is processed to produce the
1128 /// appropriate inventory elements. At the end f the scan, teh XmlInventoryCollection
1129 /// will reflect the subtree described by the entity.
1130 ///
1131 /// This is a very flexible mechanism, the entity may contain arbitrary,
1132 /// discontiguous tree fragments, or may contain single element. The caller is
1133 /// responsible for integrating this collection (and ensuring that any
1134 /// missing parent IDs are resolved).
1135 /// </summary>
1136
1137 internal XmlInventoryCollection ReconstituteEntity(InventoryRequestData rdata)
1138 {
1139
1140 Rest.Log.DebugFormat("{0} Reconstituting entity", MsgId);
1141
1142 XmlInventoryCollection ic = new XmlInventoryCollection();
1143
1144 if (rdata.request.HasEntityBody)
1145 {
1146
1147 Rest.Log.DebugFormat("{0} Entity present", MsgId);
1148
1149 ic.init(rdata);
1150
1151 try
1152 {
1153 while (ic.xml.Read())
1154 {
1155 switch (ic.xml.NodeType)
1156 {
1157 case XmlNodeType.Element :
1158 Rest.Log.DebugFormat("{0} StartElement: <{1}>",
1159 MsgId, ic.xml.Name);
1160 switch (ic.xml.Name)
1161 {
1162 case "Folder" :
1163 Rest.Log.DebugFormat("{0} Processing {1} element",
1164 MsgId, ic.xml.Name);
1165 CollectFolder(ic);
1166 break;
1167 case "Item" :
1168 Rest.Log.DebugFormat("{0} Processing {1} element",
1169 MsgId, ic.xml.Name);
1170 CollectItem(ic);
1171 break;
1172 case "Asset" :
1173 Rest.Log.DebugFormat("{0} Processing {1} element",
1174 MsgId, ic.xml.Name);
1175 CollectAsset(ic);
1176 break;
1177 case "Permissions" :
1178 Rest.Log.DebugFormat("{0} Processing {1} element",
1179 MsgId, ic.xml.Name);
1180 CollectPermissions(ic);
1181 break;
1182 default :
1183 Rest.Log.DebugFormat("{0} Ignoring {1} element",
1184 MsgId, ic.xml.Name);
1185 break;
1186 }
1187 // This stinks, but the ReadElement call above not only reads
1188 // the imbedded data, but also consumes the end tag for Asset
1189 // and moves the element pointer on to the containing Item's
1190 // element-end, however, if there was a permissions element
1191 // following, it would get us to the start of that..
1192 if (ic.xml.NodeType == XmlNodeType.EndElement &&
1193 ic.xml.Name == "Item")
1194 {
1195 Validate(ic);
1196 }
1197 break;
1198 case XmlNodeType.EndElement :
1199 switch (ic.xml.Name)
1200 {
1201 case "Folder" :
1202 Rest.Log.DebugFormat("{0} Completing {1} element",
1203 MsgId, ic.xml.Name);
1204 ic.Pop();
1205 break;
1206 case "Item" :
1207 Rest.Log.DebugFormat("{0} Completing {1} element",
1208 MsgId, ic.xml.Name);
1209 Validate(ic);
1210 break;
1211 case "Asset" :
1212 Rest.Log.DebugFormat("{0} Completing {1} element",
1213 MsgId, ic.xml.Name);
1214 break;
1215 case "Permissions" :
1216 Rest.Log.DebugFormat("{0} Completing {1} element",
1217 MsgId, ic.xml.Name);
1218 break;
1219 default :
1220 Rest.Log.DebugFormat("{0} Ignoring {1} element",
1221 MsgId, ic.xml.Name);
1222 break;
1223 }
1224 break;
1225 default :
1226 Rest.Log.DebugFormat("{0} [0] Ignoring: <{1}>:<2>",
1227 MsgId, ic.xml.NodeType, ic.xml.Value);
1228 break;
1229 }
1230 }
1231 }
1232 catch (XmlException e)
1233 {
1234 Rest.Log.WarnFormat("{0} XML parsing error: {1}", MsgId, e.Message);
1235 throw e;
1236 }
1237 catch (Exception e)
1238 {
1239 Rest.Log.WarnFormat("{0} Unexpected XML parsing error: {1}", MsgId, e.Message);
1240 throw e;
1241 }
1242
1243 }
1244 else
1245 {
1246 Rest.Log.DebugFormat("{0} Entity absent", MsgId);
1247 }
1248
1249 if (Rest.DEBUG)
1250 {
1251 Rest.Log.DebugFormat("{0} Reconstituted entity", MsgId);
1252 Rest.Log.DebugFormat("{0} {1} assets", MsgId, ic.Assets.Count);
1253 Rest.Log.DebugFormat("{0} {1} folder", MsgId, ic.Folders.Count);
1254 Rest.Log.DebugFormat("{0} {1} items", MsgId, ic.Items.Count);
1255 }
1256
1257 return ic;
1258
1259 }
1260
1261 /// <summary>
1262 /// This method creates an inventory Folder from the
1263 /// information supplied in the request's entity.
1264 /// A folder instance is created and initialized to reflect
1265 /// default values. These values are then overridden
1266 /// by information supplied in the entity.
1267 /// If context was not explicitly provided, then the
1268 /// appropriate ID values are determined.
1269 /// </summary>
1270
1271 private void CollectFolder(XmlInventoryCollection ic)
1272 {
1273
1274 Rest.Log.DebugFormat("{0} Interpret folder element", MsgId);
1275
1276 InventoryFolderBase result = new InventoryFolderBase();
1277
1278 // Default values
1279
1280 result.Name = String.Empty;
1281 result.ID = LLUUID.Zero;
1282 result.Owner = ic.UserID;
1283 result.ParentID = LLUUID.Zero; // Context
1284 result.Type = (short) AssetType.Folder;
1285 result.Version = 1;
1286
1287 if (ic.xml.HasAttributes)
1288 {
1289 for (int i = 0; i < ic.xml.AttributeCount; i++)
1290 {
1291 ic.xml.MoveToAttribute(i);
1292 switch (ic.xml.Name)
1293 {
1294 case "name" :
1295 result.Name = ic.xml.Value;
1296 break;
1297 case "uuid" :
1298 result.ID = new LLUUID(ic.xml.Value);
1299 break;
1300 case "parent" :
1301 result.ParentID = new LLUUID(ic.xml.Value);
1302 break;
1303 case "owner" :
1304 result.Owner = new LLUUID(ic.xml.Value);
1305 break;
1306 case "type" :
1307 result.Type = Int16.Parse(ic.xml.Value);
1308 break;
1309 case "version" :
1310 result.Version = UInt16.Parse(ic.xml.Value);
1311 break;
1312 default :
1313 Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}",
1314 MsgId, ic.xml.Name, ic.xml.Value);
1315 ic.Fail(Rest.HttpStatusCodeBadRequest,
1316 Rest.HttpStatusDescBadRequest);
1317 break;
1318 }
1319 }
1320 }
1321
1322 ic.xml.MoveToElement();
1323
1324 // The client is relying upon the reconstitution process
1325 // to determine the parent's UUID based upon context. This
1326 // is necessary where a new folder may have been
1327 // introduced.
1328
1329 if (result.ParentID == LLUUID.Zero)
1330 {
1331 result.ParentID = ic.Parent();
1332 }
1333 else
1334 {
1335
1336 bool found = false;
1337
1338 foreach (InventoryFolderBase parent in ic.rdata.folders)
1339 {
1340 if ( parent.ID == result.ParentID )
1341 {
1342 found = true;
1343 break;
1344 }
1345 }
1346
1347 if (!found)
1348 {
1349 Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}",
1350 MsgId, ic.Item.Folder, result.ID);
1351 ic.Fail(Rest.HttpStatusCodeBadRequest,
1352 Rest.HttpStatusDescBadRequest);
1353 }
1354
1355 }
1356
1357 // This is a new folder, so no existing UUID is available
1358 // or appropriate
1359
1360 if (result.ID == LLUUID.Zero)
1361 {
1362 result.ID = LLUUID.Random();
1363 }
1364
1365 // Treat this as a new context. Any other information is
1366 // obsolete as a consequence.
1367
1368 ic.Push(result);
1369
1370 }
1371
1372 /// <summary>
1373 /// This method is called to handle the construction of an Item
1374 /// instance from the supplied request entity. It is called
1375 /// whenever an Item start tag is detected.
1376 /// An instance of an Item is created and initialized to default
1377 /// values. These values are then overridden from values supplied
1378 /// as attributes to the Item element.
1379 /// This item is then stored in the XmlInventoryCollection and
1380 /// will be verified by Validate.
1381 /// All context is reset whenever the effective folder changes
1382 /// or an item is successfully validated.
1383 /// </summary>
1384
1385 private void CollectItem(XmlInventoryCollection ic)
1386 {
1387
1388 Rest.Log.DebugFormat("{0} Interpret item element", MsgId);
1389
1390 InventoryItemBase result = new InventoryItemBase();
1391
1392 result.Name = String.Empty;
1393 result.Description = String.Empty;
1394 result.ID = LLUUID.Zero;
1395 result.Folder = LLUUID.Zero;
1396 result.Owner = ic.UserID;
1397 result.Creator = ic.UserID;
1398 result.AssetID = LLUUID.Zero;
1399 result.GroupID = LLUUID.Zero;
1400 result.GroupOwned = false;
1401 result.InvType = (int) InventoryType.Unknown;
1402 result.AssetType = (int) AssetType.Unknown;
1403
1404 if (ic.xml.HasAttributes)
1405 {
1406 for (int i = 0; i < ic.xml.AttributeCount; i++)
1407 {
1408
1409 ic.xml.MoveToAttribute(i);
1410
1411 switch (ic.xml.Name)
1412 {
1413 case "name" :
1414 result.Name = ic.xml.Value;
1415 break;
1416 case "desc" :
1417 result.Description = ic.xml.Value;
1418 break;
1419 case "uuid" :
1420 result.ID = new LLUUID(ic.xml.Value);
1421 break;
1422 case "folder" :
1423 result.Folder = new LLUUID(ic.xml.Value);
1424 break;
1425 case "owner" :
1426 result.Owner = new LLUUID(ic.xml.Value);
1427 break;
1428 case "invtype" :
1429 result.InvType = Int32.Parse(ic.xml.Value);
1430 break;
1431 case "creator" :
1432 result.Creator = new LLUUID(ic.xml.Value);
1433 break;
1434 case "assettype" :
1435 result.AssetType = Int32.Parse(ic.xml.Value);
1436 break;
1437 case "groupowned" :
1438 result.GroupOwned = Boolean.Parse(ic.xml.Value);
1439 break;
1440 case "groupid" :
1441 result.GroupID = new LLUUID(ic.xml.Value);
1442 break;
1443 case "flags" :
1444 result.Flags = UInt32.Parse(ic.xml.Value);
1445 break;
1446 case "creationdate" :
1447 result.CreationDate = Int32.Parse(ic.xml.Value);
1448 break;
1449 case "saletype" :
1450 result.SaleType = Byte.Parse(ic.xml.Value);
1451 break;
1452 case "saleprice" :
1453 result.SalePrice = Int32.Parse(ic.xml.Value);
1454 break;
1455
1456 default :
1457 Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}",
1458 MsgId, ic.xml.Name, ic.xml.Value);
1459 ic.Fail(Rest.HttpStatusCodeBadRequest,
1460 Rest.HttpStatusDescBadRequest);
1461 break;
1462 }
1463 }
1464 }
1465
1466 ic.xml.MoveToElement();
1467
1468 ic.Push(result);
1469
1470 }
1471
1472 /// <summary>
1473 /// This method assembles an asset instance from the
1474 /// information supplied in the request's entity. It is
1475 /// called as a result of detecting a start tag for a
1476 /// type of Asset.
1477 /// The information is collected locally, and an asset
1478 /// instance is created only if the basic XML parsing
1479 /// completes successfully.
1480 /// Default values for all parts of the asset are
1481 /// established before overriding them from the supplied
1482 /// XML.
1483 /// If an asset has inline=true as an attribute, then
1484 /// the element contains the data representing the
1485 /// asset. This is saved as the data component.
1486 /// inline=false means that the element's payload is
1487 /// simply the UUID of the asset referenced by the
1488 /// item being constructed.
1489 /// An asset, if created is stored in the
1490 /// XmlInventoryCollection
1491 /// </summary>
1492
1493 private void CollectAsset(XmlInventoryCollection ic)
1494 {
1495
1496 Rest.Log.DebugFormat("{0} Interpret asset element", MsgId);
1497
1498 AssetBase asset = null;
1499
1500 string name = String.Empty;
1501 string desc = String.Empty;
1502 sbyte type = (sbyte) AssetType.Unknown;
1503 sbyte itype = (sbyte) AssetType.Unknown;
1504 bool temp = false;
1505 bool local = false;
1506
1507 // This is not a persistent attribute
1508 bool inline = true;
1509
1510 LLUUID uuid = LLUUID.Zero;
1511
1512 // Attribute is optional
1513 if (ic.xml.HasAttributes)
1514 {
1515 for (int i = 0; i < ic.xml.AttributeCount; i++)
1516 {
1517 ic.xml.MoveToAttribute(i);
1518 switch (ic.xml.Name)
1519 {
1520
1521 case "name" :
1522 name = ic.xml.Value;
1523 break;
1524
1525 case "type" :
1526 type = SByte.Parse(ic.xml.Value);
1527 break;
1528
1529 case "description" :
1530 desc = ic.xml.Value;
1531 break;
1532
1533 case "temporary" :
1534 temp = Boolean.Parse(ic.xml.Value);
1535 break;
1536
1537 case "invtype" :
1538 itype = SByte.Parse(ic.xml.Value);
1539 break;
1540
1541 case "uuid" :
1542 uuid = new LLUUID(ic.xml.Value);
1543 break;
1544
1545 case "inline" :
1546 inline = Boolean.Parse(ic.xml.Value);
1547 break;
1548
1549 case "local" :
1550 local = Boolean.Parse(ic.xml.Value);
1551 break;
1552
1553 default :
1554 Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}",
1555 MsgId, ic.xml.Name, ic.xml.Value);
1556 ic.Fail(Rest.HttpStatusCodeBadRequest,
1557 Rest.HttpStatusDescBadRequest);
1558 break;
1559 }
1560 }
1561 }
1562
1563 ic.xml.MoveToElement();
1564
1565 // If this is a reference to an existing asset, just store the
1566 // asset ID into the item.
1567
1568 if (!inline)
1569 {
1570 if (ic.Item != null)
1571 {
1572 ic.Item.AssetID = new LLUUID(ic.xml.ReadElementContentAsString());
1573 Rest.Log.DebugFormat("{0} Asset ID supplied: {1}", MsgId, ic.Item.AssetID);
1574 }
1575 else
1576 {
1577 Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId);
1578 ic.Fail(Rest.HttpStatusCodeBadRequest,
1579 Rest.HttpStatusDescBadRequest);
1580 }
1581 }
1582
1583 // Otherwise, generate an asset ID, store that into the item, and
1584 // create an entry in the asset list for the inlined asset. But
1585 // only if the size is non-zero.
1586
1587 else
1588 {
1589
1590 string b64string = null;
1591
1592 // Generate a UUID of none were given, and generally none should
1593 // be. Ever.
1594
1595 if (uuid == LLUUID.Zero)
1596 {
1597 uuid = LLUUID.Random();
1598 }
1599
1600 // Create AssetBase entity to hold the inlined asset
1601
1602 asset = new AssetBase(uuid, name);
1603
1604 asset.Description = desc;
1605 asset.Type = type; // type == 0 == texture
1606 asset.InvType = itype;
1607 asset.Local = local;
1608 asset.Temporary = temp;
1609
1610 b64string = ic.xml.ReadElementContentAsString();
1611
1612 Rest.Log.DebugFormat("{0} Data length is {1}", MsgId, b64string.Length);
1613 Rest.Log.DebugFormat("{0} Data content starts with: \n\t<{1}>", MsgId,
1614 b64string.Substring(0, b64string.Length > 132 ? 132 : b64string.Length));
1615
1616 asset.Data = Convert.FromBase64String(b64string);
1617
1618 // Ensure the asset always has some kind of data component
1619
1620 if (asset.Data == null)
1621 {
1622 asset.Data = new byte[1];
1623 }
1624
1625 // If this is in the context of an item, establish
1626 // a link with the item in context.
1627
1628 if (ic.Item != null && ic.Item.AssetID == LLUUID.Zero)
1629 {
1630 ic.Item.AssetID = uuid;
1631 }
1632
1633 }
1634
1635 ic.Push(asset);
1636
1637 }
1638
1639 /// <summary>
1640 /// Store any permissions information provided by the request.
1641 /// This overrides the default permissions set when the
1642 /// XmlInventoryCollection object was created.
1643 /// </summary>
1644
1645 private void CollectPermissions(XmlInventoryCollection ic)
1646 {
1647
1648 if (ic.xml.HasAttributes)
1649 {
1650 for (int i = 0; i < ic.xml.AttributeCount; i++)
1651 {
1652 ic.xml.MoveToAttribute(i);
1653 switch (ic.xml.Name)
1654 {
1655 case "current" :
1656 ic.CurrentPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
1657 break;
1658 case "next" :
1659 ic.NextPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
1660 break;
1661 case "everyone" :
1662 ic.EveryOnePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
1663 break;
1664 case "base" :
1665 ic.BasePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber);
1666 break;
1667 default :
1668 Rest.Log.DebugFormat("{0} Permissions: invalid attribute {1}:{2}",
1669 MsgId,ic.xml.Name, ic.xml.Value);
1670 ic.Fail(Rest.HttpStatusCodeBadRequest,
1671 Rest.HttpStatusDescBadRequest);
1672 break;
1673 }
1674 }
1675 }
1676
1677 ic.xml.MoveToElement();
1678
1679 }
1680
1681 /// <summary>
1682 /// This method is called whenever an Item has been successfully
1683 /// reconstituted from the request's entity.
1684 /// It uses the information curren tin the XmlInventoryCollection
1685 /// to complete the item's specification, including any implied
1686 /// context and asset associations.
1687 /// It fails the request if any necessary item or asset information
1688 /// is missing.
1689 /// </summary>
1690
1691 private void Validate(XmlInventoryCollection ic)
1692 {
1693
1694 // There really should be an item present if we've
1695 // called validate. So fail if there is not.
1696
1697 if (ic.Item == null)
1698 {
1699 Rest.Log.ErrorFormat("{0} Unable to parse request", MsgId);
1700 ic.Fail(Rest.HttpStatusCodeBadRequest,
1701 Rest.HttpStatusDescBadRequest);
1702 }
1703
1704 // Every item is required to have a name (via REST anyway)
1705
1706 if (ic.Item.Name == String.Empty)
1707 {
1708 Rest.Log.ErrorFormat("{0} An item name MUST be specified", MsgId);
1709 ic.Fail(Rest.HttpStatusCodeBadRequest,
1710 Rest.HttpStatusDescBadRequest);
1711 }
1712
1713 // An item MUST have an asset ID. AssetID should never be zero
1714 // here. It should always get set from the information stored
1715 // when the Asset element was processed.
1716
1717 if (ic.Item.AssetID == LLUUID.Zero)
1718 {
1719
1720 Rest.Log.ErrorFormat("{0} Unable to complete request", MsgId);
1721 Rest.Log.InfoFormat("{0} Asset information is missing", MsgId);
1722 ic.Fail(Rest.HttpStatusCodeBadRequest,
1723 Rest.HttpStatusDescBadRequest);
1724
1725 }
1726
1727 // If the item is new, then assign it an ID
1728
1729 if (ic.Item.ID == LLUUID.Zero)
1730 {
1731 ic.Item.ID = LLUUID.Random();
1732 }
1733
1734 // If the context is being implied, obtain the current
1735 // folder item's ID. If it was specified explicitly, make
1736 // sure that theparent folder exists.
1737
1738 if (ic.Item.Folder == LLUUID.Zero)
1739 {
1740 ic.Item.Folder = ic.Parent();
1741 }
1742 else
1743 {
1744
1745 bool found = false;
1746
1747 foreach (InventoryFolderBase parent in ic.rdata.folders)
1748 {
1749 if ( parent.ID == ic.Item.Folder )
1750 {
1751 found = true;
1752 break;
1753 }
1754 }
1755
1756 if (!found)
1757 {
1758 Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}",
1759 MsgId, ic.Item.Folder, ic.Item.ID);
1760 ic.Fail(Rest.HttpStatusCodeBadRequest,
1761 Rest.HttpStatusDescBadRequest);
1762 }
1763
1764 }
1765
1766 // If this is an inline asset being constructed in the context
1767 // of a new Item, then use the itm's name here too.
1768
1769 if (ic.Asset != null)
1770 {
1771 if (ic.Asset.Name == String.Empty)
1772 ic.Asset.Name = ic.Item.Name;
1773 if (ic.Asset.Description == String.Empty)
1774 ic.Asset.Description = ic.Item.Description;
1775 }
1776
1777 // Assign permissions
1778
1779 ic.Item.CurrentPermissions = ic.CurrentPermissions;
1780 ic.Item.EveryOnePermissions = ic.EveryOnePermissions;
1781 ic.Item.BasePermissions = ic.BasePermissions;
1782 ic.Item.NextPermissions = ic.NextPermissions;
1783
1784 // If no type was specified for this item, we can attempt to
1785 // infer something from the file type maybe. This is NOT as
1786 // good as having type be specified in the XML.
1787
1788 if (ic.Item.AssetType == (int) AssetType.Unknown ||
1789 ic.Item.InvType == (int) AssetType.Unknown)
1790 {
1791
1792 Rest.Log.DebugFormat("{0} Attempting to infer item type", MsgId);
1793
1794 string[] parts = ic.Item.Name.Split(Rest.CA_PERIOD);
1795
1796 if (Rest.DEBUG)
1797 {
1798 for (int i = 0; i < parts.Length; i++)
1799 {
1800 Rest.Log.DebugFormat("{0} Name part {1} : {2}",
1801 MsgId, i, parts[i]);
1802 }
1803 }
1804
1805 // If the associated item name is multi-part, then maybe
1806 // the last part will indicate the item type - if we're
1807 // lucky.
1808
1809 if (parts.Length > 1)
1810 {
1811 Rest.Log.DebugFormat("{0} File type is {1}",
1812 MsgId, parts[parts.Length - 1]);
1813 switch (parts[parts.Length - 1])
1814 {
1815 case "jpeg2000" :
1816 case "jpeg-2000" :
1817 case "jpg2000" :
1818 case "jpg-2000" :
1819 Rest.Log.DebugFormat("{0} Type {1} inferred",
1820 MsgId, parts[parts.Length-1]);
1821 if (ic.Item.AssetType == (int) AssetType.Unknown)
1822 ic.Item.AssetType = (int) AssetType.ImageJPEG;
1823 if (ic.Item.InvType == (int) AssetType.Unknown)
1824 ic.Item.InvType = (int) AssetType.ImageJPEG;
1825 break;
1826 case "jpg" :
1827 case "jpeg" :
1828 Rest.Log.DebugFormat("{0} Type {1} inferred",
1829 MsgId, parts[parts.Length - 1]);
1830 if (ic.Item.AssetType == (int) AssetType.Unknown)
1831 ic.Item.AssetType = (int) AssetType.ImageJPEG;
1832 if (ic.Item.InvType == (int) AssetType.Unknown)
1833 ic.Item.InvType = (int) AssetType.ImageJPEG;
1834 break;
1835 default :
1836 Rest.Log.DebugFormat("{0} Type was not inferred", MsgId);
1837 break;
1838 }
1839 }
1840 }
1841
1842 ic.reset();
1843
1844 }
1845
1846 #region Inventory RequestData extension
1847
1848 internal class InventoryRequestData : RequestData
1849 {
1850
1851 /// <summary>
1852 /// These are the inventory specific request/response state
1853 /// extensions.
1854 /// </summary>
1855
1856 internal bool HaveInventory = false;
1857 internal ICollection<InventoryFolderImpl> folders = null;
1858 internal ICollection<InventoryItemBase> items = null;
1859 internal UserProfileData userProfile = null;
1860 internal InventoryFolderBase root = null;
1861
1862 internal InventoryRequestData(OSHttpRequest request, OSHttpResponse response, string prefix)
1863 : base(request, response, prefix)
1864 {
1865 }
1866
1867 /// <summary>
1868 /// This is the callback method required by inventory services. The
1869 /// requestor issues an inventory request and then blocks until this
1870 /// method signals the monitor.
1871 /// </summary>
1872
1873 internal void GetUserInventory(ICollection<InventoryFolderImpl> folders, ICollection<InventoryItemBase> items)
1874 {
1875 Rest.Log.DebugFormat("{0} Asynchronously updating inventory data", MsgId);
1876 this.folders = folders;
1877 this.items = items;
1878 this.HaveInventory = true;
1879 lock(this)
1880 {
1881 Monitor.Pulse(this);
1882 }
1883 }
1884
1885 }
1886
1887 #endregion Inventory RequestData extension
1888
1889 /// <summary>
1890 /// This class is used to record and manage the hierarchy
1891 /// constructed from the entity supplied in the request for
1892 /// PUT and POST.
1893 /// </summary>
1894
1895 internal class XmlInventoryCollection : InventoryCollection
1896 {
1897
1898 internal InventoryRequestData rdata;
1899 private Stack<InventoryFolderBase> stk;
1900
1901 internal List<AssetBase> Assets;
1902
1903 internal InventoryItemBase Item;
1904 internal AssetBase Asset;
1905 internal XmlReader xml;
1906
1907 internal /*static*/ const uint DefaultCurrent = 0x7FFFFFFF;
1908 internal /*static*/ const uint DefaultNext = 0x82000;
1909 internal /*static*/ const uint DefaultBase = 0x7FFFFFFF;
1910 internal /*static*/ const uint DefaultEveryOne = 0x0;
1911
1912 internal uint CurrentPermissions = 0x00;
1913 internal uint NextPermissions = 0x00;
1914 internal uint BasePermissions = 0x00;
1915 internal uint EveryOnePermissions = 0x00;
1916
1917 internal XmlInventoryCollection()
1918 {
1919 Folders = new List<InventoryFolderBase>();
1920 Items = new List<InventoryItemBase>();
1921 Assets = new List<AssetBase>();
1922 }
1923
1924 internal void init(InventoryRequestData p_rdata)
1925 {
1926 rdata = p_rdata;
1927 UserID = rdata.uuid;
1928 stk = new Stack<InventoryFolderBase>();
1929 rdata.initXmlReader();
1930 xml = rdata.reader;
1931 initPermissions();
1932 }
1933
1934 internal void initPermissions()
1935 {
1936 CurrentPermissions = DefaultCurrent;
1937 NextPermissions = DefaultNext;
1938 BasePermissions = DefaultBase;
1939 EveryOnePermissions = DefaultEveryOne;
1940 }
1941
1942 internal LLUUID Parent()
1943 {
1944 if (stk.Count != 0)
1945 {
1946 return stk.Peek().ID;
1947 }
1948 else
1949 {
1950 return LLUUID.Zero;
1951 }
1952 }
1953
1954 internal void Push(InventoryFolderBase folder)
1955 {
1956 stk.Push(folder);
1957 Folders.Add(folder);
1958 reset();
1959 }
1960
1961 internal void Push(InventoryItemBase item)
1962 {
1963 Item = item;
1964 Items.Add(item);
1965 }
1966
1967 internal void Push(AssetBase asset)
1968 {
1969 Asset = asset;
1970 Assets.Add(asset);
1971 }
1972
1973 internal void Pop()
1974 {
1975 stk.Pop();
1976 reset();
1977 }
1978
1979 internal void reset()
1980 {
1981 Item = null;
1982 Asset = null;
1983 initPermissions();
1984 }
1985
1986 internal void Fail(int code, string desc)
1987 {
1988 rdata.Fail(code, desc);
1989 }
1990
1991 }
1992 }
1993}