// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.WebPages.Deployment.Resources;
using Microsoft.Internal.Web.Utils;
using Microsoft.Win32;
namespace System.Web.WebPages.Deployment
{
public static class WebPagesDeployment
{
private const string AppSettingsVersionKey = "webpages:Version";
private const string AppSettingsEnabledKey = "webpages:Enabled";
///
/// File name for a temporary file that we drop in bin to force recompilation.
///
private const string ForceRecompilationFile = "WebPagesRecompilation.deleteme";
private const string WebPagesRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET Web Pages\v{0}.{1}";
internal static readonly string CacheKeyPrefix = "__System.Web.WebPages.Deployment__";
private static readonly string[] _webPagesExtensions = new[] { ".cshtml", ".vbhtml" };
private static readonly object _installPathNotFound = new object();
private static readonly IFileSystem _fileSystem = new PhysicalFileSystem();
/// Physical or virtual path to a directory where we need to determine the version of WebPages to be used.
///
/// In a non-hosted scenario, this method would only look at a web.config that is present at the current path. Any config settings at an
/// ancestor directory would not be considered.
///
public static Version GetVersionWithoutEnabledCheck(string path)
{
if (String.IsNullOrEmpty(path))
{
throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
}
var binDirectory = GetBinDirectory(path);
var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, _fileSystem);
var maxVersion = AssemblyUtils.GetMaxWebPagesVersion();
return GetVersionInternal(GetAppSettings(path), binVersion, maxVersion);
}
[Obsolete("This method is obsolete and is meant for legacy code. Use GetVersionWithoutEnabled instead.")]
public static Version GetVersion(string path)
{
return GetObsoleteVersionInternal(path, GetAppSettings(path), new PhysicalFileSystem(), AssemblyUtils.GetMaxWebPagesVersion);
}
///
/// This is meant to test an obsolete method. Don't use this!
///
internal static Version GetObsoleteVersionInternal(string path, NameValueCollection configuration, IFileSystem fileSystem, Func getMaxWebPagesVersion)
{
if (String.IsNullOrEmpty(path))
{
throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
}
var binDirectory = GetBinDirectory(path);
var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, _fileSystem);
var version = GetVersionInternal(configuration, binVersion, defaultVersion: null);
if (version != null)
{
// If a webpages version is available in config or bin, return it.
return version;
}
else if (AppRootContainsWebPagesFile(fileSystem, path))
{
// If the path points to a WebPages site, return the highest version.
return getMaxWebPagesVersion();
}
return null;
}
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This operation might be expensive since it has to reflect over Assembly names.")]
public static Version GetMaxVersion()
{
return AssemblyUtils.GetMaxWebPagesVersion();
}
///
/// Determines if Asp.Net Web Pages is enabled.
/// Web Pages is enabled if there's a webPages:Enabled key in AppSettings is set to "true" or if there's a cshtml file in the current path
/// and the key is not present.
///
/// The path at which to determine if web pages is enabled.
///
/// In a non-hosted scenario, this method would only look at a web.config that is present at the current path. Any config settings at an
/// ancestor directory would not be considered.
///
public static bool IsEnabled(string path)
{
if (String.IsNullOrEmpty(path))
{
throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
}
return IsEnabled(_fileSystem, path, GetAppSettings(path));
}
///
/// In a non-hosted scenario, this method would only look at a web.config that is present at the current path. Any config settings at an
/// ancestor directory would not be considered.
///
public static bool IsExplicitlyDisabled(string path)
{
if (String.IsNullOrEmpty(path))
{
throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
}
return IsExplicitlyDisabled(GetAppSettings(path));
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static IDictionary GetIncompatibleDependencies(string appPath)
{
if (String.IsNullOrEmpty(appPath))
{
throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "appPath");
}
var configFilePath = Path.Combine(appPath, "web.config");
var assemblyReferences = AppDomainHelper.GetBinAssemblyReferences(appPath, configFilePath);
return AssemblyUtils.GetAssembliesMatchingOtherVersions(assemblyReferences);
}
internal static bool IsExplicitlyDisabled(NameValueCollection appSettings)
{
bool? enabled = GetEnabled(appSettings);
return enabled.HasValue && enabled.Value == false;
}
internal static bool IsEnabled(IFileSystem fileSystem, string path, NameValueCollection appSettings)
{
bool? enabled = GetEnabled(appSettings);
if (!enabled.HasValue)
{
return AppRootContainsWebPagesFile(fileSystem, path);
}
return enabled.Value;
}
///
/// Returns the value for webPages:Enabled AppSetting value in web.config.
///
private static bool? GetEnabled(NameValueCollection appSettings)
{
string enabledSetting = appSettings.Get(AppSettingsEnabledKey);
if (String.IsNullOrEmpty(enabledSetting))
{
return null;
}
else
{
return Boolean.Parse(enabledSetting);
}
}
///
/// Returns the version of WebPages to be used for a specified path.
///
///
/// This method would always returns a value regardless of web pages is explicitly disabled (via config) or implicitly disabled (by virtue of not having a cshtml file) at
/// the specified path.
///
internal static Version GetVersionInternal(NameValueCollection appSettings, Version binVersion, Version defaultVersion)
{
// Return version values with the following precedence:
// 1) Version in config
// 2) Version in bin
// 3) defaultVersion.
return GetVersionFromConfig(appSettings) ?? binVersion ?? defaultVersion;
}
///
/// Gets full path to a folder that contains ASP.NET WebPages assemblies for a given version. Used by
/// WebMatrix and Visual Studio so they know what to copy to an app's Bin folder or deploy to a hoster.
///
public static string GetAssemblyPath(Version version)
{
if (version == null)
{
throw new ArgumentNullException("version");
}
string webPagesRegistryKey = String.Format(CultureInfo.InvariantCulture, WebPagesRegistryKey, version.Major, version.Minor);
object installPath = Registry.GetValue(webPagesRegistryKey, "InstallPath", _installPathNotFound);
if (installPath == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
ConfigurationResources.WebPagesRegistryKeyDoesNotExist, webPagesRegistryKey));
}
else if (installPath == _installPathNotFound)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
ConfigurationResources.InstallPathNotFound, webPagesRegistryKey));
}
return Path.Combine((string)installPath, "Assemblies");
}
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This operation might be expensive since it has to reflect over Assembly names.")]
public static IEnumerable GetWebPagesAssemblies()
{
return AssemblyUtils.GetAssembliesForVersion(AssemblyUtils.ThisAssemblyName.Version);
}
private static NameValueCollection GetAppSettings(string path)
{
if (path.StartsWith("~/", StringComparison.Ordinal))
{
// Path is virtual, assume we're hosted
return (NameValueCollection)WebConfigurationManager.GetSection("appSettings", path);
}
else
{
// Path is physical, map it to an application
WebConfigurationFileMap fileMap = new WebConfigurationFileMap();
fileMap.VirtualDirectories.Add("/", new VirtualDirectoryMapping(path, true));
var config = WebConfigurationManager.OpenMappedWebConfiguration(fileMap, "/");
var appSettingsSection = config.AppSettings;
var appSettings = new NameValueCollection();
foreach (KeyValueConfigurationElement element in appSettingsSection.Settings)
{
appSettings.Add(element.Key, element.Value);
}
return appSettings;
}
}
internal static Version GetVersionFromConfig(NameValueCollection appSettings)
{
string version = appSettings.Get(AppSettingsVersionKey);
// Version will be null if the config section is registered but not present in app web.config.
if (!String.IsNullOrEmpty(version))
{
// Build and Revision are optional in config but required by Fusion, so we set them to 0 if unspecified in config.
// Valid in config: "1.0", "1.0.0", "1.0.0.0"
var fullVersion = new Version(version);
if (fullVersion.Build == -1 || fullVersion.Revision == -1)
{
fullVersion = new Version(fullVersion.Major, fullVersion.Minor,
fullVersion.Build == -1 ? 0 : fullVersion.Build,
fullVersion.Revision == -1 ? 0 : fullVersion.Revision);
}
return fullVersion;
}
return null;
}
internal static bool AppRootContainsWebPagesFile(IFileSystem fileSystem, string path)
{
var files = fileSystem.EnumerateFiles(path);
return files.Any(IsWebPagesFile);
}
private static bool IsWebPagesFile(string file)
{
var extension = Path.GetExtension(file);
return _webPagesExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
///
/// HttpRuntime.BinDirectory is unavailable in design time and throws if we try to access it. To workaround this, if we aren't hosted,
/// we will assume that the path that was passed to us is the application root.
///
///
///
private static string GetBinDirectory(string path)
{
if (HostingEnvironment.IsHosted)
{
return HttpRuntime.BinDirectory;
}
return Path.Combine(path, "bin");
}
///
/// Reads a previously persisted version number from build manager's cached directory.
///
/// Null if a previous version number does not exist or is not a valid version number, read version number otherwise.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to throw an exception from this method.")]
internal static Version GetPreviousRuntimeVersion(IBuildManager buildManagerFileSystem)
{
string fileName = GetCachedFileName();
try
{
Stream stream = buildManagerFileSystem.ReadCachedFile(fileName);
if (stream == null)
{
return null;
}
using (StreamReader reader = new StreamReader(stream))
{
string text = reader.ReadLine();
Version version;
if (Version.TryParse(text, out version))
{
return version;
}
}
}
catch
{
}
return null;
}
///
/// Persists the version number in a file under the build manager's cached directory.
///
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to throw an exception from this method.")]
internal static void PersistRuntimeVersion(IBuildManager buildManager, Version version)
{
string fileName = GetCachedFileName();
try
{
Stream stream = buildManager.CreateCachedFile(fileName);
using (var writer = new StreamWriter(stream))
{
writer.WriteLine(version.ToString());
}
}
catch
{
}
}
///
/// Forces recompilation of the application by dropping a file under bin.
///
/// File system instance used to write a file to bin directory.
/// Path to bin directory of the application
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to throw an exception from this method.")]
internal static void ForceRecompile(IFileSystem fileSystem, string binDirectory)
{
var fileToWrite = Path.Combine(binDirectory, ForceRecompilationFile);
try
{
// Note: We should use BuildManager::ForceRecompile once that method makes it into System.Web.
using (var writer = new StreamWriter(fileSystem.OpenFile(fileToWrite)))
{
writer.WriteLine();
}
}
catch
{
}
}
///
/// Name of the the temporary file used by BuildManager.CreateCachedFile / BuildManager.ReadCachedFile where we cache WebPages's version number.
///
///
private static string GetCachedFileName()
{
return typeof(WebPagesDeployment).Namespace;
}
private static string RemoveTrailingSlash(string path)
{
if (!String.IsNullOrEmpty(path))
{
path = path.TrimEnd(Path.DirectorySeparatorChar);
}
return path;
}
}
}