2010-04-21 Marek Habersack <mhabersack@novell.com>
[mono.git] / mcs / class / System.Web / System.Web / VirtualPathUtility.cs
1 //
2 // System.Web.VirtualPathUtility.cs
3 //
4 // Author:
5 //      Chris Toshok (toshok@ximian.com)
6 //      Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 //
8
9 //
10 // Copyright (C) 2005 Novell, Inc (http://www.novell.com)
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
32 using System.Collections.Specialized;
33 using System.Web.Configuration;
34 using System.Web.Util;
35 using System.Text;
36 using Microsoft.Win32;
37
38 namespace System.Web {
39
40         public static class VirtualPathUtility
41         {
42                 static bool monoSettingsVerifyCompatibility;
43                 static bool runningOnWindows;
44                 
45                 static VirtualPathUtility ()
46                 {
47                         try {
48                                 runningOnWindows = RuntimeHelpers.RunningOnWindows;
49                                 var monoSettings = WebConfigurationManager.GetWebApplicationSection ("system.web/monoSettings") as MonoSettingsSection;
50                                 if (monoSettings != null)
51                                         monoSettingsVerifyCompatibility = monoSettings.VerificationCompatibility != 1;
52                         } catch {
53                                 // ignore
54                         }
55                 }
56                 
57                 public static string AppendTrailingSlash (string virtualPath)
58                 {
59                         if (virtualPath == null)
60                                 return virtualPath;
61
62                         int length = virtualPath.Length;
63                         if (length == 0 || virtualPath [length - 1] == '/')
64                                 return virtualPath;
65
66                         return virtualPath + "/";
67                 }
68
69                 public static string Combine (string basePath, string relativePath)
70                 {
71                         basePath = Normalize (basePath);
72
73                         if (IsRooted (relativePath))
74                                 return Normalize (relativePath);
75
76                         int basePathLen = basePath.Length;
77                         if (basePath [basePathLen - 1] != '/') {
78                                 if (basePathLen > 1) {
79                                         int lastSlash = basePath.LastIndexOf ('/');
80                                         if (lastSlash >= 0)
81                                                 basePath = basePath.Substring (0, lastSlash + 1);
82                                 } else { // "~" only
83                                         basePath += "/";
84                                 }
85                         }
86
87                         return Normalize (basePath + relativePath);
88                 }
89
90                 public static string GetDirectory (string virtualPath)
91                 {
92                         return GetDirectory (virtualPath, true);
93                 }
94
95                 internal static string GetDirectory (string virtualPath, bool normalize)
96                 {
97                         if (normalize)
98                                 virtualPath = Normalize (virtualPath);
99
100                         int vpLen = virtualPath.Length;
101                         if (IsAppRelative (virtualPath) && vpLen < 3) { // "~" or "~/"
102                                 virtualPath = ToAbsolute (virtualPath);
103                                 vpLen = virtualPath.Length;
104                         }
105                         
106                         if (vpLen == 1 && virtualPath [0] == '/') // "/"
107                                 return null;
108
109                         int last = virtualPath.LastIndexOf ('/', vpLen - 2, vpLen - 2);
110                         if (last > 0)
111                                 return virtualPath.Substring (0, last + 1);
112                         else
113                                 return "/";
114                 }
115
116                 public static string GetExtension (string virtualPath)
117                 {
118                         if (StrUtils.IsNullOrEmpty (virtualPath))
119                                 throw new ArgumentNullException ("virtualPath");
120
121                         virtualPath = Canonize (virtualPath);
122
123                         int dot = virtualPath.LastIndexOf ('.');
124                         if (dot == -1 || dot == virtualPath.Length - 1 || dot < virtualPath.LastIndexOf ('/'))
125                                 return String.Empty;
126
127                         return virtualPath.Substring (dot);
128                 }
129
130                 public static string GetFileName (string virtualPath)
131                 {
132                         virtualPath = Normalize (virtualPath);
133                         
134                         if (IsAppRelative (virtualPath) && virtualPath.Length < 3) { // "~" or "~/"
135                                 virtualPath = ToAbsolute (virtualPath);
136                         }
137
138                         if (virtualPath.Length == 1 && virtualPath [0] == '/') { // "/"
139                                 return String.Empty;
140                         }
141
142                         virtualPath = RemoveTrailingSlash (virtualPath);
143                         int last = virtualPath.LastIndexOf ('/');
144                         return virtualPath.Substring (last + 1);
145                 }
146
147                 internal static bool IsRooted (string virtualPath)
148                 {
149                         return IsAbsolute (virtualPath) || IsAppRelative (virtualPath);
150                 }
151
152                 public static bool IsAbsolute (string virtualPath)
153                 {
154                         if (StrUtils.IsNullOrEmpty (virtualPath))
155                                 throw new ArgumentNullException ("virtualPath");
156
157                         return (virtualPath [0] == '/' || virtualPath [0] == '\\');
158                 }
159
160                 public static bool IsAppRelative (string virtualPath)
161                 {
162                         if (StrUtils.IsNullOrEmpty (virtualPath))
163                                 throw new ArgumentNullException ("virtualPath");
164
165                         if (virtualPath.Length == 1 && virtualPath [0] == '~')
166                                 return true;
167
168                         if (virtualPath [0] == '~' && (virtualPath [1] == '/' || virtualPath [1] == '\\'))
169                                 return true;
170
171                         return false;
172                 }
173
174                 // MSDN: If the fromPath and toPath parameters are not rooted; that is, 
175                 // they do not equal the root operator (the tilde [~]), do not start with a tilde (~), 
176                 // such as a tilde and a slash mark (~/) or a tilde and a double backslash (~//), 
177                 // or do not start with a slash mark (/), an ArgumentException exception is thrown.
178                 public static string MakeRelative (string fromPath, string toPath)
179                 {
180                         if (fromPath == null || toPath == null)
181                                 throw new NullReferenceException (); // yeah!
182
183                         if (toPath == "")
184                                 return toPath;
185
186                         toPath = ToAbsoluteInternal (toPath);
187                         fromPath = ToAbsoluteInternal (fromPath);
188
189                         if (String.CompareOrdinal (fromPath, toPath) == 0 && fromPath [fromPath.Length - 1] == '/')
190                                 return "./";
191
192                         string [] toPath_parts = toPath.Split ('/');
193                         string [] fromPath_parts = fromPath.Split ('/');
194                         int dest = 1;
195                         while (toPath_parts [dest] == fromPath_parts [dest]) {
196                                 if (toPath_parts.Length == (dest + 1) || fromPath_parts.Length == (dest + 1)) {
197                                         break;
198                                 }
199                                 dest++;
200                         }
201                         StringBuilder res = new StringBuilder();
202                         for (int i = 1; i < fromPath_parts.Length - dest; i++) {
203                                 res.Append ("../");
204                         }
205                         for (int i = dest; i < toPath_parts.Length; i++) {
206                                 res.Append (toPath_parts [i]);
207                                 if (i < toPath_parts.Length - 1)
208                                         res.Append ('/');
209                         }
210                         return res.ToString ();
211                 }
212
213                 static string ToAbsoluteInternal (string virtualPath)
214                 {
215                         if (IsAppRelative (virtualPath))
216                                 return ToAbsolute (virtualPath, HttpRuntime.AppDomainAppVirtualPath);
217                         else if (IsAbsolute (virtualPath))
218                                 return Normalize (virtualPath);
219
220                         throw new ArgumentOutOfRangeException ("Specified argument was out of the range of valid values.");
221                 }
222
223                 public static string RemoveTrailingSlash (string virtualPath)
224                 {
225                         if (virtualPath == null || virtualPath == "")
226                                 return null;
227
228                         int last = virtualPath.Length - 1;
229                         if (last == 0 || virtualPath [last] != '/')
230                                 return virtualPath;
231
232                         return virtualPath.Substring (0, last);
233                 }
234
235                 public static string ToAbsolute (string virtualPath)
236                 {
237                         return ToAbsolute (virtualPath, true);
238                 }
239
240                 internal static string ToAbsolute (string virtualPath, bool normalize)
241                 {
242                         if (IsAbsolute (virtualPath)) {
243                                 if (normalize)
244                                         return Normalize (virtualPath);
245                                 else
246                                         return virtualPath;
247                         }
248
249                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
250                         if (apppath == null)
251                                 throw new HttpException ("The path to the application is not known");
252
253                         if (virtualPath.Length == 1 && virtualPath [0] == '~')
254                                 return apppath;
255
256                         return ToAbsolute (virtualPath,apppath);
257                 }
258
259                 // If virtualPath is: 
260                 // Absolute, the ToAbsolute method returns the virtual path with no changes.
261                 // Application relative, the ToAbsolute method adds applicationPath to the beginning of the virtual path.
262                 // Not rooted, the ToAbsolute method raises an ArgumentOutOfRangeException exception.
263                 public static string ToAbsolute (string virtualPath, string applicationPath)
264                 {
265                         return ToAbsolute (virtualPath, applicationPath, true);
266                 }
267
268                 internal static string ToAbsolute (string virtualPath, string applicationPath, bool normalize)
269                 {
270                         if (StrUtils.IsNullOrEmpty (applicationPath))
271                                 throw new ArgumentNullException ("applicationPath");
272
273                         if (StrUtils.IsNullOrEmpty (virtualPath))
274                                 throw new ArgumentNullException ("virtualPath");
275
276                         if (IsAppRelative(virtualPath)) {
277                                 if (applicationPath [0] != '/')
278                                         throw new ArgumentException ("appPath is not rooted", "applicationPath");
279                                         
280                                 string path = applicationPath + (virtualPath.Length == 1 ? "/" : virtualPath.Substring (1));
281                                 if (normalize)
282                                         return Normalize (path);
283                                 else
284                                         return path;
285                         }
286
287                         if (virtualPath [0] != '/')
288                                 throw new ArgumentException (String.Format ("Relative path not allowed: '{0}'", virtualPath));
289
290                         if (normalize)
291                                 return Normalize (virtualPath);
292                         else
293                                 return virtualPath;
294
295                 }
296
297                 public static string ToAppRelative (string virtualPath)
298                 {
299                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
300                         if (apppath == null)
301                                 throw new HttpException ("The path to the application is not known");
302
303                         return ToAppRelative (virtualPath, apppath);
304                 }
305
306                 public static string ToAppRelative (string virtualPath, string applicationPath)
307                 {
308                         virtualPath = Normalize (virtualPath);
309                         if (IsAppRelative (virtualPath))
310                                 return virtualPath;
311
312                         if (!IsAbsolute (applicationPath))
313                                 throw new ArgumentException ("appPath is not absolute", "applicationPath");
314                         
315                         applicationPath = Normalize (applicationPath);
316
317                         if (applicationPath.Length == 1)
318                                 return "~" + virtualPath;
319
320                         int appPath_lenght = applicationPath.Length;
321                         if (String.CompareOrdinal (virtualPath, applicationPath) == 0)
322                                 return "~/";
323                         if (String.CompareOrdinal (virtualPath, 0, applicationPath, 0, appPath_lenght) == 0)
324                                 return "~" + virtualPath.Substring (appPath_lenght);
325
326                         return virtualPath;
327                 }
328
329                 static char [] path_sep = { '/' };
330
331                 internal static string Normalize (string path)
332                 {
333                         if (!IsRooted (path))
334                                 throw new ArgumentException (String.Format ("The relative virtual path '{0}' is not allowed here.", path));
335
336                         if (path.Length == 1) // '/' or '~'
337                                 return path;
338
339                         path = Canonize (path);
340
341                         int dotPos = path.IndexOf ('.');
342                         while (dotPos >= 0) {
343                                 if (++dotPos == path.Length)
344                                         break;
345
346                                 char nextChar = path [dotPos];
347
348                                 if ((nextChar == '/') || (nextChar == '.'))
349                                         break;
350
351                                 dotPos = path.IndexOf ('.', dotPos);
352                         }
353
354                         if (dotPos < 0)
355                                 return path;
356
357                         bool starts_with_tilda = false;
358                         bool ends_with_slash = false;
359                         string [] apppath_parts= null;
360
361                         if (path [0] == '~') {
362                                 if (path.Length == 2) // "~/"
363                                         return "~/";
364                                 starts_with_tilda = true;
365                                 path = path.Substring (1);
366                         }
367                         else if (path.Length == 1) { // "/"
368                                 return "/";
369                         }
370
371                         if (path [path.Length - 1] == '/')
372                                 ends_with_slash = true;
373
374                         string [] parts = StrUtils.SplitRemoveEmptyEntries (path, path_sep);
375                         int end = parts.Length;
376
377                         int dest = 0;
378
379                         for (int i = 0; i < end; i++) {
380                                 string current = parts [i];
381                                 if (current == ".")
382                                         continue;
383
384                                 if (current == "..") {
385                                         dest--;
386
387                                         if(dest >= 0)
388                                                 continue;
389
390                                         if (starts_with_tilda) {
391                                                 if (apppath_parts == null) {
392                                                         string apppath = HttpRuntime.AppDomainAppVirtualPath;
393                                                         apppath_parts = StrUtils.SplitRemoveEmptyEntries (apppath, path_sep);
394                                                 }
395
396                                                 if ((apppath_parts.Length + dest) >= 0)
397                                                         continue;
398                                         }
399                                         
400                                         throw new HttpException ("Cannot use a leading .. to exit above the top directory.");
401                                 }
402
403                                 if (dest >= 0)
404                                         parts [dest] = current;
405                                 else
406                                         apppath_parts [apppath_parts.Length + dest] = current;
407                                 
408                                 dest++;
409                         }
410
411                         StringBuilder str = new StringBuilder();
412                         if (apppath_parts != null) {
413                                 starts_with_tilda = false;
414                                 int count = apppath_parts.Length;
415                                 if (dest < 0)
416                                         count += dest;
417                                 for (int i = 0; i < count; i++) {
418                                         str.Append ('/');
419                                         str.Append (apppath_parts [i]);
420                                 }
421                         }
422                         else if (starts_with_tilda) {
423                                 str.Append ('~');
424                         }
425
426                         for (int i = 0; i < dest; i++) {
427                                 str.Append ('/');
428                                 str.Append (parts [i]);
429                         }
430
431                         if (str.Length > 0) {
432                                 if (ends_with_slash)
433                                         str.Append ('/');
434                         }
435                         else {
436                                 return "/";
437                         }
438
439                         return str.ToString ();
440                 }
441
442                 internal static string Canonize (string path)
443                 {
444                         int index = -1;
445                         for (int i=0; i < path.Length; i++) {
446                                 if ((path [i] == '\\') || (path [i] == '/' && (i + 1) < path.Length && (path [i + 1] == '/' || path [i + 1] == '\\'))) {
447                                         index = i;
448                                         break;
449                                 }
450                         }
451                         if (index < 0)
452                                 return path;
453
454                         StringBuilder sb = new StringBuilder (path.Length);
455                         sb.Append (path, 0, index);
456
457                         for (int i = index; i < path.Length; i++) {
458                                 if (path [i] == '\\' || path [i] == '/') {
459                                         int next = i + 1;
460                                         if (next < path.Length && (path [next] == '\\' || path [next] == '/'))
461                                                 continue;
462                                         sb.Append ('/');
463                                 }
464                                 else {
465                                         sb.Append (path [i]);
466                                 }
467                         }
468
469                         return sb.ToString ();
470                 }
471
472                 // See: http://support.microsoft.com/kb/932552
473                 // See: https://bugzilla.novell.com/show_bug.cgi?id=509163
474                 static readonly char[] invalidVirtualPathChars = {':', '*'};
475                 static readonly string aspNetVerificationKey = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\ASP.NET";
476                 internal static bool IsValidVirtualPath (string path)
477                 {
478                         if (path == null)
479                                 return false;
480
481                         bool doValidate = true;
482                         if (runningOnWindows) {
483                                 try {
484                                         object v = Registry.GetValue (aspNetVerificationKey, "VerificationCompatibility", null);
485                                         if (v != null && v is int)
486                                                 doValidate = (int)v != 1;
487                                 } catch {
488                                         // ignore
489                                 }
490                         }
491
492                         if (doValidate)
493                                 doValidate = monoSettingsVerifyCompatibility;
494
495                         if (!doValidate)
496                                 return true;
497
498                         return path.IndexOfAny (invalidVirtualPathChars) == -1;
499                 }
500         }
501 }