Implement SecurityIdentifier and improve NTAccount.
[mono.git] / mcs / class / System.Web.Routing / System.Web.Routing / PatternParser.cs
1 //
2 // PatternParser.cs
3 //
4 // Author:
5 //      Atsushi Enomoto <atsushi@ximian.com>
6 //      Marek Habersack <mhabersack@novell.com>
7 //
8 // Copyright (C) 2008-2010 Novell Inc. http://novell.com
9 //
10
11 //
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 //
31 using System;
32 using System.Collections.Generic;
33 using System.Security.Permissions;
34 using System.Text;
35 using System.Web;
36 using System.Web.Util;
37
38 namespace System.Web.Routing
39 {
40         sealed class PatternParser
41         {
42                 struct PatternSegment
43                 {
44                         public bool AllLiteral;
45                         public List <PatternToken> Tokens;
46                 }
47                 
48                 static readonly char[] placeholderDelimiters = { '{', '}' };
49                 
50                 PatternSegment[] segments;
51                 Dictionary <string, bool> parameterNames;
52                 PatternToken[] tokens;
53                 
54                 int segmentCount;
55                 bool haveSegmentWithCatchAll;
56                 
57                 public string Url {
58                         get;
59                         private set;
60                 }
61                 
62                 public PatternParser (string pattern)
63                 {
64                         this.Url = pattern;
65                         Parse ();
66                 }
67
68                 void Parse ()
69                 {
70                         string url = Url;
71                         parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
72                         
73                         if (!String.IsNullOrEmpty (url)) {
74                                 if (url [0] == '~' || url [0] == '/')
75                                         throw new ArgumentException ("Url must not start with '~' or '/'");
76                                 if (url.IndexOf ('?') >= 0)
77                                         throw new ArgumentException ("Url must not contain '?'");
78                         } else {
79                                 segments = new PatternSegment [0];
80                                 tokens = new PatternToken [0];
81                                 return;
82                         }
83                         
84                         string[] parts = url.Split ('/');
85                         int partsCount = segmentCount = parts.Length;
86                         var allTokens = new List <PatternToken> ();
87                         PatternToken tmpToken;
88                         
89                         segments = new PatternSegment [partsCount];
90                         
91                         for (int i = 0; i < partsCount; i++) {
92                                 if (haveSegmentWithCatchAll)
93                                         throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
94                                 
95                                 int catchAlls = 0;
96                                 string part = parts [i];
97                                 int partLength = part.Length;
98                                 var tokens = new List <PatternToken> ();
99
100                                 if (partLength == 0 && i < partsCount - 1)
101                                         throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
102
103                                 if (part.IndexOf ("{}") != -1)
104                                         throw new ArgumentException ("Empty URL parameter name is not allowed");
105
106                                 if (i > 0)
107                                         allTokens.Add (null);
108                                 
109                                 if (part.IndexOfAny (placeholderDelimiters) == -1) {
110                                         // no placeholders here, short-circuit it
111                                         tmpToken = new PatternToken (PatternTokenType.Literal, part);
112                                         tokens.Add (tmpToken);
113                                         allTokens.Add (tmpToken);
114                                         segments [i].AllLiteral = true;
115                                         segments [i].Tokens = tokens;
116                                         continue;
117                                 }
118
119                                 string tmp;
120                                 int from = 0, start;
121                                 bool allLiteral = true;
122                                 while (from < partLength) {
123                                         start = part.IndexOf ('{', from);
124                                         if (start >= partLength - 2)
125                                                 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
126
127                                         if (start < 0) {
128                                                 if (part.IndexOf ('}', from) >= from)
129                                                         throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
130                                                 tmp = part.Substring (from);
131                                                 tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
132                                                 tokens.Add (tmpToken);
133                                                 allTokens.Add (tmpToken);
134                                                 from += tmp.Length;
135                                                 break;
136                                         }
137
138                                         if (from == 0 && start > 0) {
139                                                 tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
140                                                 tokens.Add (tmpToken);
141                                                 allTokens.Add (tmpToken);
142                                         }
143                                         
144                                         int end = part.IndexOf ('}', start + 1);
145                                         int next = part.IndexOf ('{', start + 1);
146                                         
147                                         if (end < 0 || next >= 0 && next < end)
148                                                 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
149                                         if (next == end + 1)
150                                                 throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
151
152                                         if (next == -1)
153                                                 next = partLength;
154                                         
155                                         string token = part.Substring (start + 1, end - start - 1);
156                                         PatternTokenType type;
157                                         if (token [0] == '*') {
158                                                 catchAlls++;
159                                                 haveSegmentWithCatchAll = true;
160                                                 type = PatternTokenType.CatchAll;
161                                                 token = token.Substring (1);
162                                         } else
163                                                 type = PatternTokenType.Standard;
164
165                                         if (!parameterNames.ContainsKey (token))
166                                                 parameterNames.Add (token, true);
167
168                                         tmpToken = new PatternToken (type, token);
169                                         tokens.Add (tmpToken);
170                                         allTokens.Add (tmpToken);
171                                         allLiteral = false;
172                                         
173                                         if (end < partLength - 1) {
174                                                 token = part.Substring (end + 1, next - end - 1);
175                                                 tmpToken = new PatternToken (PatternTokenType.Literal, token);
176                                                 tokens.Add (tmpToken);
177                                                 allTokens.Add (tmpToken);
178                                                 end += token.Length;
179                                         }
180
181                                         if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
182                                                 throw new ArgumentException ("A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.");
183                                         from = end + 1;
184                                 }
185                                 
186                                 segments [i].AllLiteral = allLiteral;
187                                 segments [i].Tokens = tokens;
188                         }
189
190                         if (allTokens.Count > 0)
191                                 this.tokens = allTokens.ToArray ();
192                         allTokens = null;
193                 }
194
195                 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
196                 {
197                         if (defaults != null && defaults.Count > 0) {
198                                 string key;
199                                 foreach (var def in defaults) {
200                                         key = def.Key;
201                                         if (dict.ContainsKey (key))
202                                                 continue;
203                                         dict.Add (key, def.Value);
204                                 }
205                         }
206
207                         return dict;
208                 }
209
210                 bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
211                 {
212                         string pathSegment = argSegs [segIndex];
213                         int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
214                         int startIndex = pathSegmentLength - 1;
215                         PatternTokenType tokenType;
216                         int tokensCount = tokens.Count;
217                         PatternToken token;
218                         string tokenName;
219
220                         for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
221                                 token = tokens [tokenIndex];
222                                 if (startIndex < 0)
223                                         return false;
224
225                                 tokenType = token.Type;
226                                 tokenName = token.Name;
227
228                                 if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
229                                         var sb = new StringBuilder ();
230
231                                         for (int j = segIndex; j < argsCount; j++) {
232                                                 if (j > segIndex)
233                                                         sb.Append ('/');
234                                                 sb.Append (argSegs [j]);
235                                         }
236                                                 
237                                         ret.Add (tokenName, sb.ToString ());
238                                         break;
239                                 }
240
241                                 int scanIndex;
242                                 if (token.Type == PatternTokenType.Literal) {
243                                         int nameLen = tokenName.Length;
244                                         if (startIndex + 1 < nameLen)
245                                                 return false;
246                                         scanIndex = startIndex - nameLen + 1;
247                                         if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
248                                                 return false;
249                                         startIndex = scanIndex - 1;
250                                         continue;
251                                 }
252
253                                 // Standard token
254                                 int nextTokenIndex = tokenIndex - 1;
255                                 if (nextTokenIndex < 0) {
256                                         // First token
257                                         ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
258                                         continue;
259                                 }
260
261                                 if (startIndex == 0)
262                                         return false;
263                                 
264                                 var nextToken = tokens [nextTokenIndex];
265                                 string nextTokenName = nextToken.Name;
266
267                                 // Skip one char, since there can be no empty segments and if the
268                                 // current token's value happens to be the same as preceeding
269                                 // literal text, we'll save some time and complexity.
270                                 scanIndex = startIndex - 1;
271                                 int lastIndex = pathSegment.LastIndexOf (nextTokenName, scanIndex, StringComparison.OrdinalIgnoreCase);
272                                 if (lastIndex == -1)
273                                         return false;
274
275                                 lastIndex += nextTokenName.Length - 1;
276                                                 
277                                 string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
278                                 if (String.IsNullOrEmpty (sectionValue))
279                                         return false;
280
281                                 ret.Add (tokenName, sectionValue);
282                                 startIndex = lastIndex;
283                         }
284                         
285                         return true;
286                 }
287
288                 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
289                 {
290                         var ret = new RouteValueDictionary ();
291                         string url = Url;
292                         string [] argSegs;
293                         int argsCount;
294                         
295                         if (String.IsNullOrEmpty (path)) {
296                                 argSegs = null;
297                                 argsCount = 0;
298                         } else {
299                                 // quick check
300                                 if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
301                                         return AddDefaults (ret, defaults);
302
303                                 argSegs = path.Split ('/');
304                                 argsCount = argSegs.Length;
305
306                                 if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
307                                         argsCount--; // path ends with a trailinig '/'
308                         }
309                         bool haveDefaults = defaults != null && defaults.Count > 0;
310
311                         if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
312                                 argsCount = 0;
313                         
314                         if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
315                                 return null;
316
317                         int i = 0;
318
319                         foreach (PatternSegment segment in segments) {
320                                 if (i >= argsCount)
321                                         break;
322                                 
323                                 if (segment.AllLiteral) {
324                                         if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
325                                                 return null;
326                                         i++;
327                                         continue;
328                                 }
329
330                                 if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
331                                         return null;
332                                 i++;
333                         }
334
335                         // Check the remaining segments, if any, and see if they are required
336                         //
337                         // If a segment has more than one section (i.e. there's at least one
338                         // literal, then it cannot match defaults
339                         //
340                         // All of the remaining segments must have all defaults provided and they
341                         // must not be literals or the match will fail.
342                         if (i < segmentCount) {
343                                 if (!haveDefaults)
344                                         return null;
345                                 
346                                 for (;i < segmentCount; i++) {
347                                         var segment = segments [i];
348                                         if (segment.AllLiteral)
349                                                 return null;
350                                         
351                                         var tokens = segment.Tokens;
352                                         if (tokens.Count != 1)
353                                                 return null;
354
355                                         if (!defaults.ContainsKey (tokens [0].Name))
356                                                 return null;
357                                 }
358                         } else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
359                                 return null;
360                         
361                         return AddDefaults (ret, defaults);
362                 }
363                 
364                 public bool BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, out string value)
365                 {
366                         value = null;
367                         if (requestContext == null)
368                                 return false;
369
370                         RouteData routeData = requestContext.RouteData;
371                         RouteValueDictionary defaultValues = route != null ? route.Defaults : null;
372                         RouteValueDictionary ambientValues = routeData.Values;
373
374                         if (defaultValues != null && defaultValues.Count == 0)
375                                 defaultValues = null;
376                         if (ambientValues != null && ambientValues.Count == 0)
377                                 ambientValues = null;
378                         if (userValues != null && userValues.Count == 0)
379                                 userValues = null;
380
381                         // Check URL parameters
382                         // It is allowed to take ambient values for required parameters if:
383                         //
384                         //   - there are no default values provided
385                         //   - the default values dictionary contains at least one required
386                         //     parameter value
387                         //
388                         bool canTakeFromAmbient;
389                         if (defaultValues == null)
390                                 canTakeFromAmbient = true;
391                         else {
392                                 canTakeFromAmbient = false;
393                                 foreach (KeyValuePair <string, bool> de in parameterNames) {
394                                         if (defaultValues.ContainsKey (de.Key)) {
395                                                 canTakeFromAmbient = true;
396                                                 break;
397                                         }
398                                 }
399                         }
400                         
401                         bool allMustBeInUserValues = false;
402                         foreach (KeyValuePair <string, bool> de in parameterNames) {
403                                 string parameterName = de.Key;
404                                 // Is the parameter required?
405                                 if (defaultValues == null || !defaultValues.ContainsKey (parameterName)) {
406                                         // Yes, it is required (no value in defaults)
407                                         // Has the user provided value for it?
408                                         if (userValues == null || !userValues.ContainsKey (parameterName)) {
409                                                 if (allMustBeInUserValues)
410                                                         return false; // partial override => no match
411                                                 
412                                                 if (!canTakeFromAmbient || ambientValues == null || !ambientValues.ContainsKey (parameterName))
413                                                         return false; // no value provided => no match
414                                         } else if (canTakeFromAmbient)
415                                                 allMustBeInUserValues = true;
416                                 }
417                         }
418
419                         // Check for non-url parameters
420                         if (defaultValues != null) {
421                                 foreach (var de in defaultValues) {
422                                         string parameterName = de.Key;
423                                         
424                                         if (parameterNames.ContainsKey (parameterName))
425                                                 continue;
426
427                                         object parameterValue = null;
428                                         // Has the user specified value for this parameter and, if
429                                         // yes, is it the same as the one in defaults?
430                                         if (userValues != null && userValues.TryGetValue (parameterName, out parameterValue)) {
431                                                 object defaultValue = de.Value;
432                                                 if (defaultValue is string && parameterValue is string) {
433                                                         if (String.Compare ((string)defaultValue, (string)parameterValue, StringComparison.Ordinal) != 0)
434                                                                 return false; // different value => no match
435                                                 } else if (defaultValue != parameterValue)
436                                                         return false; // different value => no match
437                                         }
438                                 }
439                         }
440
441                         // Check the constraints
442                         RouteValueDictionary constraints = route != null ? route.Constraints : null;
443                         if (constraints != null && constraints.Count > 0) {
444                                 HttpContextBase context = requestContext.HttpContext;
445                                 bool invalidConstraint;
446                                 
447                                 foreach (var de in constraints) {
448                                         if (!Route.ProcessConstraintInternal (context, route, de.Value, de.Key, userValues, RouteDirection.UrlGeneration, out invalidConstraint) ||
449                                             invalidConstraint)
450                                                 return false; // constraint not met => no match
451                                 }
452                         }
453
454                         // We're a match, generate the URL
455                         var ret = new StringBuilder ();
456                         bool canTrim = true;
457                         
458                         // Going in reverse order, so that we can trim without much ado
459                         int tokensCount = tokens.Length - 1;
460                         for (int i = tokensCount; i >= 0; i--) {
461                                 PatternToken token = tokens [i];
462                                 if (token == null) {
463                                         if (i < tokensCount && ret.Length > 0 && ret [0] != '/')
464                                                 ret.Insert (0, '/');
465                                         continue;
466                                 }
467                                 
468                                 if (token.Type == PatternTokenType.Literal) {
469                                         ret.Insert (0, token.Name);
470                                         continue;
471                                 }
472
473                                 string parameterName = token.Name;
474                                 object tokenValue;
475
476 #if SYSTEMCORE_DEP
477                                 if (userValues.GetValue (parameterName, out tokenValue)) {
478                                         if (!defaultValues.Has (parameterName, tokenValue)) {
479                                                 canTrim = false;
480                                                 if (tokenValue != null)
481                                                         ret.Insert (0, tokenValue.ToString ());
482                                                 continue;
483                                         }
484
485                                         if (!canTrim && tokenValue != null)
486                                                 ret.Insert (0, tokenValue.ToString ());
487                                         continue;
488                                 }
489
490                                 if (defaultValues.GetValue (parameterName, out tokenValue)) {
491                                         object ambientTokenValue;
492                                         if (ambientValues.GetValue (parameterName, out ambientTokenValue))
493                                                 tokenValue = ambientTokenValue;
494                                         
495                                         if (!canTrim && tokenValue != null)
496                                                 ret.Insert (0, tokenValue.ToString ());
497                                         continue;
498                                 }
499
500                                 canTrim = false;
501                                 if (ambientValues.GetValue (parameterName, out tokenValue)) {
502                                         if (tokenValue != null)
503                                                 ret.Insert (0, tokenValue.ToString ());
504                                         continue;
505                                 }
506 #endif
507                         }
508
509                         // All the values specified in userValues that aren't part of the original
510                         // URL, the constraints or defaults collections are treated as overflow
511                         // values - they are appended as query parameters to the URL
512                         if (userValues != null) {
513                                 bool first = true;
514                                 foreach (var de in userValues) {
515                                         string parameterName = de.Key;
516
517 #if SYSTEMCORE_DEP
518                                         if (parameterNames.ContainsKey (parameterName) || defaultValues.Has (parameterName) || constraints.Has (parameterName))
519                                                 continue;
520 #endif
521
522                                         object parameterValue = de.Value;
523                                         if (parameterValue == null)
524                                                 continue;
525
526                                         var parameterValueAsString = parameterValue as string;
527                                         if (parameterValueAsString != null && parameterValueAsString.Length == 0)
528                                                 continue;
529                                         
530                                         if (first) {
531                                                 ret.Append ('?');
532                                                 first = false;
533                                         } else
534                                                 ret.Append ('&');
535
536                                         
537                                         ret.Append (Uri.EscapeDataString (parameterName));
538                                         ret.Append ('=');
539                                         if (parameterValue != null)
540                                                 ret.Append (Uri.EscapeDataString (de.Value.ToString ()));
541                                 }
542                         }
543                         
544                         value = ret.ToString ();
545                         return true;
546                 }
547         }
548 }
549