1 //------------------------------------------------------------------------------
2 // <copyright file="UrlPath.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
10 * Copyright (c) 1999 Microsoft Corporation
13 namespace System.Web.Util {
15 using System.Runtime.Serialization.Formatters;
16 using System.Runtime.InteropServices;
17 using System.Collections;
18 using System.Globalization;
20 using System.Web.Hosting;
23 * Code to perform Url path combining
25 internal static class UrlPath {
27 internal const char appRelativeCharacter = '~';
28 internal const string appRelativeCharacterString = "~/";
30 private static char[] s_slashChars = new char[] { '\\', '/' };
32 internal static bool IsRooted(String basepath) {
33 return (String.IsNullOrEmpty(basepath) || basepath[0] == '/' || basepath[0] == '\\');
36 // Checks if virtual path contains a protocol, which is referred to as a scheme in the
38 private static bool HasScheme(string virtualPath) {
39 // URIs have the format <scheme>:<scheme-specific-path>, e.g. mailto:user@ms.com,
40 // http://server/, nettcp://server/, etc. The <scheme> cannot contain slashes.
41 // The virtualPath passed to this method may be absolute or relative. Although
42 // ':' is only allowed in the <scheme-specific-path> if it is encoded, the
43 // virtual path that we're receiving here may be decoded, so it is impossible
44 // for us to determine if virtualPath has a scheme. We will be conservative
45 // and err on the side of assuming it has a scheme when we cannot tell for certain.
46 // To do this, we first check for ':'. If not found, then it doesn't have a scheme.
47 // If ':' is found, then as long as we find a '/' before the ':', it cannot be
48 // a scheme because schemes don't contain '/'. Otherwise, we will assume it has a
50 int indexOfColon = virtualPath.IndexOf(':');
51 if (indexOfColon == -1)
53 int indexOfSlash = virtualPath.IndexOf('/');
54 return (indexOfSlash == -1 || indexOfColon < indexOfSlash);
57 // Returns whether the virtual path is relative. Note that this returns true for
58 // app relative paths (e.g. "~/sub/foo.aspx")
59 internal static bool IsRelativeUrl(string virtualPath) {
60 // If it has a protocol, it's not relative
61 if (HasScheme(virtualPath))
64 return !IsRooted(virtualPath);
68 internal static bool IsAppRelativePath(string path) {
73 int len = path.Length;
76 if (len == 0) return false;
78 // It must start with ~
79 if (path[0] != appRelativeCharacter)
82 // Single character case: "~"
86 // If it's longer, checks if it starts with "~/" or "~\"
87 return path[1] == '\\' || path[1] == '/';
90 internal static bool IsValidVirtualPathWithoutProtocol(string path) {
93 return !HasScheme(path);
96 internal static String GetDirectory(String path) {
97 if (String.IsNullOrEmpty(path))
98 throw new ArgumentException(SR.GetString(SR.Empty_path_has_no_directory));
100 if (path[0] != '/' && path[0] != appRelativeCharacter)
101 throw new ArgumentException(SR.GetString(SR.Path_must_be_rooted, path));
103 // If it's just "~" or "/", return it unchanged
104 if (path.Length == 1)
107 int slashIndex = path.LastIndexOf('/');
109 // This could happen if the input looks like "~abc"
111 throw new ArgumentException(SR.GetString(SR.Path_must_be_rooted, path));
113 return path.Substring(0, slashIndex + 1);
116 private static bool IsDirectorySeparatorChar(char ch) {
117 return (ch == '\\' || ch == '/');
120 internal static bool IsAbsolutePhysicalPath(string path) {
121 if (path == null || path.Length < 3)
125 if (path[1] == ':' && IsDirectorySeparatorChar(path[2]))
128 // e.g \\server\share\foo or //server/share/foo
129 return IsUncSharePath(path);
132 internal static bool IsUncSharePath(string path) {
133 // e.g \\server\share\foo or //server/share/foo
134 if (path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]))
140 internal static void CheckValidVirtualPath(string path) {
142 // Check if it looks like a physical path (UNC shares and C:)
143 if (IsAbsolutePhysicalPath(path)) {
144 throw new HttpException(SR.GetString(SR.Physical_path_not_allowed, path));
147 // Virtual path can't have colons.
148 int iqs = path.IndexOf('?');
150 path = path.Substring(0, iqs);
152 if (HasScheme(path)) {
153 throw new HttpException(SR.GetString(SR.Invalid_vpath, path));
157 private static String Combine(String appPath, String basepath, String relative) {
160 if (String.IsNullOrEmpty(relative))
161 throw new ArgumentNullException("relative");
162 if (String.IsNullOrEmpty(basepath))
163 throw new ArgumentNullException("basepath");
165 if (basepath[0] == appRelativeCharacter && basepath.Length == 1) {
166 // If it's "~", change it to "~/"
167 basepath = appRelativeCharacterString;
170 // If the base path includes a file name, get rid of it before combining
171 int lastSlashIndex = basepath.LastIndexOf('/');
172 Debug.Assert(lastSlashIndex >= 0);
173 if (lastSlashIndex < basepath.Length - 1) {
174 basepath = basepath.Substring(0, lastSlashIndex + 1);
178 // Make sure it's a virtual path (ASURT 73641)
179 CheckValidVirtualPath(relative);
181 if (IsRooted(relative)) {
186 // If the path is exactly "~", just return the app root path
187 if (relative.Length == 1 && relative[0] == appRelativeCharacter)
190 // If the relative path starts with "~/" or "~\", treat it as app root
191 // relative (ASURT 68628)
192 if (IsAppRelativePath(relative)) {
193 if (appPath.Length > 1)
194 path = appPath + "/" + relative.Substring(2);
196 path = "/" + relative.Substring(2);
198 path = SimpleCombine(basepath, relative);
205 internal static String Combine(String basepath, String relative) {
206 return Combine(HttpRuntime.AppDomainAppVirtualPathString, basepath, relative);
209 // This simple version of combine should only be used when the relative
210 // path is known to be relative. It's more efficient, but doesn't do any
212 internal static String SimpleCombine(String basepath, String relative) {
213 Debug.Assert(!String.IsNullOrEmpty(basepath));
214 Debug.Assert(!String.IsNullOrEmpty(relative));
215 Debug.Assert(relative[0] != '/');
217 if (HasTrailingSlash(basepath))
218 return basepath + relative;
220 return basepath + "/" + relative;
223 internal static String Reduce(String path) {
225 // ignore query string
226 String queryString = null;
228 int iqs = path.IndexOf('?');
230 queryString = path.Substring(iqs);
231 path = path.Substring(0, iqs);
235 // Take care of backslashes and duplicate slashes
236 path = FixVirtualPathSlashes(path);
238 path = ReduceVirtualPath(path);
240 return (queryString != null) ? (path + queryString) : path;
243 // Same as Reduce, but for a virtual path that is known to be well formed
244 internal static String ReduceVirtualPath(String path) {
246 int length = path.Length;
249 // quickly rule out situations in which there are no . or ..
251 for (examine = 0; ; examine++) {
252 examine = path.IndexOf('.', examine);
256 if ((examine == 0 || path[examine - 1] == '/')
257 && (examine + 1 == length || path[examine + 1] == '/' ||
258 (path[examine + 1] == '.' && (examine + 2 == length || path[examine + 2] == '/'))))
262 // OK, we found a . or .. so process it:
264 ArrayList list = new ArrayList();
265 StringBuilder sb = new StringBuilder();
271 examine = path.IndexOf('/', start + 1);
276 if (examine - start <= 3 &&
277 (examine < 1 || path[examine - 1] == '.') &&
278 (start + 1 >= length || path[start + 1] == '.')) {
279 if (examine - start == 3) {
281 throw new HttpException(SR.GetString(SR.Cannot_exit_up_top_directory));
283 // We're about to backtrack onto a starting '~', which would yield
284 // incorrect results. Instead, make the path App Absolute, and call
286 if (list.Count == 1 && IsAppRelativePath(path)) {
287 Debug.Assert(sb.Length == 1);
288 return ReduceVirtualPath(MakeVirtualPathAppAbsolute(path));
291 sb.Length = (int)list[list.Count - 1];
292 list.RemoveRange(list.Count - 1, 1);
298 sb.Append(path, start, examine - start);
301 if (examine == length)
305 string result = sb.ToString();
307 // If we end up with en empty string, turn it into either "/" or "." (VSWhidbey 289175)
308 if (result.Length == 0) {
309 if (length > 0 && path[0] == '/')
318 // Change backslashes to forward slashes, and remove duplicate slashes
319 internal static String FixVirtualPathSlashes(string virtualPath) {
321 // Make sure we don't have any back slashes
322 virtualPath = virtualPath.Replace('\\', '/');
324 // Replace any double forward slashes
326 string newPath = virtualPath.Replace("//", "/");
328 // If it didn't do anything, we're done
329 if ((object)newPath == (object)virtualPath)
332 // We need to loop again to take care of triple (or more) slashes (VSWhidbey 288782)
333 virtualPath = newPath;
339 // We use file: protocol instead of http:, so that Uri.MakeRelative behaves
340 // in a case insensitive way (VSWhidbey 80078)
341 private const string dummyProtocolAndServer = "file://foo";
343 // Return the relative vpath path from one rooted vpath to another
344 internal static string MakeRelative(string from, string to) {
346 // If either path is app relative (~/...), make it absolute, since the Uri
347 // class wouldn't know how to deal with it.
348 from = MakeVirtualPathAppAbsolute(from);
349 to = MakeVirtualPathAppAbsolute(to);
351 // Make sure both virtual paths are rooted
353 throw new ArgumentException(SR.GetString(SR.Path_must_be_rooted, from));
355 throw new ArgumentException(SR.GetString(SR.Path_must_be_rooted, to));
357 // Remove the query string, so that System.Uri doesn't corrupt it
358 string queryString = null;
360 int iqs = to.IndexOf('?');
362 queryString = to.Substring(iqs);
363 to = to.Substring(0, iqs);
367 // Uri's need full url's so, we use a dummy root
368 Uri fromUri = new Uri(dummyProtocolAndServer + from);
369 Uri toUri = new Uri(dummyProtocolAndServer + to);
373 // VSWhidbey 144946: If to and from points to identical path (excluding query and fragment), just use them instead
374 // of returning an empty string.
375 if (fromUri.Equals(toUri)) {
376 int iPos = to.LastIndexOfAny(s_slashChars);
380 // If it's the same directory, simply return "./"
381 // Browsers should interpret "./" as the original directory.
382 if (iPos == to.Length - 1) {
386 relativePath = to.Substring(iPos + 1);
394 // To avoid deprecation warning. It says we should use MakeRelativeUri instead (which returns a Uri),
395 // but we wouldn't gain anything from it. The way we use MakeRelative is hacky anyway (fake protocol, ...),
396 // and I don't want to take the chance of breaking something with this change.
397 #pragma warning disable 0618
398 relativePath = fromUri.MakeRelative(toUri);
399 #pragma warning restore 0618
402 // Note that we need to re-append the query string and fragment (e.g. #anchor)
403 return relativePath + queryString + toUri.Fragment;
406 internal static string GetDirectoryOrRootName(string path) {
409 dir = Path.GetDirectoryName(path);
411 dir = Path.GetPathRoot(path);
417 internal static string GetFileName(string virtualPath) {
418 // Code copied from CLR\BCL\System\IO\Path.cs
419 // - Check for invalid chars removed
420 // - Only '/' is used as separator (path.cs also used '\' and ':')
421 if (virtualPath != null) {
422 int length = virtualPath.Length;
423 for (int i = length; --i >= 0;) {
424 char ch = virtualPath[i];
426 return virtualPath.Substring(i + 1, length - i - 1);
433 internal static string GetFileNameWithoutExtension(string virtualPath) {
434 // Code copied from CLR\BCL\System\IO\Path.cs
435 // - Check for invalid chars removed
436 virtualPath = GetFileName(virtualPath);
437 if (virtualPath != null) {
439 if ((i=virtualPath.LastIndexOf('.')) == -1)
440 return virtualPath; // No extension found
442 return virtualPath.Substring(0,i);
447 internal static string GetExtension(string virtualPath) {
448 if (virtualPath == null)
451 int length = virtualPath.Length;
452 for (int i = length; --i >= 0;) {
453 char ch = virtualPath[i];
456 return virtualPath.Substring(i, length - i);
466 internal static bool HasTrailingSlash(string virtualPath) {
467 return virtualPath[virtualPath.Length - 1] == '/';
470 internal static string AppendSlashToPathIfNeeded(string path) {
472 if (path == null) return null;
475 if (l == 0) return path;
477 if (path[l-1] != '/')
484 // Remove the trailing forward slash ('/') except in the case of the root ("/").
485 // If the string is null or empty, return null, which represents a machine.config or root web.config.
487 internal static string RemoveSlashFromPathIfNeeded(string path) {
488 if (string.IsNullOrEmpty(path)) {
493 if (l <= 1 || path[l-1] != '/') {
497 return path.Substring(0, l-1);
500 private static bool VirtualPathStartsWithVirtualPath(string virtualPath1, string virtualPath2) {
501 if (virtualPath1 == null) {
502 throw new ArgumentNullException("virtualPath1");
505 if (virtualPath2 == null) {
506 throw new ArgumentNullException("virtualPath2");
509 // if virtualPath1 as a string doesn't start with virtualPath2 as s string, then no for sure
510 if (!StringUtil.StringStartsWithIgnoreCase(virtualPath1, virtualPath2)) {
514 int virtualPath2Length = virtualPath2.Length;
516 // same length - same path
517 if (virtualPath1.Length == virtualPath2Length) {
521 // Special case for apps rooted at the root. VSWhidbey 286145
522 if (virtualPath2Length == 1) {
523 Debug.Assert(virtualPath2[0] == '/');
527 // If virtualPath2 ends with a '/', it's definitely a child
528 if (virtualPath2[virtualPath2Length - 1] == '/')
531 // If it doesn't, make sure the next char in virtualPath1 is a '/'.
532 // e.g. /app1 vs /app11 (VSWhidbey 285038)
533 if (virtualPath1[virtualPath2Length] != '/') {
541 internal static bool VirtualPathStartsWithAppPath(string virtualPath) {
542 Debug.Assert(HttpRuntime.AppDomainAppVirtualPathObject != null);
543 return VirtualPathStartsWithVirtualPath(virtualPath,
544 HttpRuntime.AppDomainAppVirtualPathString);
547 internal static string MakeVirtualPathAppRelative(string virtualPath) {
548 Debug.Assert(HttpRuntime.AppDomainAppVirtualPathObject != null);
549 return MakeVirtualPathAppRelative(virtualPath,
550 HttpRuntime.AppDomainAppVirtualPathString, false /*nullIfNotInApp*/);
553 // Same as MakeVirtualPathAppRelative, but return null if app relative can't be obtained
554 internal static string MakeVirtualPathAppRelativeOrNull(string virtualPath) {
555 Debug.Assert(HttpRuntime.AppDomainAppVirtualPathObject != null);
556 return MakeVirtualPathAppRelative(virtualPath,
557 HttpRuntime.AppDomainAppVirtualPathString, true /*nullIfNotInApp*/);
560 // If a virtual path starts with the app path, make it start with
561 // ~ instead, so that it becomes application agnostic
562 // E.g. /MyApp/Sub/foo.aspx --> ~/Sub/foo.aspx
563 internal static string MakeVirtualPathAppRelative(string virtualPath,
564 string applicationPath, bool nullIfNotInApp) {
566 if (virtualPath == null)
567 throw new ArgumentNullException("virtualPath");
569 Debug.Assert(applicationPath[0] == '/');
570 Debug.Assert(HasTrailingSlash(applicationPath));
572 int appPathLength = applicationPath.Length;
573 int virtualPathLength = virtualPath.Length;
575 // If virtualPath is the same as the app path, but without the ending slash,
576 // treat it as if it were truly the app path (VSWhidbey 495949)
577 if (virtualPathLength == appPathLength - 1) {
578 if (StringUtil.StringStartsWithIgnoreCase(applicationPath, virtualPath))
579 return appRelativeCharacterString;
582 if (!VirtualPathStartsWithVirtualPath(virtualPath, applicationPath)) {
583 // If it doesn't start with the app path, return either null or the input path
590 // If they are the same, just return "~/"
591 if (virtualPathLength == appPathLength)
592 return appRelativeCharacterString;
594 // Special case for apps rooted at the root:
595 if (appPathLength == 1) {
596 return appRelativeCharacter + virtualPath;
599 return appRelativeCharacter + virtualPath.Substring(appPathLength-1);
602 internal static string MakeVirtualPathAppAbsolute(string virtualPath) {
603 Debug.Assert(HttpRuntime.AppDomainAppVirtualPathObject != null);
604 return MakeVirtualPathAppAbsolute(virtualPath, HttpRuntime.AppDomainAppVirtualPathString);
607 // If a virtual path is app relative (i.e. starts with ~/), change it to
608 // start with the actuall app path.
609 // E.g. ~/Sub/foo.aspx --> /MyApp/Sub/foo.aspx
610 internal static string MakeVirtualPathAppAbsolute(string virtualPath, string applicationPath) {
612 // If the path is exactly "~", just return the app root path
613 if (virtualPath.Length == 1 && virtualPath[0] == appRelativeCharacter)
614 return applicationPath;
616 // If the virtual path starts with "~/" or "~\", replace with the app path
617 // relative (ASURT 68628)
618 if (virtualPath.Length >=2 && virtualPath[0] == appRelativeCharacter &&
619 (virtualPath[1] == '/' || virtualPath[1] == '\\')) {
621 if (applicationPath.Length > 1) {
622 Debug.Assert(HasTrailingSlash(applicationPath));
623 return applicationPath + virtualPath.Substring(2);
626 return "/" + virtualPath.Substring(2);
629 // Don't allow relative paths, since they cannot be made App Absolute
630 if (!IsRooted(virtualPath))
631 throw new ArgumentOutOfRangeException("virtualPath");
633 // Return it unchanged
637 // To be called by APIs accepting virtual path that is expectedto be within the app.
638 // returns reduced absolute virtual path or throws
639 internal static string MakeVirtualPathAppAbsoluteReduceAndCheck(string virtualPath) {
640 if (virtualPath == null) {
641 throw new ArgumentNullException("virtualPath");
644 string path = Reduce(MakeVirtualPathAppAbsolute(virtualPath));
646 if (!UrlPath.VirtualPathStartsWithAppPath(path)) {
647 throw new ArgumentException(SR.GetString(SR.Invalid_app_VirtualPath, virtualPath));
653 internal static bool PathEndsWithExtraSlash(String path) {
657 if (l == 0 || path[l-1] != '\\')
659 if (l == 3 && path[1] == ':') // c:\ case
664 internal static bool PathIsDriveRoot(string path) {
667 if (l == 3 && path[1] == ':' && path[2] == '\\') {
676 // NOTE: This function is also present in fx\src\configuration\system\configuration\urlpath.cs
677 // Please propagate any changes to that file.
679 // Determine if subpath is a subpath of path.
680 // For example, /myapp/foo.aspx is a subpath of /myapp
681 // Account for optional trailing slashes.
683 internal static bool IsEqualOrSubpath(string path, string subpath) {
684 if (String.IsNullOrEmpty(path))
687 if (String.IsNullOrEmpty(subpath))
691 // Compare up to but not including trailing slash
693 int lPath = path.Length;
694 if (path[lPath - 1] == '/') {
698 int lSubpath = subpath.Length;
699 if (subpath[lSubpath - 1] == '/') {
703 if (lSubpath < lPath)
706 if (!StringUtil.EqualsIgnoreCase(path, 0, subpath, 0, lPath))
709 // Check subpath that character following length of path is a slash
710 if (lSubpath > lPath && subpath[lPath] != '/')
716 internal static bool IsPathOnSameServer(string absUriOrLocalPath, Uri currentRequestUri)
719 // (1) currentRequestUri does belong to the THIS host
720 // (2) absUriOrLocalPath is allowed to have different scheme like file:// or https://
721 // (3) absUriOrLocalPath is allowed to point "above" the currentRequestUri path
723 if (!Uri.TryCreate(absUriOrLocalPath, UriKind.Absolute, out absUri)) {
725 // A failure to construct absolute url (by System.Uri) doesn't implictly mean the url is relative (on the same server)
726 // Make sure the url path can't be recognized as absolute
727 return AppSettings.AllowRelaxedRelativeUrl ||
728 ((IsRooted(absUriOrLocalPath) || IsRelativeUrl(absUriOrLocalPath)) && !absUriOrLocalPath.TrimStart(' ').StartsWith("//", StringComparison.Ordinal));
731 return absUri.IsLoopback || string.Equals(currentRequestUri.Host, absUri.Host, StringComparison.OrdinalIgnoreCase);