The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button.
+ You can add your own health checks, have a look at the documentation for more information about custom health checks.
diff --git a/src/Umbraco.Web.UI/config/Dashboard.Release.config b/src/Umbraco.Web.UI/config/Dashboard.Release.config
index 8459a88028..fe0082f009 100644
--- a/src/Umbraco.Web.UI/config/Dashboard.Release.config
+++ b/src/Umbraco.Web.UI/config/Dashboard.Release.config
@@ -37,11 +37,6 @@
views/dashboard/developer/examinemanagement.html
-
-
- views/dashboard/developer/xmldataintegrityreport.html
-
-
@@ -97,4 +92,15 @@
+
+
+ developer
+
+
+
+ views/dashboard/developer/healthcheck.html
+
+
+
+
diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config
index 18adec4e90..6f12a0482c 100644
--- a/src/Umbraco.Web.UI/config/Dashboard.config
+++ b/src/Umbraco.Web.UI/config/Dashboard.config
@@ -34,11 +34,6 @@
views/dashboard/developer/examinemanagement.html
-
-
- views/dashboard/developer/xmldataintegrityreport.html
-
-
@@ -104,4 +99,14 @@
+
+
+ developer
+
+
+
+ views/dashboard/developer/healthcheck.html
+
+
+
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
index 84dedbb364..72a7289121 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
@@ -1271,4 +1271,106 @@ To manage your website, simply open the Umbraco back office and start adding con
...or enter a custom validationField is mandatory
+
+
+ Value is set to the recommended value: '%0%'.
+ Value was set to '%1%' for XPath '%2%' in configuration file '%3%'.
+ Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'.
+ Found unexpected value '%0%' for '%2%' in configuration file '%3%'.
+
+
+ Custom errors are set to '%0%'.
+ Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live.
+ Custom errors successfully set to '%0%'.
+
+ MacroErrors are set to '%0%'.
+ MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely when there's any errors in macros. Rectifying this will set the value to '%1%'.
+ MacroErrors are now set to '%0%'.
+
+
+ Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'.
+ Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%).
+ Try Skip IIS Custom Errors successfully set to '%0%'.
+
+
+ File does not exist: '%0%'.
+ '%0%' in config file '%1%'.]]>
+ There was an error, check log for full error: %0%.
+
+ Total XML: %0%, Total: %1%
+ Total XML: %0%, Total: %1%
+ Total XML: %0%, Total published: %1%
+
+ Certificate validation error: '%0%'
+ Error pinging the URL %0% - '%1%'
+ You are currently %0% viewing the site using the HTTPS scheme.
+ The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'.
+ The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure.
+ Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0%
+
+
+ Enable HTTPS
+ Sets umbracoSSL setting to true in the appSettings of the web.config file.
+ The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure.
+
+ Fix
+ Cannot fix a check with a value comparison type of 'ShouldNotEqual'.
+ Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value.
+ Value to fix check not provided.
+
+ Debug compilation mode is disabled.
+ Debug compilation mode is currently enabled. It is recommended to disable this setting before go live.
+ Debug compilation mode successfully disabled.
+
+ Trace mode is disabled.
+ Trace mode is currently enabled. It is recommended to disable this setting before go live.
+ Trace mode successfully disabled.
+
+ All folders have the correct permissions set.
+
+ %0%.]]>
+ %0%. If they aren't being written to no action need be taken.]]>
+
+ All files have the correct permissions set.
+
+ %0%.]]>
+ %0%. If they aren't being written to no action need be taken.]]>
+
+ X-Frame-Options used to control whether a site can be IFRAMed by another was found.]]>
+ X-Frame-Options used to control whether a site can be IFRAMed by another was not found.]]>
+ Set Header in Config
+ Added a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMed.
+ A setting to create a header preventing IFRAMing of the site has been added to your web.config file.
+ Could not update web.config file. Error: %0%
+
+
+ %0%.]]>
+ No headers revealing information about the website technology were found.
+
+ In the Web.config file, system.net/mailsettings could not be found.
+ In the Web.config file system.net/mailsettings section, the host is not configured.
+ SMTP settings are configured correctly and the service is operating as expected.
+ The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct.
+
+ %0%.]]>
+ %0%.]]>
+
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
index ec41299b25..ebbd08b2c8 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
@@ -1276,4 +1276,106 @@ To manage your website, simply open the Umbraco back office and start adding con
...or enter a custom validationField is mandatory
+
+
+ Value is set to the recommended value: '%0%'.
+ Value was set to '%1%' for XPath '%2%' in configuration file '%3%'.
+ Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'.
+ Found unexpected value '%0%' for '%2%' in configuration file '%3%'.
+
+
+ Custom errors are set to '%0%'.
+ Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live.
+ Custom errors successfully set to '%0%'.
+
+ MacroErrors are set to '%0%'.
+ MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely when there's any errors in macros. Rectifying this will set the value to '%1%'.
+ MacroErrors are now set to '%0%'.
+
+
+ Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'.
+ Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%).
+ Try Skip IIS Custom Errors successfully set to '%0%'.
+
+
+ File does not exist: '%0%'.
+ '%0%' in config file '%1%'.]]>
+ There was an error, check log for full error: %0%.
+
+ Total XML: %0%, Total: %1%
+ Total XML: %0%, Total: %1%
+ Total XML: %0%, Total published: %1%
+
+ Certificate validation error: '%0%'
+ Error pinging the URL %0% - '%1%'
+ You are currently %0% viewing the site using the HTTPS scheme.
+ The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'.
+ The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure.
+ Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0%
+
+
+ Enable HTTPS
+ Sets umbracoSSL setting to true in the appSettings of the web.config file.
+ The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure.
+
+ Fix
+ Cannot fix a check with a value comparison type of 'ShouldNotEqual'.
+ Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value.
+ Value to fix check not provided.
+
+ Debug compilation mode is disabled.
+ Debug compilation mode is currently enabled. It is recommended to disable this setting before go live.
+ Debug compilation mode successfully disabled.
+
+ Trace mode is disabled.
+ Trace mode is currently enabled. It is recommended to disable this setting before go live.
+ Trace mode successfully disabled.
+
+ All folders have the correct permissions set.
+
+ %0%.]]>
+ %0%. If they aren't being written to no action need be taken.]]>
+
+ All files have the correct permissions set.
+
+ %0%.]]>
+ %0%. If they aren't being written to no action need be taken.]]>
+
+ X-Frame-Options used to control whether a site can be IFRAMed by another was found.]]>
+ X-Frame-Options used to control whether a site can be IFRAMed by another was not found.]]>
+ Set Header in Config
+ Added a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMed.
+ A setting to create a header preventing IFRAMing of the site has been added to your web.config file.
+ Could not update web.config file. Error: %0%
+
+
+ %0%.]]>
+ No headers revealing information about the website technology were found.
+
+ In the Web.config file, system.net/mailsettings could not be found.
+ In the Web.config file system.net/mailsettings section, the host is not configured.
+ SMTP settings are configured correctly and the service is operating as expected.
+ The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct.
+
+ %0%.]]>
+ %0%.]]>
+
diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs
index 95e1f7803e..2103bedd1b 100644
--- a/src/Umbraco.Web/Editors/BackOfficeController.cs
+++ b/src/Umbraco.Web/Editors/BackOfficeController.cs
@@ -28,6 +28,7 @@ using Umbraco.Core.Manifest;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
+using Umbraco.Web.HealthCheck;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
@@ -350,6 +351,10 @@ namespace Umbraco.Web.Editors
{
"xmlDataIntegrityBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
controller => controller.CheckContentXmlTable())
+ },
+ {
+ "healthCheckBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAllHealthChecks())
}
}
},
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs
new file mode 100644
index 0000000000..001bd4e3e8
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Umbraco.Core.IO;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+
+ public abstract class AbstractConfigCheck : HealthCheck
+ {
+ private readonly ConfigurationService _configurationService;
+ private readonly ILocalizedTextService _textService;
+
+ ///
+ /// Gets the config file path.
+ ///
+ public abstract string FilePath { get; }
+
+ ///
+ /// Gets XPath statement to the config element to check.
+ ///
+ public abstract string XPath { get; }
+
+ ///
+ /// Gets the values to compare against.
+ ///
+ public abstract IEnumerable Values { get; }
+
+ ///
+ /// Gets the current value
+ ///
+ public string CurrentValue { get; set; }
+
+ ///
+ /// Gets the provided value
+ ///
+ public string ProvidedValue { get; set; }
+
+ ///
+ /// Gets the comparison type for checking the value.
+ ///
+ public abstract ValueComparisonType ValueComparisonType { get; }
+
+ protected AbstractConfigCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ _configurationService = new ConfigurationService(AbsoluteFilePath, XPath);
+ }
+
+ ///
+ /// Gets the name of the file.
+ ///
+ private string FileName
+ {
+ get { return Path.GetFileName(FilePath); }
+ }
+
+ ///
+ /// Gets the absolute file path.
+ ///
+ private string AbsoluteFilePath
+ {
+ get { return IOHelper.MapPath(FilePath); }
+ }
+
+ ///
+ /// Gets the message for when the check has succeeded.
+ ///
+ public virtual string CheckSuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/checkSuccessMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, XPath, AbsoluteFilePath });
+ }
+ }
+
+ ///
+ /// Gets the message for when the check has failed.
+ ///
+ public virtual string CheckErrorMessage
+ {
+ get
+ {
+ return ValueComparisonType == ValueComparisonType.ShouldEqual
+ ? _textService.Localize("healthcheck/checkErrorMessageDifferentExpectedValue",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, XPath, AbsoluteFilePath })
+ : _textService.Localize("healthcheck/checkErrorMessageUnexpectedValue",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, XPath, AbsoluteFilePath });
+ }
+ }
+
+ ///
+ /// Gets the rectify success message.
+ ///
+ public virtual string RectifySuccessMessage
+ {
+ get
+ {
+ var recommendedValue = Values.FirstOrDefault(v => v.IsRecommended);
+ var rectifiedValue = recommendedValue != null
+ ? recommendedValue.Value
+ : ProvidedValue;
+ return _textService.Localize("healthcheck/rectifySuccessMessage",
+ new[]
+ {
+ CurrentValue,
+ rectifiedValue,
+ XPath,
+ AbsoluteFilePath
+ });
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether this check can be rectified automatically.
+ ///
+ public virtual bool CanRectify
+ {
+ get { return ValueComparisonType == ValueComparisonType.ShouldEqual; }
+ }
+
+ ///
+ /// Gets a value indicating whether this check can be rectified automatically if a value is provided.
+ ///
+ public virtual bool CanRectifyWithValue
+ {
+ get { return ValueComparisonType == ValueComparisonType.ShouldNotEqual; }
+ }
+
+ public override IEnumerable GetStatus()
+ {
+ var configValue = _configurationService.GetConfigurationValue();
+ if (configValue.Success == false)
+ {
+ var message = configValue.Result;
+ return new[] { new HealthCheckStatus(message) { ResultType = StatusResultType.Error } };
+ }
+
+ CurrentValue = configValue.Result;
+
+ var valueFound = Values.Any(value => string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase));
+ if (ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound || ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false)
+ {
+ var message = string.Format(CheckSuccessMessage, FileName, XPath, Values, CurrentValue);
+ return new[] { new HealthCheckStatus(message) { ResultType = StatusResultType.Success } };
+ }
+
+ // Declare the action for rectifying the config value
+ var rectifyAction = new HealthCheckAction("rectify", Id)
+ {
+ Name = _textService.Localize("healthcheck/rectifyButton"),
+ ValueRequired = CanRectifyWithValue,
+ };
+
+ var resultMessage = string.Format(CheckErrorMessage, FileName, XPath, Values, CurrentValue);
+ return new[]
+ {
+ new HealthCheckStatus(resultMessage)
+ {
+ ResultType = StatusResultType.Error,
+ Actions = CanRectify || CanRectifyWithValue ? new[] { rectifyAction } : new HealthCheckAction[0]
+ }
+ };
+ }
+
+ ///
+ /// Rectifies this check.
+ ///
+ ///
+ public virtual HealthCheckStatus Rectify()
+ {
+ if (ValueComparisonType == ValueComparisonType.ShouldNotEqual)
+ throw new InvalidOperationException(_textService.Localize("healthcheck/cannotRectifyShouldNotEqual"));
+
+ var recommendedValue = Values.First(v => v.IsRecommended).Value;
+ return UpdateConfigurationValue(recommendedValue);
+ }
+
+ ///
+ /// Rectifies this check with a provided value.
+ ///
+ /// Value provided
+ ///
+ public virtual HealthCheckStatus Rectify(string value)
+ {
+ if (ValueComparisonType == ValueComparisonType.ShouldEqual)
+ throw new InvalidOperationException(_textService.Localize("healthcheck/cannotRectifyShouldEqualWithValue"));
+
+ if (string.IsNullOrWhiteSpace(value))
+ throw new InvalidOperationException(_textService.Localize("healthcheck/valueToRectifyNotProvided"));
+
+ // Need to track provided value in order to correctly put together the rectify message
+ ProvidedValue = value;
+
+ return UpdateConfigurationValue(value);
+ }
+
+ private HealthCheckStatus UpdateConfigurationValue(string value)
+ {
+ var updateConfigFile = _configurationService.UpdateConfigFile(value);
+
+ if (updateConfigFile.Success == false)
+ {
+ var message = updateConfigFile.Result;
+ return new HealthCheckStatus(message) { ResultType = StatusResultType.Error };
+ }
+
+ var resultMessage = string.Format(RectifySuccessMessage, FileName, XPath, Values);
+ return new HealthCheckStatus(resultMessage) { ResultType = StatusResultType.Success };
+ }
+
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ return string.IsNullOrEmpty(action.ProvidedValue)
+ ? Rectify()
+ : Rectify(action.ProvidedValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/AcceptableConfiguration.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/AcceptableConfiguration.cs
new file mode 100644
index 0000000000..e7c3036991
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/AcceptableConfiguration.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ public class AcceptableConfiguration
+ {
+ public string Value { get; set; }
+ public bool IsRecommended { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs
new file mode 100644
index 0000000000..989046b464
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ [HealthCheck("61214FF3-FC57-4B31-B5CF-1D095C977D6D", "Debug Compilation Mode",
+ Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.",
+ Group = "Live Environment")]
+ public class CompilationDebugCheck : AbstractConfigCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public CompilationDebugCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ public override string FilePath
+ {
+ get { return "~/Web.config"; }
+ }
+
+ public override string XPath
+ {
+ get { return "/configuration/system.web/compilation/@debug"; }
+ }
+
+ public override ValueComparisonType ValueComparisonType
+ {
+ get { return ValueComparisonType.ShouldEqual; }
+ }
+
+ public override IEnumerable Values
+ {
+ get
+ {
+ return new List
+ {
+ new AcceptableConfiguration { IsRecommended = true, Value = bool.FalseString.ToLower() }
+ };
+ }
+ }
+
+ public override string CheckSuccessMessage
+ {
+ get { return _textService.Localize("healthcheck/compilationDebugCheckSuccessMessage"); }
+ }
+
+ public override string CheckErrorMessage
+ {
+ get { return _textService.Localize("healthcheck/compilationDebugCheckErrorMessage"); }
+ }
+
+ public override string RectifySuccessMessage
+ {
+ get { return _textService.Localize("healthcheck/compilationDebugCheckRectifySuccessMessage"); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs
new file mode 100644
index 0000000000..dd92cfa5ec
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs
@@ -0,0 +1,115 @@
+using System;
+using System.IO;
+using System.Xml;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ //TODO: Add config transform for when config with specified XPath is not found
+
+ public class ConfigurationService
+ {
+ private readonly string _configFilePath;
+ private readonly string _xPath;
+ private readonly ILocalizedTextService _textService;
+
+ /// The absolute file location of the configuration file
+ /// The XPath to select the value
+ ///
+ public ConfigurationService(string configFilePath, string xPath)
+ {
+ _configFilePath = configFilePath;
+ _xPath = xPath;
+ _textService = UmbracoContext.Current.Application.Services.TextService;
+ }
+
+ ///
+ /// Gets a value from a given configuration file with the given XPath
+ ///
+ public ConfigurationServiceResult GetConfigurationValue()
+ {
+ try
+ {
+ if (File.Exists(_configFilePath) == false)
+ return new ConfigurationServiceResult
+ {
+ Success = false,
+ Result = _textService.Localize("healthcheck/configurationServiceFileNotFound", new[] { _configFilePath })
+ };
+
+ var xmlDocument = new XmlDocument();
+ xmlDocument.Load(_configFilePath);
+
+ var xmlNode = xmlDocument.SelectSingleNode(_xPath);
+ if (xmlNode == null)
+ return new ConfigurationServiceResult
+ {
+ Success = false,
+ Result = _textService.Localize("healthcheck/configurationServiceNodeNotFound", new[] { _xPath, _configFilePath })
+ };
+
+ return new ConfigurationServiceResult
+ {
+ Success = true,
+ Result = string.Format(xmlNode.Value ?? xmlNode.InnerText)
+ };
+ }
+ catch (Exception exception)
+ {
+ LogHelper.Error("Error trying to get configuration value", exception);
+ return new ConfigurationServiceResult
+ {
+ Success = false,
+ Result = _textService.Localize("healthcheck/configurationServiceError", new[] { exception.Message })
+ };
+ }
+ }
+
+ ///
+ /// Updates a value in a given configuration file with the given XPath
+ ///
+ ///
+ ///
+ public ConfigurationServiceResult UpdateConfigFile(string value)
+ {
+ try
+ {
+ if (File.Exists(_configFilePath) == false)
+ return new ConfigurationServiceResult
+ {
+ Success = false,
+ Result = _textService.Localize("healthcheck/configurationServiceFileNotFound", new[] { _configFilePath })
+ };
+
+ var xmlDocument = new XmlDocument { PreserveWhitespace = true };
+ xmlDocument.Load(_configFilePath);
+
+ var node = xmlDocument.SelectSingleNode(_xPath);
+ if (node == null)
+ return new ConfigurationServiceResult
+ {
+ Success = false,
+ Result = _textService.Localize("healthcheck/configurationServiceNodeNotFound", new[] { _xPath, _configFilePath })
+ };
+
+ if (node.NodeType == XmlNodeType.Element)
+ node.InnerText = value;
+ else
+ node.Value = value;
+
+ xmlDocument.Save(_configFilePath);
+ return new ConfigurationServiceResult { Success = true };
+ }
+ catch (Exception exception)
+ {
+ LogHelper.Error("Error trying to update configuration", exception);
+ return new ConfigurationServiceResult
+ {
+ Success = false,
+ Result = _textService.Localize("healthcheck/configurationServiceError", new[] { exception.Message })
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationServiceResult.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationServiceResult.cs
new file mode 100644
index 0000000000..e88492d6b2
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationServiceResult.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ public class ConfigurationServiceResult
+ {
+ public bool Success { get; set; }
+ public string Result { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs
new file mode 100644
index 0000000000..f6e47103e7
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Configuration;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ [HealthCheck("4090C0A1-2C52-4124-92DD-F028FD066A64", "Custom Errors",
+ Description = "Leaving custom errors off will display a complete stack trace to your visitors if an exception occurs.",
+ Group = "Live Environment")]
+ public class CustomErrorsCheck : AbstractConfigCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public CustomErrorsCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ public override string FilePath
+ {
+ get { return "~/Web.config"; }
+ }
+
+ public override string XPath
+ {
+ get { return "/configuration/system.web/customErrors/@mode"; }
+ }
+
+ public override ValueComparisonType ValueComparisonType
+ {
+ get { return ValueComparisonType.ShouldEqual; }
+ }
+
+ public override IEnumerable Values
+ {
+ get
+ {
+ return new List
+ {
+ new AcceptableConfiguration { IsRecommended = true, Value = CustomErrorsMode.RemoteOnly.ToString() },
+ new AcceptableConfiguration { IsRecommended = false, Value = "On" }
+ };
+ }
+ }
+
+ public override string CheckSuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/customErrorsCheckSuccessMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value });
+ }
+ }
+
+ public override string CheckErrorMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/customErrorsCheckErrorMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value });
+ }
+ }
+
+ public override string RectifySuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/customErrorsCheckRectifySuccessMessage",
+ new[] { Values.First(v => v.IsRecommended).Value });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs
new file mode 100644
index 0000000000..0fe37e11e9
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ [HealthCheck("D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", "Macro errors",
+ Description = "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.",
+ Group = "Configuration")]
+ public class MacroErrorsCheck : AbstractConfigCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public MacroErrorsCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ public override string FilePath
+ {
+ get { return "~/Config/umbracoSettings.config"; }
+ }
+
+ public override string XPath
+ {
+ get { return "/settings/content/MacroErrors"; }
+ }
+
+ public override ValueComparisonType ValueComparisonType
+ {
+ get { return ValueComparisonType.ShouldEqual; }
+ }
+
+ public override IEnumerable Values
+ {
+ get
+ {
+ var values = new List
+ {
+ new AcceptableConfiguration
+ {
+ IsRecommended = true,
+ Value = "inline"
+ },
+ new AcceptableConfiguration
+ {
+ IsRecommended = false,
+ Value = "silent"
+ }
+ };
+
+ return values;
+ }
+ }
+
+ public override string CheckSuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/macroErrorModeCheckSuccessMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value });
+ }
+ }
+
+ public override string CheckErrorMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/macroErrorModeCheckErrorMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value });
+ }
+ }
+
+ public override string RectifySuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/macroErrorModeCheckRectifySuccessMessage",
+ new[] { Values.First(v => v.IsRecommended).Value });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs
new file mode 100644
index 0000000000..bcdb0ebc24
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ [HealthCheck("3E2F7B14-4B41-452B-9A30-E67FBC8E1206", "Notification Email Settings",
+ Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.",
+ Group = "Configuration")]
+ public class NotificationEmailCheck : AbstractConfigCheck
+ {
+ private readonly ILocalizedTextService _textService;
+ private const string DefaultFromEmail = "your@email.here";
+
+ public NotificationEmailCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ public override string FilePath
+ {
+ get { return "~/Config/umbracoSettings.config"; }
+ }
+
+ public override string XPath
+ {
+ get { return "/settings/content/notifications/email"; }
+ }
+
+ public override ValueComparisonType ValueComparisonType
+ {
+ get { return ValueComparisonType.ShouldNotEqual; }
+ }
+
+ public override IEnumerable Values
+ {
+ get
+ {
+ return new List
+ {
+ new AcceptableConfiguration { IsRecommended = false, Value = DefaultFromEmail }
+ };
+ }
+ }
+
+ public override string CheckSuccessMessage
+ {
+ get { return _textService.Localize("healthcheck/notificationEmailsCheckSuccessMessage", new [] { CurrentValue } ); }
+ }
+
+ public override string CheckErrorMessage
+ {
+ get { return _textService.Localize("healthcheck/notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs
new file mode 100644
index 0000000000..d0e38815da
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ [HealthCheck("9BED6EF4-A7F3-457A-8935-B64E9AA8BAB3", "Trace Mode",
+ Description = "Leaving trace mode enabled can make valuable information about your system available to hackers.",
+ Group = "Live Environment")]
+ public class TraceCheck : AbstractConfigCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public TraceCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ public override string FilePath
+ {
+ get { return "~/Web.config"; }
+ }
+
+ public override string XPath
+ {
+ get { return "/configuration/system.web/trace/@enabled"; }
+ }
+
+ public override ValueComparisonType ValueComparisonType
+ {
+ get { return ValueComparisonType.ShouldEqual; }
+ }
+
+ public override IEnumerable Values
+ {
+ get
+ {
+ return new List
+ {
+ new AcceptableConfiguration { IsRecommended = true, Value = bool.FalseString.ToLower() }
+ };
+ }
+ }
+
+ public override string CheckSuccessMessage
+ {
+ get { return _textService.Localize("healthcheck/traceModeCheckSuccessMessage"); }
+ }
+
+ public override string CheckErrorMessage
+ {
+ get { return _textService.Localize("healthcheck/traceModeCheckErrorMessage"); }
+ }
+
+ public override string RectifySuccessMessage
+ {
+ get { return _textService.Localize("healthcheck/traceModeCheckRectifySuccessMessage"); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs
new file mode 100644
index 0000000000..654d7dd209
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ [HealthCheck("046A066C-4FB2-4937-B931-069964E16C66", "Try Skip IIS Custom Errors",
+ Description = "Starting with IIS 7.5, this must be set to true for Umbraco 404 pages to show. Otherwise, IIS will takeover and render its built-in error page.",
+ Group = "Configuration")]
+ public class TrySkipIisCustomErrorsCheck : AbstractConfigCheck
+ {
+ private readonly Version _serverVersion = HttpRuntime.IISVersion;
+ private readonly ILocalizedTextService _textService;
+
+ public TrySkipIisCustomErrorsCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ public override string FilePath
+ {
+ get { return "~/Config/umbracoSettings.config"; }
+ }
+
+ public override string XPath
+ {
+ get { return "/settings/web.routing/@trySkipIisCustomErrors"; }
+ }
+
+ public override ValueComparisonType ValueComparisonType
+ {
+ get { return ValueComparisonType.ShouldEqual; }
+ }
+
+ public override IEnumerable Values
+ {
+ get
+ {
+ var recommendedValue = _serverVersion >= new Version("7.5.0")
+ ? bool.TrueString.ToLower()
+ : bool.FalseString.ToLower();
+ return new List { new AcceptableConfiguration { IsRecommended = true, Value = recommendedValue } };
+ }
+ }
+
+ public override string CheckSuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckSuccessMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() });
+ }
+ }
+
+ public override string CheckErrorMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckErrorMessage",
+ new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() });
+ }
+ }
+
+ public override string RectifySuccessMessage
+ {
+ get
+ {
+ return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckRectifySuccessMessage",
+ new[] { Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/ValueComparisonType.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/ValueComparisonType.cs
new file mode 100644
index 0000000000..88c8a94a09
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Config/ValueComparisonType.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Web.HealthCheck.Checks.Config
+{
+ public enum ValueComparisonType
+ {
+ ShouldEqual,
+ ShouldNotEqual,
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/XmlDataIntegrityHealthCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/XmlDataIntegrityHealthCheck.cs
new file mode 100644
index 0000000000..5ba16305de
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/XmlDataIntegrityHealthCheck.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Core;
+using Umbraco.Core.Models.Rdbms;
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.SqlSyntax;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.DataIntegrity
+{
+ ///
+ /// This moves the functionality from the XmlIntegrity check dashboard into a health check
+ ///
+ [HealthCheck(
+ "D999EB2B-64C2-400F-B50C-334D41F8589A",
+ "XML Data Integrity",
+ Description = "Checks the integrity of the XML data in Umbraco",
+ Group = "Data Integrity")]
+ public class XmlDataIntegrityHealthCheck : HealthCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ private const string CheckContentXmlTableAction = "checkContentXmlTable";
+ private const string CheckMediaXmlTableAction = "checkMediaXmlTable";
+ private const string CheckMembersXmlTableAction = "checkMembersXmlTable";
+
+ public XmlDataIntegrityHealthCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _sqlSyntax = HealthCheckContext.ApplicationContext.DatabaseContext.SqlSyntax;
+ _services = HealthCheckContext.ApplicationContext.Services;
+ _database = HealthCheckContext.ApplicationContext.DatabaseContext.Database;
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ private readonly ISqlSyntaxProvider _sqlSyntax;
+ private readonly ServiceContext _services;
+ private readonly UmbracoDatabase _database;
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public override IEnumerable GetStatus()
+ {
+ //return the statuses
+ return new[] { CheckContent(), CheckMedia(), CheckMembers() };
+ }
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ switch (action.Alias)
+ {
+ case CheckContentXmlTableAction:
+ _services.ContentService.RebuildXmlStructures();
+ return CheckContent();
+ case CheckMediaXmlTableAction:
+ _services.MediaService.RebuildXmlStructures();
+ return CheckMedia();
+ case CheckMembersXmlTableAction:
+ _services.MemberService.RebuildXmlStructures();
+ return CheckMembers();
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private HealthCheckStatus CheckMembers()
+ {
+ var total = _services.MemberService.Count();
+ var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member);
+ var subQuery = new Sql()
+ .Select("Count(*)")
+ .From(_sqlSyntax)
+ .InnerJoin(_sqlSyntax)
+ .On(_sqlSyntax, left => left.NodeId, right => right.NodeId)
+ .Where(dto => dto.NodeObjectType == memberObjectType);
+ var totalXml = _database.ExecuteScalar(subQuery);
+
+ var actions = new List();
+ if (totalXml != total)
+ actions.Add(new HealthCheckAction(CheckMembersXmlTableAction, Id));
+
+ return new HealthCheckStatus(_textService.Localize("healthcheck/xmlDataIntegrityCheckMembers", new[] { totalXml.ToString(), total.ToString() }))
+ {
+ ResultType = totalXml == total ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+
+ private HealthCheckStatus CheckMedia()
+ {
+ var total = _services.MediaService.Count();
+ var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media);
+ var subQuery = new Sql()
+ .Select("Count(*)")
+ .From(_sqlSyntax)
+ .InnerJoin(_sqlSyntax)
+ .On(_sqlSyntax, left => left.NodeId, right => right.NodeId)
+ .Where(dto => dto.NodeObjectType == mediaObjectType);
+ var totalXml = _database.ExecuteScalar(subQuery);
+
+ var actions = new List();
+ if (totalXml != total)
+ actions.Add(new HealthCheckAction(CheckMediaXmlTableAction, Id));
+
+ return new HealthCheckStatus(_textService.Localize("healthcheck/xmlDataIntegrityCheckMedia", new[] { totalXml.ToString(), total.ToString() }))
+ {
+ ResultType = totalXml == total ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+
+ private HealthCheckStatus CheckContent()
+ {
+ var total = _services.ContentService.CountPublished();
+ var subQuery = new Sql()
+ .Select("DISTINCT cmsContentXml.nodeId")
+ .From(_sqlSyntax)
+ .InnerJoin(_sqlSyntax)
+ .On(_sqlSyntax, left => left.NodeId, right => right.NodeId);
+ var totalXml = _database.ExecuteScalar("SELECT COUNT(*) FROM (" + subQuery.SQL + ") as tmp");
+
+ var actions = new List();
+ if (totalXml != total)
+ actions.Add(new HealthCheckAction(CheckContentXmlTableAction, Id));
+
+ return new HealthCheckStatus(_textService.Localize("healthcheck/xmlDataIntegrityCheckContent", new[] { totalXml.ToString(), total.ToString() }))
+ {
+ ResultType = totalXml == total ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs
new file mode 100644
index 0000000000..36e774287b
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Core.IO;
+using Umbraco.Core.Services;
+using Umbraco.Web.Install;
+
+namespace Umbraco.Web.HealthCheck.Checks.Permissions
+{
+ internal enum PermissionCheckRequirement
+ {
+ Required,
+ Optional
+ }
+
+ internal enum PermissionCheckFor
+ {
+ Folder,
+ File
+ }
+
+ [HealthCheck(
+ "53DBA282-4A79-4B67-B958-B29EC40FCC23",
+ "Folder & File Permissions",
+ Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.",
+ Group = "Permissions")]
+ public class FolderAndFilePermissionsCheck : HealthCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public FolderAndFilePermissionsCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public override IEnumerable GetStatus()
+ {
+ //return the statuses
+ return new[] { CheckFolderPermissions(), CheckFilePermissions() };
+ }
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions");
+ }
+
+ private HealthCheckStatus CheckFolderPermissions()
+ {
+ // Create lists of paths to check along with a flag indicating if modify rights are required
+ // in ALL circumstances or just some
+ var pathsToCheck = new Dictionary
+ {
+ { SystemDirectories.AppCode, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Data, PermissionCheckRequirement.Required },
+ { SystemDirectories.Packages, PermissionCheckRequirement.Required},
+ { SystemDirectories.Preview, PermissionCheckRequirement.Required },
+ { SystemDirectories.AppPlugins, PermissionCheckRequirement.Required },
+ { SystemDirectories.Bin, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Config, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Css, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Masterpages, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Media, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Scripts, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Umbraco, PermissionCheckRequirement.Optional },
+ { SystemDirectories.UmbracoClient, PermissionCheckRequirement.Optional },
+ { SystemDirectories.UserControls, PermissionCheckRequirement.Optional },
+ { SystemDirectories.MvcViews, PermissionCheckRequirement.Optional },
+ { SystemDirectories.Xslt, PermissionCheckRequirement.Optional },
+ };
+
+ // Run checks for required and optional paths for modify permission
+ List requiredFailedPaths;
+ List optionalFailedPaths;
+ var requiredPathCheckResult = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out requiredFailedPaths);
+ var optionalPathCheckResult = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out optionalFailedPaths);
+
+ return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.Folder);
+ }
+
+ private HealthCheckStatus CheckFilePermissions()
+ {
+ // Create lists of paths to check along with a flag indicating if modify rights are required
+ // in ALL circumstances or just some
+ var pathsToCheck = new Dictionary
+ {
+ { "~/Web.config", PermissionCheckRequirement.Optional },
+ };
+
+ // Run checks for required and optional paths for modify permission
+ List requiredFailedPaths;
+ List optionalFailedPaths;
+ var requiredPathCheckResult = FilePermissionHelper.TestFiles(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out requiredFailedPaths);
+ var optionalPathCheckResult = FilePermissionHelper.TestFiles(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out optionalFailedPaths);
+
+ return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.File);
+ }
+
+ private static string[] GetPathsToCheck(Dictionary pathsToCheck,
+ PermissionCheckRequirement requirement)
+ {
+ return pathsToCheck
+ .Where(x => x.Value == requirement)
+ .Select(x => IOHelper.MapPath(x.Key))
+ .OrderBy(x => x)
+ .ToArray();
+ }
+
+ private HealthCheckStatus GetStatus(bool requiredPathCheckResult, List requiredFailedPaths,
+ bool optionalPathCheckResult, IEnumerable optionalFailedPaths,
+ PermissionCheckFor checkingFor)
+ {
+ // Return error if any required parths fail the check, or warning if any optional ones do
+ var resultType = StatusResultType.Success;
+ var messageKey = string.Format("healthcheck/{0}PermissionsCheckMessage",
+ checkingFor == PermissionCheckFor.Folder ? "folder" : "file");
+ var message = _textService.Localize(messageKey);
+ if (requiredPathCheckResult == false)
+ {
+ resultType = StatusResultType.Error;
+ messageKey = string.Format("healthcheck/required{0}PermissionFailed",
+ checkingFor == PermissionCheckFor.Folder ? "Folder" : "File");
+ message = GetMessageForPathCheckFailure(messageKey, requiredFailedPaths);
+ }
+ else if (optionalPathCheckResult == false)
+ {
+ resultType = StatusResultType.Warning;
+ messageKey = string.Format("healthcheck/optional{0}PermissionFailed",
+ checkingFor == PermissionCheckFor.Folder ? "Folder" : "File");
+ message = GetMessageForPathCheckFailure(messageKey, optionalFailedPaths);
+ }
+
+ var actions = new List();
+ return
+ new HealthCheckStatus(message)
+ {
+ ResultType = resultType,
+ Actions = actions
+ };
+ }
+
+ private string GetMessageForPathCheckFailure(string messageKey, IEnumerable failedPaths)
+ {
+ var rootFolder = IOHelper.MapPath("/");
+ var failedFolders = failedPaths
+ .Select(x => ParseFolderFromFullPath(rootFolder, x));
+ return _textService.Localize(messageKey,
+ new[] { string.Join(", ", failedFolders) });
+ }
+
+ private string ParseFolderFromFullPath(string rootFolder, string filePath)
+ {
+ return filePath.Replace(rootFolder, string.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs
new file mode 100644
index 0000000000..beaeee8b18
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ClickJackingCheck.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using Umbraco.Core.IO;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Security
+{
+ [HealthCheck(
+ "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0",
+ "Click-Jacking Protection",
+ Description = "Checks if your site is allowed to be IFRAMed by another site and thus would be susceptible to click-jacking.",
+ Group = "Security")]
+ public class ClickJackingCheck : HealthCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ private const string SetFrameOptionsHeaderInConfigActiobn = "setFrameOptionsHeaderInConfig";
+
+ private const string XFrameOptionsHeader = "X-Frame-Options";
+
+ public ClickJackingCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public override IEnumerable GetStatus()
+ {
+ //return the statuses
+ return new[] { CheckForFrameOptionsHeader() };
+ }
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ switch (action.Alias)
+ {
+ case SetFrameOptionsHeaderInConfigActiobn:
+ return SetFrameOptionsHeaderInConfig();
+ default:
+ throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist");
+ }
+ }
+
+ private HealthCheckStatus CheckForFrameOptionsHeader()
+ {
+ var message = string.Empty;
+ var success = false;
+ var url = HealthCheckContext.HttpContext.Request.Url;
+
+ // Access the site home page and check for the click-jack protection header or meta tag
+ var address = string.Format("http://{0}:{1}", url.Host.ToLower(), url.Port);
+ var request = WebRequest.Create(address);
+ request.Method = "GET";
+ try
+ {
+ var response = request.GetResponse();
+
+ // Check first for header
+ success = DoHeadersContainFrameOptions(response);
+
+ // If not found, check for meta-tag
+ if (success == false)
+ {
+ success = DoMetaTagsContainFrameOptions(response);
+ }
+
+ message = success
+ ? _textService.Localize("healthcheck/clickJackingCheckHeaderFound")
+ : _textService.Localize("healthcheck/clickJackingCheckHeaderNotFound");
+ }
+ catch (Exception ex)
+ {
+ message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { address, ex.Message });
+ }
+
+ var actions = new List();
+ if (success == false)
+ {
+ actions.Add(new HealthCheckAction(SetFrameOptionsHeaderInConfigActiobn, Id)
+ {
+ Name = _textService.Localize("healthcheck/clickJackingSetHeaderInConfig"),
+ Description = _textService.Localize("healthcheck/clickJackingSetHeaderInConfigDescription")
+ });
+ }
+
+ return
+ new HealthCheckStatus(message)
+ {
+ ResultType = success ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+
+ private static bool DoHeadersContainFrameOptions(WebResponse response)
+ {
+ return response.Headers.AllKeys.Contains(XFrameOptionsHeader);
+ }
+
+ private static bool DoMetaTagsContainFrameOptions(WebResponse response)
+ {
+ using (var stream = response.GetResponseStream())
+ {
+ if (stream == null) return false;
+ using (var reader = new StreamReader(stream))
+ {
+ var html = reader.ReadToEnd();
+ var metaTags = ParseMetaTags(html);
+ return metaTags.ContainsKey(XFrameOptionsHeader);
+ }
+ }
+ }
+
+ private static Dictionary ParseMetaTags(string html)
+ {
+ var regex = new Regex("");
+
+ return regex.Matches(html)
+ .Cast()
+ .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value);
+ }
+
+ private HealthCheckStatus SetFrameOptionsHeaderInConfig()
+ {
+ var errorMessage = string.Empty;
+ var success = SaveHeaderToConfigFile(out errorMessage);
+
+ if (success)
+ {
+ return
+ new HealthCheckStatus(_textService.Localize("healthcheck/clickJackingSetHeaderInConfigSuccess"))
+ {
+ ResultType = StatusResultType.Success
+ };
+ }
+
+ return
+ new HealthCheckStatus(_textService.Localize("healthcheck/clickJackingSetHeaderInConfigError", new [] { errorMessage }))
+ {
+ ResultType = StatusResultType.Error
+ };
+ }
+
+ private static bool SaveHeaderToConfigFile(out string errorMessage)
+ {
+ try
+ {
+ // There don't look to be any useful classes defined in https://msdn.microsoft.com/en-us/library/system.web.configuration(v=vs.110).aspx
+ // for working with the customHeaders section, so working with the XML directly.
+ var configFile = IOHelper.MapPath("~/Web.config");
+ var doc = XDocument.Load(configFile);
+ var systemWebServerElement = doc.XPathSelectElement("/configuration/system.webServer");
+ var httpProtocolElement = systemWebServerElement.Element("httpProtocol");
+ if (httpProtocolElement == null)
+ {
+ httpProtocolElement = new XElement("httpProtocol");
+ systemWebServerElement.Add(httpProtocolElement);
+ }
+
+ var customHeadersElement = httpProtocolElement.Element("customHeaders");
+ if (customHeadersElement == null)
+ {
+ customHeadersElement = new XElement("customHeaders");
+ httpProtocolElement.Add(customHeadersElement);
+ }
+
+ var removeHeaderElement = customHeadersElement.Elements("remove")
+ .SingleOrDefault(x => x.Attribute("name") != null &&
+ x.Attribute("name").Value == XFrameOptionsHeader);
+ if (removeHeaderElement == null)
+ {
+ removeHeaderElement = new XElement("remove");
+ removeHeaderElement.Add(new XAttribute("name", XFrameOptionsHeader));
+ customHeadersElement.Add(removeHeaderElement);
+ }
+
+ var addHeaderElement = customHeadersElement.Elements("add")
+ .SingleOrDefault(x => x.Attribute("name") != null &&
+ x.Attribute("name").Value == XFrameOptionsHeader);
+ if (addHeaderElement == null)
+ {
+ addHeaderElement = new XElement("add");
+ addHeaderElement.Add(new XAttribute("name", XFrameOptionsHeader));
+ addHeaderElement.Add(new XAttribute("value", "deny"));
+ customHeadersElement.Add(addHeaderElement);
+ }
+
+ doc.Save(configFile);
+
+ errorMessage = string.Empty;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs
new file mode 100644
index 0000000000..af1b15818a
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Web;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Security
+{
+ [HealthCheck(
+ "92ABBAA2-0586-4089-8AE2-9A843439D577",
+ "Excessive Headers",
+ Description = "Checks to see if your site is revealing information in it's headers that gives away unnecessary details about the technology used to build and host it.",
+ Group = "Security")]
+ public class ExcessiveHeadersCheck : HealthCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public ExcessiveHeadersCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public override IEnumerable GetStatus()
+ {
+ //return the statuses
+ return new[] { CheckForHeaders() };
+ }
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions");
+ }
+
+ private HealthCheckStatus CheckForHeaders()
+ {
+ var message = string.Empty;
+ var success = false;
+ var url = HealthCheckContext.HttpContext.Request.Url;
+
+ // Access the site home page and check for the headers
+ var address = string.Format("http://{0}:{1}", url.Host.ToLower(), url.Port);
+ var request = WebRequest.Create(address);
+ request.Method = "HEAD";
+ try
+ {
+ var response = request.GetResponse();
+ var allHeaders = response.Headers.AllKeys;
+ var headersToCheckFor = new [] {"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version"};
+ var headersFound = allHeaders
+ .Intersect(headersToCheckFor)
+ .ToArray();
+ success = headersFound.Any() == false;
+ message = success
+ ? _textService.Localize("healthcheck/excessiveHeadersNotFound")
+ : _textService.Localize("healthcheck/excessiveHeadersFound", new [] { string.Join(", ", headersFound) });
+ }
+ catch (Exception ex)
+ {
+ message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { address, ex.Message });
+ }
+
+ var actions = new List();
+ return
+ new HealthCheckStatus(message)
+ {
+ ResultType = success ? StatusResultType.Success : StatusResultType.Warning,
+ Actions = actions
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs
new file mode 100644
index 0000000000..80853c01d8
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Web;
+using Umbraco.Core.IO;
+using Umbraco.Core.Services;
+using Umbraco.Web.HealthCheck.Checks.Config;
+
+namespace Umbraco.Web.HealthCheck.Checks.Security
+{
+ [HealthCheck(
+ "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7",
+ "HTTPS Configuration",
+ Description = "Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.",
+ Group = "Security")]
+ public class HttpsCheck : HealthCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ private const string FixHttpsSettingAction = "fixHttpsSetting";
+
+ public HttpsCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public override IEnumerable GetStatus()
+ {
+ //return the statuses
+ return new[] { CheckIfCurrentSchemeIsHttps(), CheckHttpsConfigurationSetting(), CheckForValidCertificate() };
+ }
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ switch (action.Alias)
+ {
+ case FixHttpsSettingAction:
+ return FixHttpsSetting();
+ default:
+ throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist");
+ }
+ }
+
+ private HealthCheckStatus CheckForValidCertificate()
+ {
+ var message = string.Empty;
+ var success = false;
+ var url = HealthCheckContext.HttpContext.Request.Url;
+
+ // Attempt to access the site over HTTPS to see if it HTTPS is supported
+ // and a valid certificate has been configured
+ var address = string.Format("https://{0}:{1}", url.Host.ToLower(), url.Port);
+ var request = (HttpWebRequest)WebRequest.Create(address);
+ request.Method = "HEAD";
+
+ try
+ {
+ var response = (HttpWebResponse)request.GetResponse();
+ success = response.StatusCode == HttpStatusCode.OK;
+ }
+ catch (Exception ex)
+ {
+ var exception = ex as WebException;
+ if (exception != null)
+ {
+ message = exception.Status == WebExceptionStatus.TrustFailure
+ ? _textService.Localize("healthcheck/httpsCheckInvalidCertificate", new [] { exception.Message })
+ : _textService.Localize("healthcheck/httpsCheckInvalidUrl", new [] { address, exception.Message });
+ }
+ else
+ {
+ message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { address, ex.Message });
+ }
+ }
+
+ var actions = new List();
+
+ return
+ new HealthCheckStatus(message)
+ {
+ ResultType = success ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+
+ private HealthCheckStatus CheckIfCurrentSchemeIsHttps()
+ {
+ var uri = HttpContext.Current.Request.Url;
+ var success = uri.Scheme == "https";
+
+ var actions = new List();
+
+ return
+ new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" }))
+ {
+ ResultType = success ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+
+ private HealthCheckStatus CheckHttpsConfigurationSetting()
+ {
+ var httpsSettingEnabled = Core.Configuration.GlobalSettings.UseSSL;
+ var uri = HttpContext.Current.Request.Url;
+ var actions = new List();
+
+ string resultMessage;
+ StatusResultType resultType;
+ if (uri.Scheme != "https")
+ {
+ resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationRectifyNotPossible");
+ resultType = StatusResultType.Info;
+ }
+ else
+ {
+ if (httpsSettingEnabled == false)
+ actions.Add(new HealthCheckAction(FixHttpsSettingAction, Id)
+ {
+ Name = _textService.Localize("healthcheck/httpsCheckEnableHttpsButton"),
+ Description = _textService.Localize("healthcheck/httpsCheckEnableHttpsDescription")
+ });
+
+ resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationCheckResult",
+ new[] {httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not"});
+ resultType = httpsSettingEnabled ? StatusResultType.Success: StatusResultType.Error;
+ }
+
+ return
+ new HealthCheckStatus(resultMessage)
+ {
+ ResultType = resultType,
+ Actions = actions
+ };
+ }
+
+ private HealthCheckStatus FixHttpsSetting()
+ {
+ var configFile = IOHelper.MapPath("~/Web.config");
+ const string xPath = "/configuration/appSettings/add[@key='umbracoUseSSL']/@value";
+ var configurationService = new ConfigurationService(configFile, xPath);
+ var updateConfigFile = configurationService.UpdateConfigFile("true");
+
+ if (updateConfigFile.Success)
+ {
+ return
+ new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckEnableHttpsSuccess"))
+ {
+ ResultType = StatusResultType.Success
+ };
+ }
+
+ return
+ new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckEnableHttpsError", new [] { updateConfigFile.Result }))
+ {
+ ResultType = StatusResultType.Error
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs
new file mode 100644
index 0000000000..a1f085865c
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Configuration;
+using System.Net.Sockets;
+using System.Web.Configuration;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck.Checks.Services
+{
+ [HealthCheck(
+ "1B5D221B-CE99-4193-97CB-5F3261EC73DF",
+ "SMTP Settings",
+ Description = "Checks that valid settings for sending emails are in place.",
+ Group = "Services")]
+ public class SmtpCheck : HealthCheck
+ {
+ private readonly ILocalizedTextService _textService;
+
+ public SmtpCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext)
+ {
+ _textService = healthCheckContext.ApplicationContext.Services.TextService;
+ }
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public override IEnumerable GetStatus()
+ {
+ //return the statuses
+ return new[] { CheckSmtpSettings() };
+ }
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ throw new InvalidOperationException("SmtpCheck has no executable actions");
+ }
+
+ private HealthCheckStatus CheckSmtpSettings()
+ {
+ const int DefaultSmtpPort = 25;
+ var message = string.Empty;
+ var success = false;
+
+ var config = WebConfigurationManager.OpenWebConfiguration(HealthCheckContext.HttpContext.Request.ApplicationPath);
+ var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings");
+ if (settings == null)
+ {
+ message = _textService.Localize("healthcheck/smtpMailSettingsNotFound");
+ }
+ else
+ {
+ var host = settings.Smtp.Network.Host;
+ var port = settings.Smtp.Network.Port == 0 ? DefaultSmtpPort : settings.Smtp.Network.Port;
+ if (string.IsNullOrEmpty(host))
+ {
+ message = _textService.Localize("healthcheck/smtpMailSettingsHostNotConfigured");
+ }
+ else
+ {
+ success = CanMakeSmtpConnection(host, port);
+ message = success
+ ? _textService.Localize("healthcheck/smtpMailSettingsConnectionSuccess")
+ : _textService.Localize("healthcheck/smtpMailSettingsConnectionFail", new [] { host, port.ToString() });
+ }
+ }
+
+ var actions = new List();
+ return
+ new HealthCheckStatus(message)
+ {
+ ResultType = success ? StatusResultType.Success : StatusResultType.Error,
+ Actions = actions
+ };
+ }
+
+ private bool CanMakeSmtpConnection(string host, int port)
+ {
+ try
+ {
+ using (var client = new TcpClient())
+ {
+ client.Connect(host, port);
+ using (var stream = client.GetStream())
+ {
+ using (var writer = new StreamWriter(stream))
+ using (var reader = new StreamReader(stream))
+ {
+ writer.WriteLine("EHLO " + host);
+ writer.Flush();
+ reader.ReadLine();
+ return true;
+ }
+ }
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheck.cs b/src/Umbraco.Web/HealthCheck/HealthCheck.cs
new file mode 100644
index 0000000000..ec1358947a
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheck.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Umbraco.Core;
+
+namespace Umbraco.Web.HealthCheck
+{
+ ///
+ /// The abstract health check class
+ ///
+ [DataContract(Name = "healtCheck", Namespace = "")]
+ public abstract class HealthCheck
+ {
+ protected HealthCheck(HealthCheckContext healthCheckContext)
+ {
+ HealthCheckContext = healthCheckContext;
+ //Fill in the metadata
+ var thisType = this.GetType();
+ var meta = thisType.GetCustomAttribute(false);
+ if (meta == null)
+ throw new InvalidOperationException(
+ string.Format("The health check {0} requires a {1}", thisType, typeof(HealthCheckAttribute)));
+ Name = meta.Name;
+ Description = meta.Description;
+ Group = meta.Group;
+ Id = meta.Id;
+ }
+
+ [IgnoreDataMember]
+ public HealthCheckContext HealthCheckContext { get; private set; }
+
+ [DataMember(Name = "id")]
+ public Guid Id { get; private set; }
+
+ [DataMember(Name = "name")]
+ public string Name { get; private set; }
+
+ [DataMember(Name = "description")]
+ public string Description { get; private set; }
+
+ [DataMember(Name = "group")]
+ public string Group { get; private set; }
+
+ ///
+ /// Get the status for this health check
+ ///
+ ///
+ public abstract IEnumerable GetStatus();
+
+ ///
+ /// Executes the action and returns it's status
+ ///
+ ///
+ ///
+ public abstract HealthCheckStatus ExecuteAction(HealthCheckAction action);
+
+ //TODO: What else?
+
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckAction.cs b/src/Umbraco.Web/HealthCheck/HealthCheckAction.cs
new file mode 100644
index 0000000000..526ca0ece3
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckAction.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.HealthCheck
+{
+ [DataContract(Name = "healtCheckAction", Namespace = "")]
+ public class HealthCheckAction
+ {
+ ///
+ /// Empty ctor used for serialization
+ ///
+ public HealthCheckAction() { }
+
+ ///
+ /// Default ctor
+ ///
+ ///
+ ///
+ public HealthCheckAction(string alias, Guid healthCheckId)
+ {
+ Alias = alias;
+ HealthCheckId = healthCheckId;
+ }
+
+ ///
+ /// The alias of the action - this is used by the Health Check instance to execute the action
+ ///
+ [DataMember(Name = "alias")]
+ public string Alias { get; set; }
+
+ ///
+ /// The Id of the Health Check instance
+ ///
+ ///
+ /// This is used to find the Health Check instance to execute this action
+ ///
+ [DataMember(Name = "healthCheckId")]
+ public Guid HealthCheckId { get; set; }
+
+ ///
+ /// This could be used if the status has a custom view that specifies some parameters to be sent to the server
+ /// when an action needs to be executed
+ ///
+ [DataMember(Name = "actionParameters")]
+ public Dictionary ActionParameters { get; set; }
+
+
+ ///
+ /// The name of the action - this is used to name the fix button
+ ///
+ [DataMember(Name = "name")]
+ private string _name = UmbracoContext.Current.Application.Services.TextService.Localize("healthcheck/rectifyButton");
+ public string Name
+ {
+ get { return _name; }
+ set { _name = value; }
+ }
+
+ ///
+ /// The description of the action - this is used to give a description before executing the action
+ ///
+ [DataMember(Name = "description")]
+ public string Description { get; set; }
+
+ ///
+ /// Indicates if a value is required to rectify the issue
+ ///
+ [DataMember(Name = "valueRequired")]
+ public bool ValueRequired { get; set; }
+
+ ///
+ /// Provides a value to rectify the issue
+ ///
+ [DataMember(Name = "providedValue")]
+ public string ProvidedValue { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckAttribute.cs b/src/Umbraco.Web/HealthCheck/HealthCheckAttribute.cs
new file mode 100644
index 0000000000..885af8b3ba
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckAttribute.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Umbraco.Web.HealthCheck
+{
+ ///
+ /// Metadata attribute for Health checks
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public sealed class HealthCheckAttribute : Attribute
+ {
+ public HealthCheckAttribute(string id, string name)
+ {
+ Id = new Guid(id);
+ Name = name;
+ }
+
+ public string Name { get; private set; }
+ public string Description { get; set; }
+
+ public string Group { get; set; }
+
+ public Guid Id { get; private set; }
+
+ //TODO: Do we need more metadata?
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckContext.cs b/src/Umbraco.Web/HealthCheck/HealthCheckContext.cs
new file mode 100644
index 0000000000..4ec02a2386
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckContext.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Web;
+using Umbraco.Core;
+
+namespace Umbraco.Web.HealthCheck
+{
+ ///
+ /// Context exposing all services that could be required for health check classes to perform and/or fix their checks
+ ///
+ public class HealthCheckContext
+ {
+ public HealthCheckContext(HttpContextBase httpContext, UmbracoContext umbracoContext)
+ {
+ if (httpContext == null) throw new ArgumentNullException("httpContext");
+ if (umbracoContext == null) throw new ArgumentNullException("umbracoContext");
+ HttpContext = httpContext;
+ UmbracoContext = umbracoContext;
+ ApplicationContext = UmbracoContext.Application;
+ }
+
+ public HttpContextBase HttpContext { get; private set; }
+ public UmbracoContext UmbracoContext { get; private set; }
+ public ApplicationContext ApplicationContext { get; private set; }
+
+ //TODO: Do we need any more info/service exposed here?
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs
new file mode 100644
index 0000000000..919df88962
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http;
+using Umbraco.Web.Editors;
+
+namespace Umbraco.Web.HealthCheck
+{
+ ///
+ /// The API controller used to display the health check info and execute any actions
+ ///
+ public class HealthCheckController : UmbracoAuthorizedJsonController
+ {
+ private readonly IHealthCheckResolver _healthCheckResolver;
+
+ public HealthCheckController()
+ {
+ _healthCheckResolver = HealthCheckResolver.Current;
+ }
+
+ public HealthCheckController(IHealthCheckResolver healthCheckResolver)
+ {
+ _healthCheckResolver = healthCheckResolver;
+ }
+
+ ///
+ /// Gets a grouped list of health checks, but doesn't actively check the status of each health check.
+ ///
+ /// Returns a collection of anonymous objects representing each group.
+ public object GetAllHealthChecks()
+ {
+ var groups = _healthCheckResolver.HealthChecks
+ .GroupBy(x => x.Group)
+ .OrderBy(x => x.Key);
+ var healthCheckGroups = new List();
+ foreach (var healthCheckGroup in groups)
+ {
+ var hcGroup = new HealthCheckGroup
+ {
+ Name = healthCheckGroup.Key,
+ Checks = healthCheckGroup
+ .OrderBy(x => x.Name)
+ .ToList()
+ };
+ healthCheckGroups.Add(hcGroup);
+ }
+
+ return healthCheckGroups;
+ }
+
+ public object GetStatus(Guid id)
+ {
+ var check = _healthCheckResolver.HealthChecks.FirstOrDefault(x => x.Id == id);
+ if (check == null) throw new InvalidOperationException("No health check found with ID " + id);
+
+ return check.GetStatus();
+ }
+
+ [HttpPost]
+ public HealthCheckStatus ExecuteAction(HealthCheckAction action)
+ {
+ var check = _healthCheckResolver.HealthChecks.FirstOrDefault(x => x.Id == action.HealthCheckId);
+ if (check == null) throw new InvalidOperationException("No health check found with id " + action.HealthCheckId);
+
+ return check.ExecuteAction(action);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckGroup.cs b/src/Umbraco.Web/HealthCheck/HealthCheckGroup.cs
new file mode 100644
index 0000000000..f01c65f854
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckGroup.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.HealthCheck
+{
+ [DataContract(Name = "healthCheckGroup", Namespace = "")]
+ public class HealthCheckGroup
+ {
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ [DataMember(Name = "checks")]
+ public List Checks { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckResolver.cs b/src/Umbraco.Web/HealthCheck/HealthCheckResolver.cs
new file mode 100644
index 0000000000..dfe5b792a5
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckResolver.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.ObjectResolution;
+using Umbraco.Core.Persistence.SqlSyntax;
+
+namespace Umbraco.Web.HealthCheck
+{
+ ///
+ /// Resolves all health check instances
+ ///
+ ///
+ /// Each instance scoped to the lifespan of the http request
+ ///
+ internal class HealthCheckResolver : LazyManyObjectsResolverBase, IHealthCheckResolver
+ {
+ public HealthCheckResolver(ILogger logger, Func> lazyTypeList)
+ : base(new HealthCheckServiceProvider(), logger, lazyTypeList, ObjectLifetimeScope.HttpRequest)
+ {
+ }
+
+ ///
+ /// Returns all health check instances
+ ///
+ public IEnumerable HealthChecks
+ {
+ get { return Values; }
+ }
+
+ ///
+ /// This will ctor the HealthCheck instances
+ ///
+ ///
+ /// This is like a super crappy DI - in v8 we have real DI
+ ///
+ private class HealthCheckServiceProvider : IServiceProvider
+ {
+ public object GetService(Type serviceType)
+ {
+ var normalArgs = new[] { typeof(HealthCheckContext) };
+ var found = serviceType.GetConstructor(normalArgs);
+ if (found != null)
+ {
+ return found.Invoke(new object[]
+ {
+ new HealthCheckContext(new HttpContextWrapper(HttpContext.Current), UmbracoContext.Current)
+ });
+ }
+
+ //use normal ctor
+ return Activator.CreateInstance(serviceType);
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckStatus.cs b/src/Umbraco.Web/HealthCheck/HealthCheckStatus.cs
new file mode 100644
index 0000000000..c966cadfa7
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/HealthCheckStatus.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.HealthCheck
+{
+ ///
+ /// The status returned for a health check when it performs it check
+ /// TODO: This model will be used in the WebApi result so needs attributes for JSON usage
+ ///
+ [DataContract(Name = "healtCheckStatus", Namespace = "")]
+ public class HealthCheckStatus
+ {
+ public HealthCheckStatus(string message)
+ {
+ Message = message;
+ Actions = Enumerable.Empty();
+ }
+
+ ///
+ /// The status message
+ ///
+ [DataMember(Name = "message")]
+ public string Message { get; private set; }
+
+ ///
+ /// The status description if one is necessary
+ ///
+ [DataMember(Name = "description")]
+ public string Description { get; set; }
+
+ ///
+ /// This is optional but would allow a developer to specify a path to an angular html view
+ /// in order to either show more advanced information and/or to provide input for the admin
+ /// to configure how an action is executed
+ ///
+ [DataMember(Name = "view")]
+ public string View { get; set; }
+
+ ///
+ /// The status type
+ ///
+ [DataMember(Name = "resultType")]
+ public StatusResultType ResultType { get; set; }
+
+ ///
+ /// The potential actions to take (in any)
+ ///
+ [DataMember(Name = "actions")]
+ public IEnumerable Actions { get; set; }
+
+ //TODO: What else?
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/IHealthCheckResolver.cs b/src/Umbraco.Web/HealthCheck/IHealthCheckResolver.cs
new file mode 100644
index 0000000000..795b61198e
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/IHealthCheckResolver.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Web.HealthCheck
+{
+ public interface IHealthCheckResolver
+ {
+ IEnumerable HealthChecks { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/HealthCheck/StatusResultType.cs b/src/Umbraco.Web/HealthCheck/StatusResultType.cs
new file mode 100644
index 0000000000..8e5fbb4cc9
--- /dev/null
+++ b/src/Umbraco.Web/HealthCheck/StatusResultType.cs
@@ -0,0 +1,10 @@
+namespace Umbraco.Web.HealthCheck
+{
+ public enum StatusResultType
+ {
+ Success,
+ Warning,
+ Error,
+ Info
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs
index e9835e0f32..87491b93a8 100644
--- a/src/Umbraco.Web/Install/FilePermissionHelper.cs
+++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs
@@ -62,7 +62,7 @@ namespace Umbraco.Web.Install
{
errorReport = new List();
bool succes = true;
- foreach (string file in PermissionFiles)
+ foreach (string file in files)
{
bool result = OpenFileForWrite(IOHelper.MapPath(file));
if (result == false)
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index de4b49bc77..3d0163a20f 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -319,6 +319,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs
index 93928d0867..6031e09393 100644
--- a/src/Umbraco.Web/WebBootManager.cs
+++ b/src/Umbraco.Web/WebBootManager.cs
@@ -47,6 +47,7 @@ using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Publishing;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
+using Umbraco.Web.HealthCheck;
using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings;
using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine;
@@ -533,6 +534,9 @@ namespace Umbraco.Web
CultureDictionaryFactoryResolver.Current = new CultureDictionaryFactoryResolver(
new DefaultCultureDictionaryFactory());
+
+ HealthCheckResolver.Current = new HealthCheckResolver(LoggerResolver.Current.Logger,
+ () => PluginManager.ResolveTypes());
}
///