diff --git a/src/Umbraco.Core/IO/IIOHelper.cs b/src/Umbraco.Core/IO/IIOHelper.cs
index 5a0f5cab45..8c84526268 100644
--- a/src/Umbraco.Core/IO/IIOHelper.cs
+++ b/src/Umbraco.Core/IO/IIOHelper.cs
@@ -21,6 +21,14 @@ namespace Umbraco.Core.IO
///
string MapPath(string path);
+ ///
+ /// Returns true if the path has a root, and is considered fully qualified for the OS it is on
+ /// See https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 for the .NET Standard 2.1 version of this
+ ///
+ /// The path to check
+ /// True if the path is fully qualified, false otherwise
+ bool IsPathFullyQualified(string path);
+
///
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
///
@@ -45,7 +53,7 @@ namespace Umbraco.Core.IO
/// A value indicating whether the filepath is valid.
bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions);
- bool PathStartsWith(string path, string root, char separator);
+ bool PathStartsWith(string path, string root, params char[] separators);
void EnsurePathExists(string path);
diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs
index 08241d28f4..e7f0586a2c 100644
--- a/src/Umbraco.Core/IO/IOHelper.cs
+++ b/src/Umbraco.Core/IO/IOHelper.cs
@@ -4,12 +4,13 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.InteropServices;
using Umbraco.Core.Hosting;
using Umbraco.Core.Strings;
namespace Umbraco.Core.IO
{
- public class IOHelper : IIOHelper
+ public abstract class IOHelper : IIOHelper
{
private readonly IHostingEnvironment _hostingEnvironment;
@@ -29,7 +30,7 @@ namespace Umbraco.Core.IO
if (virtualPath.StartsWith("~"))
retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath);
- if (virtualPath.StartsWith("/") && virtualPath.StartsWith(_hostingEnvironment.ApplicationVirtualPath) == false)
+ if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath))
retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart('/');
return retval;
@@ -64,19 +65,18 @@ namespace Umbraco.Core.IO
{
if (path == null) throw new ArgumentNullException(nameof(path));
- // Check if the path is already mapped
- if ((path.Length >= 2 && path[1] == Path.VolumeSeparatorChar)
- || path.StartsWith(@"\\")) //UNC Paths start with "\\". If the site is running off a network drive mapped paths will look like "\\Whatever\Boo\Bar"
+ // Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1
+ if (Path.IsPathRooted(path) &&
+ (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || // Linux paths are fully qualified as long as they are rooted, Windows paths can be rooted but not fully qualified
+ ((path.Length >= 2 && path[1] == Path.VolumeSeparatorChar) || path.StartsWith(@"\\") //UNC Paths start with "\\". If the site is running off a network drive mapped paths will look like "\\Whatever\Boo\Bar"
+ )))
{
return path;
}
- // Check that we even have an HttpContext! otherwise things will fail anyways
- // http://umbraco.codeplex.com/workitem/30946
-
if (_hostingEnvironment.IsHosted)
{
- var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || path.StartsWith(_hostingEnvironment.ApplicationVirtualPath)))
+ var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath)))
? _hostingEnvironment.MapPathWebRoot(path)
: _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart('/'));
@@ -91,6 +91,14 @@ namespace Umbraco.Core.IO
return retval;
}
+ ///
+ /// Returns true if the path has a root, and is considered fully qualified for the OS it is on
+ /// See https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 for the .NET Standard 2.1 version of this
+ ///
+ /// The path to check
+ /// True if the path is fully qualified, false otherwise
+ public abstract bool IsPathFullyQualified(string path);
+
///
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
@@ -121,7 +129,7 @@ namespace Umbraco.Core.IO
// not going to fix everything today
var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath);
- if (filePath.StartsWith(mappedRoot) == false)
+ if (!PathStartsWith(filePath, mappedRoot))
filePath = _hostingEnvironment.MapPathContentRoot(filePath);
// yes we can (see above)
@@ -131,10 +139,10 @@ namespace Umbraco.Core.IO
foreach (var dir in validDirs)
{
var validDir = dir;
- if (validDir.StartsWith(mappedRoot) == false)
+ if (!PathStartsWith(validDir, mappedRoot))
validDir = _hostingEnvironment.MapPathContentRoot(validDir);
- if (PathStartsWith(filePath, validDir, Path.DirectorySeparatorChar))
+ if (PathStartsWith(filePath, validDir))
return true;
}
@@ -153,16 +161,7 @@ namespace Umbraco.Core.IO
return ext != null && validFileExtensions.Contains(ext.TrimStart('.'));
}
- public bool PathStartsWith(string path, string root, char separator)
- {
- // either it is identical to root,
- // or it is root + separator + anything
-
- if (path.StartsWith(root, StringComparison.OrdinalIgnoreCase) == false) return false;
- if (path.Length == root.Length) return true;
- if (path.Length < root.Length) return false;
- return path[root.Length] == separator;
- }
+ public abstract bool PathStartsWith(string path, string root, params char[] separators);
public void EnsurePathExists(string path)
{
@@ -181,7 +180,7 @@ namespace Umbraco.Core.IO
if (path.IsFullPath())
{
var rootDirectory = MapPath("~");
- var relativePath = path.ToLowerInvariant().Replace(rootDirectory.ToLowerInvariant(), string.Empty);
+ var relativePath = PathStartsWith(path, rootDirectory) ? path.Substring(rootDirectory.Length) : path;
path = relativePath;
}
diff --git a/src/Umbraco.Core/IO/IOHelperLinux.cs b/src/Umbraco.Core/IO/IOHelperLinux.cs
new file mode 100644
index 0000000000..2c2e778740
--- /dev/null
+++ b/src/Umbraco.Core/IO/IOHelperLinux.cs
@@ -0,0 +1,28 @@
+using System;
+using System.IO;
+using System.Linq;
+using Umbraco.Core.Hosting;
+
+namespace Umbraco.Core.IO
+{
+ public class IOHelperLinux : IOHelper
+ {
+ public IOHelperLinux(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment)
+ {
+ }
+
+ public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
+
+ public override bool PathStartsWith(string path, string root, params char[] separators)
+ {
+ // either it is identical to root,
+ // or it is root + separator + anything
+
+ if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
+ if (!path.StartsWith(root, StringComparison.Ordinal)) return false;
+ if (path.Length == root.Length) return true;
+ if (path.Length < root.Length) return false;
+ return separators.Contains(path[root.Length]);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/IO/IOHelperOSX.cs b/src/Umbraco.Core/IO/IOHelperOSX.cs
new file mode 100644
index 0000000000..90d96998c3
--- /dev/null
+++ b/src/Umbraco.Core/IO/IOHelperOSX.cs
@@ -0,0 +1,29 @@
+using System;
+using System.IO;
+using System.Linq;
+using Umbraco.Core.Hosting;
+
+
+namespace Umbraco.Core.IO
+{
+ public class IOHelperOSX : IOHelper
+ {
+ public IOHelperOSX(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment)
+ {
+ }
+
+ public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
+
+ public override bool PathStartsWith(string path, string root, params char[] separators)
+ {
+ // either it is identical to root,
+ // or it is root + separator + anything
+
+ if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
+ if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false;
+ if (path.Length == root.Length) return true;
+ if (path.Length < root.Length) return false;
+ return separators.Contains(path[root.Length]);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/IO/IOHelperWindows.cs b/src/Umbraco.Core/IO/IOHelperWindows.cs
new file mode 100644
index 0000000000..3cffc27751
--- /dev/null
+++ b/src/Umbraco.Core/IO/IOHelperWindows.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Linq;
+using Umbraco.Core.Hosting;
+
+
+namespace Umbraco.Core.IO
+{
+ public class IOHelperWindows : IOHelper
+ {
+ public IOHelperWindows(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment)
+ {
+ }
+
+ public override bool IsPathFullyQualified(string path)
+ {
+ // TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1
+
+ if (path.Length < 2)
+ {
+ // It isn't fixed, it must be relative. There is no way to specify a fixed
+ // path with one character (or less).
+ return false;
+ }
+
+ if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar)
+ {
+ // There is no valid way to specify a relative path with two initial slashes or
+ // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
+ return path[1] == '?' || path[1] == Path.DirectorySeparatorChar || path[1] == Path.AltDirectorySeparatorChar;
+ }
+
+ // The only way to specify a fixed path that doesn't begin with two slashes
+ // is the drive, colon, slash format- i.e. C:\
+ return (path.Length >= 3)
+ && (path[1] == Path.VolumeSeparatorChar)
+ && (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar)
+ // To match old behavior we'll check the drive character for validity as the path is technically
+ // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
+ && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'));
+ }
+
+ public override bool PathStartsWith(string path, string root, params char[] separators)
+ {
+ // either it is identical to root,
+ // or it is root + separator + anything
+
+ if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
+ if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false;
+ if (path.Length == root.Length) return true;
+ if (path.Length < root.Length) return false;
+ return separators.Contains(path[root.Length]);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs
index 095879209a..9485865d8d 100644
--- a/src/Umbraco.Tests.Common/TestHelperBase.cs
+++ b/src/Umbraco.Tests.Common/TestHelperBase.cs
@@ -23,6 +23,7 @@ using Umbraco.Core.Strings;
using Umbraco.Web;
using Umbraco.Web.Routing;
using Umbraco.Tests.Common.Builders;
+using System.Runtime.InteropServices;
namespace Umbraco.Tests.Common
{
@@ -86,7 +87,11 @@ namespace Umbraco.Tests.Common
get
{
if (_ioHelper == null)
- _ioHelper = new IOHelper(GetHostingEnvironment());
+ {
+ var hostingEnvironment = GetHostingEnvironment();
+ _ioHelper = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? new IOHelperLinux(hostingEnvironment)
+ : (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? (IIOHelper)new IOHelperOSX(hostingEnvironment) : new IOHelperWindows(hostingEnvironment));
+ }
return _ioHelper;
}
}
diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
index c98cbca39e..348cccb9f1 100644
--- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
@@ -373,7 +373,8 @@ namespace Umbraco.Extensions
throw new InvalidOperationException($"Could not resolve type {typeof(GlobalSettings)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}");
hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment);
- ioHelper = new IOHelper(hostingEnvironment);
+ ioHelper = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? new IOHelperLinux(hostingEnvironment)
+ : (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? (IIOHelper)new IOHelperOSX(hostingEnvironment) : new IOHelperWindows(hostingEnvironment));
AddLogger(services, hostingEnvironment, loggingConfiguration, configuration);
backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings);
profiler = GetWebProfiler(hostingEnvironment);
diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs
index fe10cfba81..a04cea4fed 100644
--- a/src/Umbraco.Web/UmbracoApplicationBase.cs
+++ b/src/Umbraco.Web/UmbracoApplicationBase.cs
@@ -58,7 +58,7 @@ namespace Umbraco.Web
var hostingEnvironment = new AspNetHostingEnvironment(Options.Create(hostingSettings));
var loggingConfiguration = new LoggingConfiguration(
Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data\\Logs"));
- var ioHelper = new IOHelper(hostingEnvironment);
+ var ioHelper = new IOHelperWindows(hostingEnvironment);
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, new ConfigurationRoot(new List()));
var backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, ioHelper, _loggerFactory.CreateLogger(), Options.Create(webRoutingSettings));