diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 6c11cc9fc5..8a07ee380a 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -23,6 +23,7 @@ ECHO Change directory to %CD%\..\src\Umbraco.Web.UI.Client\ CD %CD%\..\src\Umbraco.Web.UI.Client\ ECHO Do npm install and the grunt build of Belle +call npm cache clean --quiet call npm install --quiet call npm install -g grunt-cli --quiet call npm install -g bower --quiet diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 98fdb01ff4..2dfad2d103 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -69,7 +69,16 @@ namespace Umbraco.Core.IO } } - public static string AppPlugins + public static string AppCode + { + get + { + //NOTE: this is not configurable and shouldn't need to be + return "~/App_Code"; + } + } + + public static string AppPlugins { get { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/healthcheck.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/healthcheck.resource.js new file mode 100644 index 0000000000..2c17016f05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/healthcheck.resource.js @@ -0,0 +1,76 @@ +/** + * @ngdoc service + * @name umbraco.resources.healthCheckResource + * @function + * + * @description + * Used by the health check dashboard to get checks and send requests to fix checks. + */ +(function () { + 'use strict'; + + function healthCheckResource($http, umbRequestHelper) { + + /** + * @ngdoc function + * @name umbraco.services.healthCheckService#getAllChecks + * @methodOf umbraco.services.healthCheckService + * @function + * + * @description + * Called to get all available health checks + */ + function getAllChecks() { + return umbRequestHelper.resourcePromise( + $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + "GetAllHealthChecks"), + "Failed to retrieve health checks" + ); + } + + /** + * @ngdoc function + * @name umbraco.services.healthCheckService#getStatus + * @methodOf umbraco.services.healthCheckService + * @function + * + * @description + * Called to get execute a health check and return the check status + */ + function getStatus(id) { + return umbRequestHelper.resourcePromise( + $http.get(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'GetStatus?id=' + id), + 'Failed to retrieve status for health check with ID ' + id + ); + } + + /** + * @ngdoc function + * @name umbraco.services.healthCheckService#executeAction + * @methodOf umbraco.services.healthCheckService + * @function + * + * @description + * Called to execute a health check action (rectifying an issue) + */ + function executeAction(action) { + return umbRequestHelper.resourcePromise( + $http.post(Umbraco.Sys.ServerVariables.umbracoUrls.healthCheckBaseUrl + 'ExecuteAction', action), + 'Failed to execute action with alias ' + action.alias + ' and healthCheckId + ' + action.healthCheckId + ); + } + + var resource = { + getAllChecks: getAllChecks, + getStatus: getStatus, + executeAction: executeAction + }; + + return resource; + + } + + + angular.module('umbraco.resources').factory('healthCheckResource', healthCheckResource); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 9d01cae4b9..27bceeeb58 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -126,3 +126,5 @@ @import "typeahead.less"; @import "hacks.less"; + +@import "healthcheck.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index b7d6015b76..8c1acd97d8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -13,8 +13,8 @@ box-shadow: 0 5px 0 rgba(0, 0, 0, 0.08), 0 1px 0 rgba(0, 0, 0, 0.16); transition: box-shadow 1s; top: 101px; /* height of header: 100px + its bottom-border: 1px */ - transform: translate(0, 50%); - + margin-top: 0; + margin-bottom: 0; } .umb-group-builder__property-preview .umb-editor-sub-header { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less index b2d03147a0..6443ec47e6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less @@ -7,6 +7,8 @@ left: 50%; transform: translate(-50%, -50%); font-size: 0; + margin-left: -6px; // hack to center it + margin-top: -6px; // hack to center it } .umb-load-indicator__bubble { diff --git a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less new file mode 100644 index 0000000000..a5f2d454f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less @@ -0,0 +1,333 @@ +/* Vars */ + @red-orange: #FF3F34; + @sunrise: #F5D226; + @emerald: #50C878; + + +.umb-healthcheck { + display: flex; + flex-wrap: wrap; +} + + +/* Group and states */ +.umb-healthcheck-group { + display: flex; + flex-wrap: wrap; + flex-direction: column; + + background: @grayLighter; + border-radius: 3px; + + padding: 15px 10px; + box-sizing: border-box; + + text-align: center; + border: 2px solid transparent; + height: 100%; +} + +.umb-healthcheck-group:hover { + border: 2px solid @blue; + cursor: pointer; +} + +.umb-healthcheck-group__load-container { + position: relative; + height: 30px; + margin-top: 15px; + margin-bottom: 16px; +} + + +/* Title */ +.umb-healthcheck-title { + margin-bottom: 15px; + font-size: 14px; + font-weight: bold; +} + + +/* Messages */ +.umb-healthcheck-messages { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.umb-healthcheck-message { + position: relative; + background: #fff; + border-radius: 50px; + display: flex; + align-items: center; + flex: 1 1 auto; + padding: 5px 10px; + margin-bottom: 5px; + color: #000; + font-weight: bold; +} + +.umb-healthcheck-message i { + font-size: 20px; + margin-right: 5px; +} + +.umb-healthcheck-details-link { + color: @blue; +} + +.umb-healthcheck-details-link:hover { + text-decoration: none; + color: @blue; +} + + +/* Helpers */ +.align-self-center { + align-self: center; +} + + +/* umb-buttons-era */ +.umb-era-button { + display: flex; + justify-content: center; + align-items: center; + + font-size: 14px; + font-weight: bold; + + height: 38px; + line-height: 1; + + max-width: 100%; + padding: 0 18px; + + color: #484848; + background-color: #e0e0e0; + + text-decoration: none !important; + user-select: none; + + white-space: nowrap; + overflow: hidden; + + border-radius: 3px; + border: 0 none; + box-sizing: border-box; + + cursor: pointer; + + transition: background-color 80ms ease, color 80ms ease, opacity 80ms ease; +} + + +.umb-era-button:hover, +.umb-era-button:active { + color: #484848; + background-color: #d3d3d3; + outline: none; + text-decoration: none; +} + + +.umb-era-button:focus { + outline: none; +} + +.umb-era-button.-blue { + background: @blue; + color: white; +} + +.umb-era-button.-blue:hover { + background-color: @blueDark; +} + +.umb-era-button.-link { + padding: 0; + background: transparent; +} + +.umb-era-button.-link:hover { + background-color: transparent; + opacity: .6; +} + +.umb-era-button.-inactive { + cursor: not-allowed; + color: #BBB; + background: #EAE7E7; +} + +.umb-era-button.-inactive:hover { + color: #BBB; + background: #EAE7E7; +} + + +.umb-era-button.-full-width { + display: block; + width: 100%; +} + +.umb-era-button.-white { + background-color: @white; + + &:hover { + opacity: .9; + } +} + +.umb-era-button.-text-black { + color: @black; +} + + +/* Spacing for boxes */ +.umb-air { + flex: 0 0 auto; + flex-basis: 100%; + max-width: 100%; + + padding: 10px; + box-sizing: border-box; + + @media (min-width: 500px) { + flex-basis: 50%; + max-width: 50%; + } + + @media (min-width: 768px) { + flex-basis: 20%; + max-width: 20%; + } +} + + +/* DETAILS */ + +.umb-healthcheck-back-link { + font-weight: bold; + color: @black; +} + +.umb-healthcheck-group__details { + border-radius: 3px; + margin-bottom: 40px; +} + +.umb-healthcheck-group__details-group-title { + background-color: @blue; + padding: 10px 20px; + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 3px 3px 0 0; +} + +.umb-healthcheck-group__details-group-name { + font-size: 16px; + color: @white; + font-weight: bold; +} + +.umb-healthcheck-group__details-checks { + border: 2px solid @grayLight; + border-top: none; + border-radius: 0 0 3px 3px; +} + +.umb-healthcheck-group__details-check { +} + +.umb-healthcheck-group__details-check-title { + padding: 15px 20px; + background-color: @grayLighter; +} + +.umb-healthcheck-group__details-check-name { + font-size: 14px; + color: @black; + font-weight: bold; +} + +.umb-healthcheck-group__details-check-description { + font-size: 12px; + color: @grayMed; + line-height: 1.6rem; + //margin-top: 10px; +} + +.umb-healthcheck-group__details-status { + padding: 15px 0; + display: flex; + border-bottom: 2px solid @grayLighter; + position: relative; +} + +.umb-healthcheck-group__details-status-overlay { + background: @white; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0.9; +} + +.umb-healthcheck-group__details-status:last-child { + border-bottom: none; +} + +.umb-healthcheck-group__details-status-icon-container { + flex: 0 0 50px; + display: flex; + justify-content: center; + padding: 0 20px; +} + +.umb-healthcheck-status-icon { + font-size: 20px; + margin-top: 2px; +} + +.umb-healthcheck-status-icon.-large { + width: 70px; + height: 70px; + font-size: 30px; + background-color: @white; +} + +.umb-healthcheck-group__details-status-content { + padding: 0 20px; + flex: 1 1 auto; +} + +.umb-healthcheck-group__details-status-text { + line-height: 1.6em; +} + +.umb-healthcheck-group__details-status-actions { + display: flex; + flex-direction: column; + margin-top: 10px; +} + +.umb-healthcheck-group__details-status-action { + background-color: @grayLighter; + padding: 10px; + margin-bottom: 10px; + border-radius: 3px; +} + +.umb-healthcheck-group__details-status-action:last-child { + margin-bottom: 0; +} + +.umb-healthcheck-group__details-status-action-description { + margin-top: 10px; + font-size: 13px; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js new file mode 100644 index 0000000000..e335ddd365 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js @@ -0,0 +1,136 @@ +(function () { + "use strict"; + + function HealthCheckController($scope, healthCheckResource) { + + var SUCCESS = 0; + var WARNING = 1; + var ERROR = 2; + var INFO = 3; + + var vm = this; + + vm.viewState = "list"; + vm.groups = []; + vm.selectedGroup = {}; + + vm.getStatus = getStatus; + vm.executeAction = executeAction; + vm.checkAllInGroup = checkAllInGroup; + vm.openGroup = openGroup; + vm.setViewState = setViewState; + + // Get a (grouped) list of all health checks + healthCheckResource.getAllChecks().then( + function(response) { + + // set number of checks which has been executed + for (var i = 0; i < response.length; i++) { + var group = response[i]; + group.checkCounter = 0; + checkAllInGroup(group, group.checks); + } + + vm.groups = response; + + } + ); + + function setGroupGlobalResultType(group) { + + var totalSuccess = 0; + var totalError = 0; + var totalWarning = 0; + var totalInfo = 0; + + // count total number of statusses + angular.forEach(group.checks, function(check){ + angular.forEach(check.status, function(status){ + switch(status.resultType) { + case SUCCESS: + totalSuccess = totalSuccess + 1; + break; + case WARNING: + totalWarning = totalWarning + 1; + break; + case ERROR: + totalError = totalError + 1; + break; + case INFO: + totalInfo = totalInfo + 1; + break; + } + }); + }); + + group.totalSuccess = totalSuccess; + group.totalError = totalError; + group.totalWarning = totalWarning; + group.totalInfo = totalInfo; + + } + + // Get the status of an individual check + function getStatus(check) { + check.loading = true; + check.status = null; + healthCheckResource.getStatus(check.id).then(function(response) { + check.loading = false; + check.status = response; + }); + } + + function executeAction(check, index, action) { + healthCheckResource.executeAction(action).then(function (response) { + check.status[index] = response; + }); + } + + function checkAllInGroup(group, checks) { + + group.checkCounter = 0; + group.loading = true; + + angular.forEach(checks, function(check) { + + check.loading = true; + + healthCheckResource.getStatus(check.id).then(function(response) { + check.status = response; + group.checkCounter = group.checkCounter + 1; + check.loading = false; + + // when all checks are done, set global group result + if (group.checkCounter === checks.length) { + setGroupGlobalResultType(group); + group.loading = false; + } + + }); + }); + + } + + function openGroup(group) { + vm.selectedGroup = group; + vm.viewState = "details"; + } + + function setViewState(state) { + vm.viewState = state; + + if(state === 'list') { + + for (var i = 0; i < vm.groups.length; i++) { + var group = vm.groups[i]; + setGroupGlobalResultType(group); + } + + } + + } + + } + + angular.module("umbraco").controller("Umbraco.Dashboard.HealthCheckController", HealthCheckController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html new file mode 100644 index 0000000000..2c60fa6868 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html @@ -0,0 +1,114 @@ +
+

Health Check

+

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.

+ +
+ +
+
+
{{group.name}}
+ +
+ +
+ +
+ +
+ + {{ group.totalSuccess }} +
+ +
+ + {{ group.totalWarning }} +
+ +
+ + {{ group.totalError }} +
+ +
+ + {{ group.totalInfo }} +
+ +
+ +
+
+ +
+ +
+ + + + ← Take me back + + + + +
+ +
+
{{ vm.selectedGroup.name }}
+ +
+ +
+ +
+ +
+
{{ check.name }}
+
{{ check.description }}
+
+ +
+ +
+ + + + +
+ +
+ +
+
+
+
+ +
+
+ + Set new value: + + + +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
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 validation Field 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 validation Field 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()); } ///