Merge pull request #3389 from lambdageek/bug-43099
[mono.git] / mcs / class / referencesource / System.Web / XmlSiteMapProvider.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="XmlSiteMapProvider.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6
7 /*
8  * XmlSiteMapProvider class definition
9  *
10  * Copyright (c) 2002 Microsoft Corporation
11  */
12
13 namespace System.Web {
14
15     using System;
16     using System.Collections;
17     using System.Collections.Specialized;
18     using System.ComponentModel;
19     using System.ComponentModel.Design;
20     using System.Configuration;
21     using System.Configuration.Provider;
22     using System.Diagnostics.CodeAnalysis;
23     using System.Globalization;
24     using System.IO;
25     using System.Resources;
26     using System.Security;
27     using System.Security.Permissions;
28     using System.Web.Compilation;
29     using System.Web.Configuration;
30     using System.Web.Hosting;
31     using System.Web.UI;
32     using System.Web.Util;
33     using System.Xml;
34
35     // XmlMapProvider that generates sitemap tree from xml files
36
37     public class XmlSiteMapProvider : StaticSiteMapProvider, IDisposable {
38
39         private string _filename;
40         private VirtualPath _virtualPath;
41         private VirtualPath _normalizedVirtualPath;
42         private SiteMapNode _siteMapNode;
43         private XmlDocument _document;
44         private bool _initialized;
45         private FileChangeEventHandler _handler;
46         private StringCollection _parentSiteMapFileCollection;
47
48         private const string _providerAttribute = "provider";
49         private const string _siteMapFileAttribute = "siteMapFile";
50         private const string _siteMapNodeName = "siteMapNode";
51         private const string _xmlSiteMapFileExtension = ".sitemap";
52         private const string _resourcePrefix = "$resources:";
53         private const int _resourcePrefixLength = 10;
54         private const char _resourceKeySeparator = ',';
55         private static readonly char[] _seperators = new char[] { ';', ',' };
56
57         private ArrayList _childProviderList;
58
59         // table containing mappings from child providers to their root nodes.
60         private Hashtable _childProviderTable;
61
62
63         public XmlSiteMapProvider() {
64         }
65
66         private ArrayList ChildProviderList {
67             get {
68                 ArrayList returnList = _childProviderList;
69                 if (returnList == null) {
70                     lock (_lock) {
71                         if (_childProviderList == null) {
72                             returnList = ArrayList.ReadOnly(new ArrayList(ChildProviderTable.Keys));
73                             _childProviderList = returnList;
74                         }
75                         else {
76                             returnList = _childProviderList;
77                         }
78                     }
79                 }
80
81                 return returnList;
82             }
83         }
84
85         private Hashtable ChildProviderTable {
86             get {
87                 if (_childProviderTable == null) {
88                     lock (_lock) {
89                         if (_childProviderTable == null) {
90                             _childProviderTable = new Hashtable();
91                         }
92                     }
93                 }
94
95                 return _childProviderTable;
96             }
97         }
98
99
100         public override SiteMapNode RootNode {
101             get {
102                 BuildSiteMap();
103                 SiteMapNode node = ReturnNodeIfAccessible(_siteMapNode);
104                 return ApplyModifierIfExists(node);
105             }
106         }
107
108         public override SiteMapNode CurrentNode {
109             get {
110                 return ApplyModifierIfExists(base.CurrentNode);
111             }
112         }
113
114         public override SiteMapNode GetParentNode(SiteMapNode node) {
115             SiteMapNode parentNode = base.GetParentNode(node);
116             return ApplyModifierIfExists(parentNode);
117         }
118
119         public override SiteMapNodeCollection GetChildNodes(SiteMapNode node) {
120             SiteMapNodeCollection subNodes = base.GetChildNodes(node);
121             HttpContext context = HttpContext.Current;
122
123             // Do nothing if the modifier doesn't apply
124             if (context == null || !context.Response.UsePathModifier || subNodes.Count == 0) {
125                 return subNodes;
126             }
127
128             // Apply the modifier to the children nodes
129             SiteMapNodeCollection resultNodes = new SiteMapNodeCollection(subNodes.Count);
130         
131             foreach (SiteMapNode n in subNodes) {
132                 resultNodes.Add(ApplyModifierIfExists(n));
133             }
134
135             return resultNodes;
136         }
137
138         protected internal override void AddNode(SiteMapNode node, SiteMapNode parentNode) {
139             if (node == null) {
140                 throw new ArgumentNullException("node");
141             }
142
143             if (parentNode == null) {
144                 throw new ArgumentNullException("parentNode");
145             }
146
147             SiteMapProvider ownerProvider = node.Provider;
148             SiteMapProvider parentOwnerProvider = parentNode.Provider;
149
150             if (ownerProvider != this) {
151                 throw new ArgumentException(SR.GetString(SR.XmlSiteMapProvider_cannot_add_node, node.ToString()), "node");
152             }
153
154             if (parentOwnerProvider != this) {
155                 throw new ArgumentException(SR.GetString(SR.XmlSiteMapProvider_cannot_add_node, parentNode.ToString()), "parentNode");
156             }
157
158             lock (_lock) {
159                 // First remove it from its current location.
160                 RemoveNode(node);
161                 AddNodeInternal(node, parentNode, null);
162             }
163         }
164
165         private void AddNodeInternal(SiteMapNode node, SiteMapNode parentNode, XmlNode xmlNode) {
166             lock (_lock) {
167                 String url = node.Url;
168                 String key = node.Key;
169
170                 bool isValidUrl = false;
171
172                 // Only add the node to the url table if it's a static node.
173                 if (!String.IsNullOrEmpty(url)) {
174                     if (UrlTable[url] != null) {
175                         if (xmlNode != null) {
176                             throw new ConfigurationErrorsException(
177                                 SR.GetString(SR.XmlSiteMapProvider_Multiple_Nodes_With_Identical_Url, url),
178                                 xmlNode);
179                         }
180                         else {
181                             throw new InvalidOperationException(SR.GetString(
182                                 SR.XmlSiteMapProvider_Multiple_Nodes_With_Identical_Url, url));
183                         }
184                     }
185
186                     isValidUrl = true;
187                 }
188
189                 if (KeyTable.Contains(key)) {
190                     if (xmlNode != null) {
191                         throw new ConfigurationErrorsException(
192                             SR.GetString(SR.XmlSiteMapProvider_Multiple_Nodes_With_Identical_Key, key),
193                             xmlNode);
194                     }
195                     else {
196                         throw new InvalidOperationException(
197                            SR.GetString(SR.XmlSiteMapProvider_Multiple_Nodes_With_Identical_Key, key));
198                     }
199                 }
200
201                 if (isValidUrl) {
202                     UrlTable[url] = node;
203                 }
204
205                 KeyTable[key] = node;
206
207                 // Add the new node into parentNode collection
208                 if (parentNode != null) {
209                     ParentNodeTable[node] = parentNode;
210
211                     if (ChildNodeCollectionTable[parentNode] == null) {
212                         ChildNodeCollectionTable[parentNode] = new SiteMapNodeCollection();
213                     }
214
215                     ((SiteMapNodeCollection)ChildNodeCollectionTable[parentNode]).Add(node);
216                 }
217             }
218         }
219
220         protected virtual void AddProvider(string providerName, SiteMapNode parentNode) {
221             if (parentNode == null) {
222                 throw new ArgumentNullException("parentNode");
223             }
224
225             if (parentNode.Provider != this) {
226                 throw new ArgumentException(SR.GetString(SR.XmlSiteMapProvider_cannot_add_node, parentNode.ToString()), "parentNode");
227             }
228
229             SiteMapNode node = GetNodeFromProvider(providerName);
230             AddNodeInternal(node, parentNode, null);
231         }
232
233
234         [SuppressMessage("Microsoft.Security", "MSEC1205:DoNotAllowDtdOnXmlTextReader", Justification = "Legacy code that trusts our developer-controlled input.")]
235         [SuppressMessage("Microsoft.Security.Xml", "CA3054:DoNotAllowDtdOnXmlTextReader", Justification = "Legacy code that trusts our developer-controlled input.")]
236         public override SiteMapNode BuildSiteMap() {
237
238             SiteMapNode tempNode = _siteMapNode;
239
240             // If siteMap is already constructed, simply returns it.
241             // Child providers will only be updated when the parent providers need to access them.
242             if (tempNode != null) {
243                 return tempNode;
244             }
245
246             XmlDocument document = GetConfigDocument();
247
248             lock (_lock) {
249                 if (_siteMapNode != null) {
250                     return _siteMapNode;
251                 }
252
253                 Clear();
254
255                 // Need to check if the sitemap file exists before opening it.
256                 CheckSiteMapFileExists();
257
258                 try {
259                     using (Stream stream = _normalizedVirtualPath.OpenFile()) {
260                         XmlReader reader = new XmlTextReader(stream);
261                         document.Load(reader);
262                     }
263                 }
264                 catch (XmlException e) {
265                     string sourceFile = _virtualPath.VirtualPathString;
266                     string physicalDir = _normalizedVirtualPath.MapPathInternal();
267                     if (physicalDir != null && HttpRuntime.HasPathDiscoveryPermission(physicalDir)) {
268                         sourceFile = physicalDir;
269                     }
270
271                     throw new ConfigurationErrorsException(
272                                             SR.GetString(SR.XmlSiteMapProvider_Error_loading_Config_file, _virtualPath, e.Message),
273                                             e, sourceFile, e.LineNumber);
274                 }
275                 catch (Exception e) {
276                     throw new ConfigurationErrorsException(
277                         SR.GetString(SR.XmlSiteMapProvider_Error_loading_Config_file, _virtualPath, e.Message), e);
278                 }
279
280                 XmlNode node = null;
281                 foreach (XmlNode siteMapMode in document.ChildNodes) {
282                     if (String.Equals(siteMapMode.Name, "siteMap", StringComparison.Ordinal)) {
283                         node = siteMapMode;
284                         break;
285                     }
286                 }
287
288                 if (node == null)
289                     throw new ConfigurationErrorsException(
290                         SR.GetString(SR.XmlSiteMapProvider_Top_Element_Must_Be_SiteMap),
291                         document);
292
293                 bool enableLocalization = false;
294                 HandlerBase.GetAndRemoveBooleanAttribute(node, "enableLocalization", ref enableLocalization);
295                 EnableLocalization = enableLocalization;
296
297                 XmlNode topElement = null;
298                 foreach (XmlNode subNode in node.ChildNodes) {
299                     if (subNode.NodeType == XmlNodeType.Element) {
300                         if (!_siteMapNodeName.Equals(subNode.Name)) {
301                             throw new ConfigurationErrorsException(
302                                 SR.GetString(SR.XmlSiteMapProvider_Only_SiteMapNode_Allowed),
303                                 subNode);
304                         }
305
306                         if (topElement != null) {
307                             throw new ConfigurationErrorsException(
308                                 SR.GetString(SR.XmlSiteMapProvider_Only_One_SiteMapNode_Required_At_Top),
309                                 subNode);
310                         }
311
312                         topElement = subNode;
313                     }
314                 }
315
316                 if (topElement == null) {
317                     throw new ConfigurationErrorsException(
318                          SR.GetString(SR.XmlSiteMapProvider_Only_One_SiteMapNode_Required_At_Top),
319                          node);
320                 }
321
322                 Queue queue = new Queue(50);
323
324                 // The parentnode of the top node does not exist,
325                 // simply add a null to satisfy the ConvertFromXmlNode condition.
326                 queue.Enqueue(null);
327                 queue.Enqueue(topElement);
328                 _siteMapNode = ConvertFromXmlNode(queue);
329
330                 return _siteMapNode;
331             }
332         }
333
334         private void CheckSiteMapFileExists() {
335             if (!System.Web.UI.Util.VirtualFileExistsWithAssert(_normalizedVirtualPath)) {
336                 throw new InvalidOperationException(
337                     SR.GetString(SR.XmlSiteMapProvider_FileName_does_not_exist, _virtualPath));
338             }
339         }
340
341
342         protected override void Clear() {
343             lock (_lock) {
344                 ChildProviderTable.Clear();
345                 _siteMapNode = null;
346                 _childProviderList = null;
347
348                 base.Clear();
349             }
350         }
351
352         // helper method to convert an XmlNode to a SiteMapNode
353         private SiteMapNode ConvertFromXmlNode(Queue queue) {
354
355             SiteMapNode rootNode = null;
356             while (true) {
357                 if (queue.Count == 0) {
358                     return rootNode;
359                 }
360
361                 SiteMapNode parentNode = (SiteMapNode)queue.Dequeue();
362                 XmlNode xmlNode = (XmlNode)queue.Dequeue();
363
364                 SiteMapNode node = null;
365
366                 if (!_siteMapNodeName.Equals(xmlNode.Name)) {
367                     throw new ConfigurationErrorsException(
368                         SR.GetString(SR.XmlSiteMapProvider_Only_SiteMapNode_Allowed),
369                         xmlNode);
370                 }
371
372                 string providerName = null;
373                 HandlerBase.GetAndRemoveNonEmptyStringAttribute(xmlNode, _providerAttribute, ref providerName);
374
375                 // If the siteMapNode references another provider
376                 if (providerName != null) {
377                     node = GetNodeFromProvider(providerName);
378
379                     // No other attributes or child nodes are allowed on a provider node.
380                     HandlerBase.CheckForUnrecognizedAttributes(xmlNode);
381                     HandlerBase.CheckForNonCommentChildNodes(xmlNode);
382                 }
383                 else {
384                     string siteMapFile = null;
385                     HandlerBase.GetAndRemoveNonEmptyStringAttribute(xmlNode, _siteMapFileAttribute, ref siteMapFile);
386
387                     if (siteMapFile != null) {
388                         node = GetNodeFromSiteMapFile(xmlNode, VirtualPath.Create(siteMapFile));
389                     }
390                     else {
391                         node = GetNodeFromXmlNode(xmlNode, queue);
392                     }
393                 }
394
395                 AddNodeInternal(node, parentNode, xmlNode);
396
397                 if (rootNode == null) {
398                     rootNode = node;
399                 }
400             }
401         }
402
403         protected virtual void Dispose(bool disposing) {
404             if (_handler != null) {
405                 Debug.Assert(_filename != null);
406                 HttpRuntime.FileChangesMonitor.StopMonitoringFile(_filename, _handler);
407             }
408         }
409
410         public void Dispose() {
411             Dispose(true);
412             GC.SuppressFinalize(this);
413         }
414
415         private void EnsureChildSiteMapProviderUpToDate(SiteMapProvider childProvider) {
416             SiteMapNode oldNode = (SiteMapNode)ChildProviderTable[childProvider];
417
418             SiteMapNode newNode = childProvider.GetRootNodeCore();
419             if (newNode == null) {
420                 throw new ProviderException(SR.GetString(SR.XmlSiteMapProvider_invalid_sitemapnode_returned, childProvider.Name));
421             }
422
423             // child providers have been updated.
424             if (!oldNode.Equals(newNode)) {
425
426                 // If the child provider table has been updated, simply return null.
427                 // This will happen when the current provider's sitemap file is changed or Clear() is called;
428                 if (oldNode == null) {
429                     return;
430                 }
431
432                 lock (_lock) {
433                     oldNode = (SiteMapNode)ChildProviderTable[childProvider];
434                     // If the child provider table has been updated, simply return null. See above.
435                     if (oldNode == null) {
436                         return;
437                     }
438
439                     newNode = childProvider.GetRootNodeCore();
440                     if (newNode == null) {
441                         throw new ProviderException(SR.GetString(SR.XmlSiteMapProvider_invalid_sitemapnode_returned, childProvider.Name));
442                     }
443
444                     if (!oldNode.Equals(newNode)) {
445
446                         // If the current provider does not contain any nodes but one child provider
447                         // ie. _siteMapNode == oldNode
448                         // the oldNode needs to be removed from Url table and the new node will be added.
449                         if (_siteMapNode.Equals(oldNode)) {
450                             UrlTable.Remove(oldNode.Url);
451                             KeyTable.Remove(oldNode.Key);
452
453                             UrlTable.Add(newNode.Url, newNode);
454                             KeyTable.Add(newNode.Key, newNode);
455
456                             _siteMapNode = newNode;
457                         }
458
459                         // First find the parent node
460                         SiteMapNode parent = (SiteMapNode)ParentNodeTable[oldNode];
461
462                         // parent is null when the provider does not contain any static nodes, ie.
463                         // it only contains definition to include one child provider.
464                         if (parent != null) {
465                             // Update the child nodes table
466                             SiteMapNodeCollection list = (SiteMapNodeCollection)ChildNodeCollectionTable[parent];
467
468                             // Add the newNode to where the oldNode is within parent node's collection.
469                             int index = list.IndexOf(oldNode);
470                             if (index != -1) {
471                                 list.Remove(oldNode);
472                                 list.Insert(index, newNode);
473                             }
474                             else {
475                                 list.Add(newNode);
476                             }
477
478                             // Update the parent table
479                             ParentNodeTable[newNode] = parent;
480                             ParentNodeTable.Remove(oldNode);
481
482                             // Update the Url table
483                             UrlTable.Remove(oldNode.Url);
484                             KeyTable.Remove(oldNode.Key);
485
486                             UrlTable.Add(newNode.Url, newNode);
487                             KeyTable.Add(newNode.Key, newNode);
488                         }
489                         else {
490                             // Notify the parent provider to update its child provider collection.
491                             XmlSiteMapProvider provider = ParentProvider as XmlSiteMapProvider;
492                             if (provider != null) {
493                                 provider.EnsureChildSiteMapProviderUpToDate(this);
494                             }
495                         }
496
497                         // Update provider nodes;
498                         ChildProviderTable[childProvider] = newNode;
499                         _childProviderList = null;
500                     }
501                 }
502             }
503         }
504
505         // Returns sitemap node; Search recursively in child providers if not found.
506
507         public override SiteMapNode FindSiteMapNode(string rawUrl) {
508             SiteMapNode node = base.FindSiteMapNode(rawUrl);
509
510             if (node == null) {
511                 foreach(SiteMapProvider provider in ChildProviderList) {
512                     // First make sure the child provider is up-to-date.
513                     EnsureChildSiteMapProviderUpToDate(provider);
514
515                     node = provider.FindSiteMapNode(rawUrl);
516                     if (node != null) {
517                         return node;
518                     }
519                 }
520             }
521
522             return node;
523         }
524
525         // Returns sitemap node; Search recursively in child providers if not found.
526         public override SiteMapNode FindSiteMapNodeFromKey(string key) {
527             SiteMapNode node = base.FindSiteMapNodeFromKey(key);
528
529             if (node == null) {
530                 foreach (SiteMapProvider provider in ChildProviderList) {
531                     // First make sure the child provider is up-to-date.
532                     EnsureChildSiteMapProviderUpToDate(provider);
533
534                     node = provider.FindSiteMapNodeFromKey(key);
535                     if (node != null) {
536                         return node;
537                     }
538                 }
539             }
540
541             return node;
542         }
543
544         private XmlDocument GetConfigDocument() {
545             if (_document != null)
546                 return _document;
547
548             if (!_initialized) {
549                 throw new InvalidOperationException(
550                     SR.GetString(SR.XmlSiteMapProvider_Not_Initialized));
551             }
552
553             // Do the error checking here
554             if (_virtualPath == null) {
555                 throw new ArgumentException(
556                     SR.GetString(SR.XmlSiteMapProvider_missing_siteMapFile, _siteMapFileAttribute));
557             }
558
559             if (!_virtualPath.Extension.Equals(_xmlSiteMapFileExtension, StringComparison.OrdinalIgnoreCase)) {
560                 throw new InvalidOperationException(
561                     SR.GetString(SR.XmlSiteMapProvider_Invalid_Extension, _virtualPath));
562             }
563
564             _normalizedVirtualPath = _virtualPath.CombineWithAppRoot();
565             _normalizedVirtualPath.FailIfNotWithinAppRoot();
566
567             // Make sure the file exists
568             CheckSiteMapFileExists();
569
570             _parentSiteMapFileCollection = new StringCollection();
571             XmlSiteMapProvider xmlParentProvider = ParentProvider as XmlSiteMapProvider;
572             if (xmlParentProvider != null && xmlParentProvider._parentSiteMapFileCollection != null) {
573                 if (xmlParentProvider._parentSiteMapFileCollection.Contains(_normalizedVirtualPath.VirtualPathString)) {
574                     throw new InvalidOperationException(
575                         SR.GetString(SR.XmlSiteMapProvider_FileName_already_in_use, _virtualPath));
576                 }
577
578                 // Copy the sitemapfiles in used from parent provider to current provider.
579                 foreach (string filename in xmlParentProvider._parentSiteMapFileCollection) {
580                     _parentSiteMapFileCollection.Add(filename);
581                 }
582             }
583
584             // Add current sitemap file to the collection
585             _parentSiteMapFileCollection.Add(_normalizedVirtualPath.VirtualPathString);
586
587             _filename = HostingEnvironment.MapPathInternal(_normalizedVirtualPath);
588
589             if (!String.IsNullOrEmpty(_filename)) {
590                 _handler = new FileChangeEventHandler(this.OnConfigFileChange);
591                 HttpRuntime.FileChangesMonitor.StartMonitoringFile(_filename, _handler);
592                 ResourceKey = (new FileInfo(_filename)).Name;
593             }
594
595             _document = new ConfigXmlDocument();
596
597             return _document;
598         }
599
600         private SiteMapNode GetNodeFromProvider(string providerName) {
601             SiteMapProvider provider = GetProviderFromName(providerName);
602             SiteMapNode node = null;
603
604             // Check infinite recursive sitemap files
605             if (provider is XmlSiteMapProvider) {
606                 XmlSiteMapProvider xmlProvider = (XmlSiteMapProvider)provider;
607
608                 StringCollection parentSiteMapFileCollection = new StringCollection();
609                 if (_parentSiteMapFileCollection != null) {
610                     foreach (string filename in _parentSiteMapFileCollection) {
611                         parentSiteMapFileCollection.Add(filename);
612                     }
613                 }
614
615                 // Make sure the provider is initialized before adding to the collection.
616                 xmlProvider.BuildSiteMap();
617
618                 parentSiteMapFileCollection.Add(_normalizedVirtualPath.VirtualPathString);
619                 if (parentSiteMapFileCollection.Contains(VirtualPath.GetVirtualPathString(xmlProvider._normalizedVirtualPath))) {
620                     throw new InvalidOperationException(SR.GetString(SR.XmlSiteMapProvider_FileName_already_in_use, xmlProvider._virtualPath));
621                 }
622
623                 xmlProvider._parentSiteMapFileCollection = parentSiteMapFileCollection;
624             }
625
626             node = provider.GetRootNodeCore();
627             if (node == null) {
628                 throw new InvalidOperationException(
629                     SR.GetString(SR.XmlSiteMapProvider_invalid_GetRootNodeCore, ((ProviderBase)provider).Name));
630             }
631
632             ChildProviderTable.Add(provider, node);
633             _childProviderList = null;
634
635             provider.ParentProvider = this;
636
637             return node;
638         }
639
640         private SiteMapNode GetNodeFromSiteMapFile(XmlNode xmlNode, VirtualPath siteMapFile) {
641
642             SiteMapNode node = null;
643
644             // For external sitemap files, its secuity setting is inherited from parent provider
645             bool secuityTrimmingEnabled = SecurityTrimmingEnabled;
646             HandlerBase.GetAndRemoveBooleanAttribute(xmlNode, _securityTrimmingEnabledAttrName, ref secuityTrimmingEnabled);
647
648             // No other attributes or non-comment nodes are allowed on a siteMapFile node
649             HandlerBase.CheckForUnrecognizedAttributes(xmlNode);
650             HandlerBase.CheckForNonCommentChildNodes(xmlNode);
651
652             XmlSiteMapProvider childProvider = new XmlSiteMapProvider();
653
654             // siteMapFile was relative to the sitemap file where this xmlnode is defined, make it an application path.
655             siteMapFile = _normalizedVirtualPath.Parent.Combine(siteMapFile);
656
657             childProvider.ParentProvider = this;
658             childProvider.Initialize(siteMapFile, secuityTrimmingEnabled);
659             childProvider.BuildSiteMap();
660
661             node = childProvider._siteMapNode;
662
663             ChildProviderTable.Add(childProvider, node);
664             _childProviderList = null;
665
666             return node;
667         }
668
669         private void HandleResourceAttribute(XmlNode xmlNode, ref NameValueCollection collection, 
670             string attrName, ref string text, bool allowImplicitResource) {
671             if (String.IsNullOrEmpty(text)) {
672                 return;
673             }
674
675             string resourceKey = null;
676             string temp = text.TrimStart(new char[] { ' ' });
677
678             if (temp != null && temp.Length > _resourcePrefixLength) {
679                 if (temp.ToLower(CultureInfo.InvariantCulture).StartsWith(_resourcePrefix, StringComparison.Ordinal)) {
680                     if (!allowImplicitResource) {
681                         throw new ConfigurationErrorsException(
682                             SR.GetString(SR.XmlSiteMapProvider_multiple_resource_definition, attrName), xmlNode);
683                     }
684
685                     resourceKey = temp.Substring(_resourcePrefixLength + 1);
686
687                     if (resourceKey.Length == 0) {
688                         throw new ConfigurationErrorsException(
689                             SR.GetString(SR.XmlSiteMapProvider_resourceKey_cannot_be_empty), xmlNode);
690                     }
691
692                     // Retrieve className from attribute
693                     string className = null;
694                     string key = null;
695                     int index = resourceKey.IndexOf(_resourceKeySeparator);
696                     if (index == -1) {
697                         throw new ConfigurationErrorsException(
698                             SR.GetString(SR.XmlSiteMapProvider_invalid_resource_key, resourceKey), xmlNode);
699                     }
700
701                     className = resourceKey.Substring(0, index);
702                     key = resourceKey.Substring(index + 1);
703
704                     // Retrieve resource key and default value from attribute
705                     int defaultIndex = key.IndexOf(_resourceKeySeparator);
706                     if (defaultIndex != -1) {
707                         text = key.Substring(defaultIndex + 1);
708                         key = key.Substring(0, defaultIndex);
709                     }
710                     else {
711                         text = null;
712                     }
713
714                     if (collection == null) {
715                         collection = new NameValueCollection();
716                     }
717
718                     collection.Add(attrName, className.Trim());
719                     collection.Add(attrName, key.Trim());
720                 }
721             }
722         }
723
724         private SiteMapNode GetNodeFromXmlNode(XmlNode xmlNode, Queue queue) {
725             SiteMapNode node = null;
726             // static nodes
727             string title = null, url = null, description = null, roles = null, resourceKey = null;
728
729             // Url attribute is NOT required for a xml node.
730             HandlerBase.GetAndRemoveStringAttribute(xmlNode, "url", ref url);
731             HandlerBase.GetAndRemoveStringAttribute(xmlNode, "title", ref title);
732             HandlerBase.GetAndRemoveStringAttribute(xmlNode, "description", ref description);
733             HandlerBase.GetAndRemoveStringAttribute(xmlNode, "roles", ref roles);
734             HandlerBase.GetAndRemoveStringAttribute(xmlNode, "resourceKey", ref resourceKey);
735
736             // Do not add the resourceKey if the resource is not valid.
737             if (!String.IsNullOrEmpty(resourceKey) && 
738                 !ValidateResource(ResourceKey, resourceKey + ".title")) {
739                 resourceKey = null;
740             }
741
742             HandlerBase.CheckForbiddenAttribute(xmlNode, _securityTrimmingEnabledAttrName);
743
744             NameValueCollection resourceKeyCollection = null;
745             bool allowImplicitResourceAttribute = String.IsNullOrEmpty(resourceKey);
746             HandleResourceAttribute(xmlNode, ref resourceKeyCollection, 
747                 "title", ref title, allowImplicitResourceAttribute);
748             HandleResourceAttribute(xmlNode, ref resourceKeyCollection, 
749                 "description", ref description, allowImplicitResourceAttribute);
750
751             ArrayList roleList = new ArrayList();
752             if (roles != null) {
753                 int foundIndex = roles.IndexOf('?');
754                 if (foundIndex != -1) {
755                     throw new ConfigurationErrorsException(
756                         SR.GetString(SR.Auth_rule_names_cant_contain_char,
757                         roles[foundIndex].ToString(CultureInfo.InvariantCulture)), xmlNode);
758                 }
759
760                 foreach (string role in roles.Split(_seperators)) {
761                     string trimmedRole = role.Trim();
762                     if (trimmedRole.Length > 0) {
763                         roleList.Add(trimmedRole);
764                     }
765                 }
766             }
767             roleList = ArrayList.ReadOnly(roleList);
768
769             String key = null;
770
771             // Make urls absolute.
772             if (!String.IsNullOrEmpty(url)) {
773                 // URL needs to be trimmed. VSWhidbey 411041
774                 url = url.Trim();
775
776                 if (!UrlPath.IsAbsolutePhysicalPath(url)) {
777                     if (UrlPath.IsRelativeUrl(url)) {
778                         url = UrlPath.Combine(HttpRuntime.AppDomainAppVirtualPathString, url);
779                     }
780                 }
781
782                 // VSWhidbey 418056, Reject any suspicious or mal-formed Urls.
783                 string decodedUrl = HttpUtility.UrlDecode(url);
784                 if (!String.Equals(url, decodedUrl, StringComparison.Ordinal)) {
785                     throw new ConfigurationErrorsException(
786                         SR.GetString(SR.Property_Had_Malformed_Url, "url", url), xmlNode);
787                 }
788
789                 key = url.ToLowerInvariant();
790             }
791             else {
792                 key = Guid.NewGuid().ToString();
793             }
794
795             // attribute collection does not contain pre-defined properties like title, url, etc.
796             ReadOnlyNameValueCollection attributeCollection = new ReadOnlyNameValueCollection();
797             attributeCollection.SetReadOnly(false);
798             foreach (XmlAttribute attribute in xmlNode.Attributes) {
799                 string value = attribute.Value;
800                 HandleResourceAttribute(xmlNode, ref resourceKeyCollection, attribute.Name, ref value, allowImplicitResourceAttribute);
801                 attributeCollection[attribute.Name] = value;
802             }
803             attributeCollection.SetReadOnly(true);
804
805             node = new SiteMapNode(this, key, url, title, description, roleList, attributeCollection, resourceKeyCollection, resourceKey);
806             node.ReadOnly = true;
807
808             foreach (XmlNode subNode in xmlNode.ChildNodes) {
809                 if (subNode.NodeType != XmlNodeType.Element)
810                     continue;
811
812                 queue.Enqueue(node);
813                 queue.Enqueue(subNode);
814             }
815
816             return node;
817         }
818
819         private SiteMapProvider GetProviderFromName(string providerName) {
820             Debug.Assert(providerName != null);
821
822             SiteMapProvider provider = SiteMap.Providers[providerName];
823             if (provider == null) {
824                 throw new ProviderException(SR.GetString(SR.Provider_Not_Found, providerName));
825             }
826
827             return provider;
828         }
829
830         protected internal override SiteMapNode GetRootNodeCore() {
831             BuildSiteMap();
832             return _siteMapNode;
833         }
834
835
836         public override void Initialize(string name, NameValueCollection attributes) {
837             if (_initialized) {
838                 throw new InvalidOperationException(
839                     SR.GetString(SR.XmlSiteMapProvider_Cannot_Be_Inited_Twice));
840             }
841
842             if (attributes != null) {
843                 if (string.IsNullOrEmpty(attributes["description"])) {
844                     attributes.Remove("description");
845                     attributes.Add("description", SR.GetString(SR.XmlSiteMapProvider_Description));
846                 }
847
848                 string siteMapFile = null;
849                 ProviderUtil.GetAndRemoveStringAttribute(attributes, _siteMapFileAttribute, name, ref siteMapFile);
850                 _virtualPath = VirtualPath.CreateAllowNull(siteMapFile);
851             }
852
853             base.Initialize(name, attributes);
854
855             if (attributes != null) {
856                 ProviderUtil.CheckUnrecognizedAttributes(attributes, name);
857             }
858
859             _initialized = true;
860         }
861
862         private void Initialize(VirtualPath virtualPath, bool secuityTrimmingEnabled) {
863             NameValueCollection coll = new NameValueCollection();
864             coll.Add(_siteMapFileAttribute, virtualPath.VirtualPathString);
865             coll.Add(_securityTrimmingEnabledAttrName, System.Web.UI.Util.GetStringFromBool(secuityTrimmingEnabled));
866
867             // Use the siteMapFile virtual path as the provider name
868             Initialize(virtualPath.VirtualPathString, coll);
869         }
870
871         private void OnConfigFileChange(Object sender, FileChangeEvent e) {
872             // Notifiy the parent for the change.
873             XmlSiteMapProvider parentProvider = ParentProvider as XmlSiteMapProvider;
874             if (parentProvider != null) {
875                 parentProvider.OnConfigFileChange(sender, e);
876             }
877
878             Clear();
879         }
880
881         protected internal override void RemoveNode(SiteMapNode node) {
882             if (node == null) {
883                 throw new ArgumentNullException("node");
884             }
885
886             SiteMapProvider ownerProvider = node.Provider;
887
888             if (ownerProvider != this) {
889
890                 // Only nodes defined in this provider tree can be removed.
891                 SiteMapProvider parentProvider = ownerProvider.ParentProvider;
892                 while (parentProvider != this) {
893                     if (parentProvider == null) {
894                         // Cannot remove nodes defined in other providers
895                         throw new InvalidOperationException(
896                             SR.GetString(SR.XmlSiteMapProvider_cannot_remove_node, node.ToString(), 
897                             this.Name, ownerProvider.Name));
898                     }
899
900                     parentProvider = parentProvider.ParentProvider;
901                 }
902             }
903
904             if (node.Equals(ownerProvider.GetRootNodeCore())) {
905                 throw new InvalidOperationException(SR.GetString(SR.SiteMapProvider_cannot_remove_root_node));
906             }
907
908             if (ownerProvider != this) {
909                 // Remove node from the owner provider.
910                 ownerProvider.RemoveNode(node);
911             }
912
913             base.RemoveNode(node);
914         }
915
916         protected virtual void RemoveProvider(string providerName) {
917             if (providerName == null) {
918                 throw new ArgumentNullException("providerName");
919             }
920
921             lock (_lock) {
922                 SiteMapProvider provider = GetProviderFromName(providerName);
923                 SiteMapNode rootNode = (SiteMapNode)ChildProviderTable[provider];
924
925                 if (rootNode == null) {
926                     throw new InvalidOperationException(SR.GetString(SR.XmlSiteMapProvider_cannot_find_provider, provider.Name, this.Name));
927                 }
928
929                 provider.ParentProvider = null;
930                 ChildProviderTable.Remove(provider);
931                 _childProviderList = null;
932
933                 base.RemoveNode(rootNode);
934             }
935         }
936
937         // VSWhidbey: 493981 Helper method to check if the valid resource type exists. 
938         // Note that this only returns false if the classKey cannot be found, regardless of resourceKey.
939         private bool ValidateResource(string classKey, string resourceKey) {
940             try {
941                 HttpContext.GetGlobalResourceObject(classKey, resourceKey);
942             }
943             catch (MissingManifestResourceException) {
944                 return false;
945             }
946
947             return true;
948         }
949
950         // Dev10# 923217 - SiteMapProvider URL Table Invalid Using Cookieless
951         // Don't keep the modifier inside the links table. Apply the modifier as approriate on demand
952         private static SiteMapNode ApplyModifierIfExists(SiteMapNode node) {
953             HttpContext context = HttpContext.Current;
954
955             // Do nothing if the modifier doesn't apply
956             if (node == null || context == null || !context.Response.UsePathModifier) {
957                 return node;
958             }
959
960             // Set Url with the modifier applied
961             SiteMapNode resultNode = node.Clone();
962             resultNode.Url = context.Response.ApplyAppPathModifier(node.Url);
963  
964             return resultNode;
965         }
966
967         private class ReadOnlyNameValueCollection : NameValueCollection {
968
969             public ReadOnlyNameValueCollection() {
970                 IsReadOnly = true;
971             }
972
973             internal void SetReadOnly(bool isReadonly) {
974                 IsReadOnly = isReadonly;
975             }
976         }
977     }
978 }