diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs new file mode 100644 index 0000000000..45492423ab --- /dev/null +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -0,0 +1,61 @@ +using System; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Publishing +{ + /// + /// Used to perform scheduled publishing/unpublishing + /// + internal class ScheduledPublisher + { + private readonly IContentService _contentService; + + public ScheduledPublisher(IContentService contentService) + { + _contentService = contentService; + } + + public void CheckPendingAndProcess() + { + foreach (var d in _contentService.GetContentForRelease()) + { + try + { + d.ReleaseDate = null; + var result = _contentService.SaveAndPublishWithStatus(d, (int)d.GetWriterProfile().Id); + if (result.Success == false) + { + if (result.Exception != null) + { + LogHelper.Error("Could not published the document (" + d.Id + ") based on it's scheduled release, status result: " + result.Result.StatusType, result.Exception); + } + else + { + LogHelper.Warn("Could not published the document (" + d.Id + ") based on it's scheduled release. Status result: " + result.Result.StatusType); + } + } + } + catch (Exception ee) + { + LogHelper.Error(string.Format("Error publishing node {0}", d.Id), ee); + throw; + } + } + foreach (var d in _contentService.GetContentForExpiration()) + { + try + { + d.ExpireDate = null; + _contentService.UnPublish(d, (int)d.GetWriterProfile().Id); + } + catch (Exception ee) + { + LogHelper.Error(string.Format("Error unpublishing node {0}", d.Id), ee); + throw; + } + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs b/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs new file mode 100644 index 0000000000..95305b7cdc --- /dev/null +++ b/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Sync +{ + /// + /// The current status of the server in the Umbraco environment + /// + internal enum CurrentServerEnvironmentStatus + { + /// + /// If the current server is detected as the 'master' server when configured in a load balanced scenario + /// + Master, + + /// + /// If the current server is detected as a 'slave' server when configured in a load balanced scenario + /// + Slave, + + /// + /// If the current server cannot be detected as a 'slave' or 'master' when configured in a load balanced scenario + /// + Unknown, + + /// + /// If load balancing is not enabled and this is the only server in the umbraco environment + /// + Single + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs new file mode 100644 index 0000000000..73dca4cc98 --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Web; +using System.Xml; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Sync +{ + /// + /// A helper used to determine the current server environment status + /// + internal static class ServerEnvironmentHelper + { + /// + /// Returns the current umbraco base url for the current server depending on it's environment + /// status. This will attempt to determine the internal umbraco base url that can be used by the current + /// server to send a request to itself if it is in a load balanced environment. + /// + /// The full base url including schema (i.e. http://myserver:80/umbraco ) + public static string GetCurrentServerUmbracoBaseUrl() + { + var status = GetStatus(); + + if (status == CurrentServerEnvironmentStatus.Single) + { + //if it's a single install, then the base url has to be the first url registered + return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl); + } + + var servers = UmbracoSettings.DistributionServers; + + var nodes = servers.SelectNodes("./server"); + if (nodes == null) + { + //cannot be determined, then the base url has to be the first url registered + return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl); + } + + var xmlNodes = nodes.Cast().ToList(); + + foreach (var xmlNode in xmlNodes) + { + var appId = xmlNode.AttributeValue("appId"); + var serverName = xmlNode.AttributeValue("serverName"); + + if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) + { + continue; + } + + if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) + || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) + { + //match by appId or computer name! return the url configured + return string.Format("{0}://{1}:{2}/{3}", + xmlNode.AttributeValue("forceProtocol").IsNullOrWhiteSpace() ? "http" : xmlNode.AttributeValue("forceProtocol"), + xmlNode.InnerText, + xmlNode.AttributeValue("forcePortnumber").IsNullOrWhiteSpace() ? "80" : xmlNode.AttributeValue("forcePortnumber"), + IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); + } + } + + //cannot be determined, then the base url has to be the first url registered + return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl); + } + + /// + /// Returns the current environment status for the current server + /// + /// + public static CurrentServerEnvironmentStatus GetStatus() + { + if (UmbracoSettings.UseDistributedCalls == false) + { + return CurrentServerEnvironmentStatus.Single; + } + + var servers = UmbracoSettings.DistributionServers; + + var nodes = servers.SelectNodes("./server"); + if (nodes == null) + { + return CurrentServerEnvironmentStatus.Unknown; + } + + var master = nodes.Cast().FirstOrDefault(); + + if (master == null) + { + return CurrentServerEnvironmentStatus.Unknown; + } + + //we determine master/slave based on the first server registered + //TODO: In v7 we have publicized ServerRegisterResolver - we won't be able to determine this based on that + // but we'd need to change the IServerAddress interfaces which is breaking. + + var appId = master.AttributeValue("appId"); + var serverName = master.AttributeValue("serverName"); + + if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) + { + return CurrentServerEnvironmentStatus.Unknown; + } + + if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) + || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) + { + //match by appdid or server name! + return CurrentServerEnvironmentStatus.Master; + } + + return CurrentServerEnvironmentStatus.Slave; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index bc0fdff936..b394c04460 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -994,6 +994,7 @@ + @@ -1066,7 +1067,9 @@ + + diff --git a/src/Umbraco.Tests/TypeHelperTests.cs b/src/Umbraco.Tests/TypeHelperTests.cs index 42e5b0bdd2..9b8d38aec7 100644 --- a/src/Umbraco.Tests/TypeHelperTests.cs +++ b/src/Umbraco.Tests/TypeHelperTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Web; using Umbraco.Web.Cache; +using Umbraco.Web.Scheduling; using UmbracoExamine; using UmbracoExamine.DataServices; using umbraco; @@ -67,7 +68,7 @@ namespace Umbraco.Tests Assert.AreEqual(typeof(PropertyAliasDto), t5.Result); var t6 = TypeHelper.GetLowestBaseType(typeof (IApplicationEventHandler), - typeof (LegacyScheduledTasks), + typeof (Scheduler), typeof(CacheRefresherEventHandler)); Assert.IsTrue(t6.Success); Assert.AreEqual(typeof(IApplicationEventHandler), t6.Result); diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 2ccc8a5077..4ed96cf533 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -74,16 +74,47 @@ - + 0 + + + - - - - + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index ab02e229e6..36e724a915 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -210,16 +210,51 @@ - - + + 0 + + + - - - - + + + + localhost + umb1.dev + umb2.dev + diff --git a/src/Umbraco.Web/LegacyScheduledTasks.cs b/src/Umbraco.Web/LegacyScheduledTasks.cs deleted file mode 100644 index 1fcc34d082..0000000000 --- a/src/Umbraco.Web/LegacyScheduledTasks.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Web; -using System.Web.Caching; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using global::umbraco.BusinessLogic; - -namespace Umbraco.Web -{ - - internal sealed class LegacyScheduledTasks : ApplicationEventHandler - { - Timer _pingTimer; - Timer _publishingTimer; - CacheItemRemovedCallback _onCacheRemove; - - protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, Core.ApplicationContext applicationContext) - { - if (umbracoApplication.Context == null) - return; - - // time to setup the tasks - - // these are the legacy tasks - // just copied over here for backward compatibility - // of course we should have a proper scheduler, see #U4-809 - - // ping/keepalive - _pingTimer = new Timer(new TimerCallback(global::umbraco.presentation.keepAliveService.PingUmbraco), applicationContext, 60000, 300000); - - // (un)publishing _and_ also run scheduled tasks (!) - _publishingTimer = new Timer(new TimerCallback(global::umbraco.presentation.publishingService.CheckPublishing), applicationContext, 30000, 60000); - - // log scrubbing - AddTask(LOG_SCRUBBER_TASK_NAME, GetLogScrubbingInterval()); - } - - #region Log Scrubbing - - // this is a raw copy of the legacy code in all its uglyness - - const string LOG_SCRUBBER_TASK_NAME = "ScrubLogs"; - - private static int GetLogScrubbingInterval() - { - int interval = 24 * 60 * 60; //24 hours - try - { - if (UmbracoConfig.For.UmbracoSettings().Logging.CleaningMiliseconds > -1) - interval = UmbracoConfig.For.UmbracoSettings().Logging.CleaningMiliseconds; - } - catch (Exception e) - { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); - } - return interval; - } - - private static int GetLogScrubbingMaximumAge() - { - int maximumAge = 24 * 60 * 60; - try - { - if (UmbracoConfig.For.UmbracoSettings().Logging.MaxLogAge > -1) - maximumAge = UmbracoConfig.For.UmbracoSettings().Logging.MaxLogAge; - } - catch (Exception e) - { - LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); - } - return maximumAge; - - } - - private void AddTask(string name, int seconds) - { - _onCacheRemove = new CacheItemRemovedCallback(CacheItemRemoved); - HttpRuntime.Cache.Insert(name, seconds, null, - DateTime.Now.AddSeconds(seconds), System.Web.Caching.Cache.NoSlidingExpiration, - CacheItemPriority.NotRemovable, _onCacheRemove); - } - - public void CacheItemRemoved(string k, object v, CacheItemRemovedReason r) - { - if (k.Equals(LOG_SCRUBBER_TASK_NAME)) - { - ScrubLogs(); - } - AddTask(k, Convert.ToInt32(v)); - } - - private static void ScrubLogs() - { - Log.CleanLogs(GetLogScrubbingMaximumAge()); - } - - #endregion - } -} diff --git a/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs new file mode 100644 index 0000000000..de8cf65a53 --- /dev/null +++ b/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs @@ -0,0 +1,108 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Mvc +{ + /// + /// Used for authorizing scheduled tasks + /// + public sealed class AdminTokenAuthorizeAttribute : AuthorizeAttribute + { + private readonly ApplicationContext _applicationContext; + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// + public AdminTokenAuthorizeAttribute(ApplicationContext appContext) + { + if (appContext == null) throw new ArgumentNullException("appContext"); + _applicationContext = appContext; + } + + public AdminTokenAuthorizeAttribute() + { + } + + private ApplicationContext GetApplicationContext() + { + return _applicationContext ?? ApplicationContext.Current; + } + + /// + /// Used to return the value that needs to go in the Authorization header + /// + /// + /// + public static string GetAuthHeaderTokenVal(ApplicationContext appContext) + { + var admin = appContext.Services.UserService.GetUserById(0); + + var token = string.Format("{0}u____u{1}u____u{2}", admin.Email, admin.Username, admin.RawPasswordValue); + + var encrypted = token.EncryptWithMachineKey(); + var bytes = Encoding.UTF8.GetBytes(encrypted); + var base64 = Convert.ToBase64String(bytes); + return "AToken val=\"" + base64 + "\""; + } + + /// + /// Ensures that the user must be in the Administrator or the Install role + /// + /// + /// + protected override bool AuthorizeCore(HttpContextBase httpContext) + { + if (httpContext == null) throw new ArgumentNullException("httpContext"); + + var appContext = GetApplicationContext(); + + //we need to that the app is configured and that a user is logged in + if (appContext.IsConfigured == false) return false; + + //need the auth header + if (httpContext.Request.Headers["Authorization"] == null || httpContext.Request.Headers["Authorization"].IsNullOrWhiteSpace()) return false; + + var header = httpContext.Request.Headers["Authorization"]; + if (header.StartsWith("AToken ") == false) return false; + + var keyVal = Regex.Matches(header, "AToken val=(.*?)(?:$|\\s)"); + if (keyVal.Count != 1) return false; + if (keyVal[0].Groups.Count != 2) return false; + + var admin = appContext.Services.UserService.GetUserById(0); + if (admin == null) return false; + + try + { + //get the token value from the header + var val = keyVal[0].Groups[1].Value.Trim("\""); + //un-base 64 the string + var bytes = Convert.FromBase64String(val); + var encrypted = Encoding.UTF8.GetString(bytes); + //decrypt the string + var text = encrypted.DecryptWithMachineKey(); + + //split + var split = text.Split(new[] {"u____u"}, StringSplitOptions.RemoveEmptyEntries); + if (split.Length != 3) return false; + + //compare + return + split[0] == admin.Email + && split[1] == admin.Username + && split[2] == admin.RawPasswordValue; + } + catch (Exception ex) + { + LogHelper.Error("Failed to format passed in token value", ex); + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs index adf4353cfb..c5ebf24626 100644 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs @@ -7,7 +7,7 @@ using umbraco.BasePages; namespace Umbraco.Web.Mvc { - /// + /// /// Ensures authorization is successful for a back office user /// public sealed class UmbracoAuthorizeAttribute : AuthorizeAttribute diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs new file mode 100644 index 0000000000..443c3727e1 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -0,0 +1,35 @@ +using System; +using System.Net; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Sync; + +namespace Umbraco.Web.Scheduling +{ + internal class KeepAlive + { + public static void Start(object sender) + { + //NOTE: sender will be the umbraco ApplicationContext + + var appContext = sender as ApplicationContext; + if (appContext == null) return; + + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); + + var url = string.Format("{0}/ping.aspx", umbracoBaseUrl); + + try + { + using (var wc = new WebClient()) + { + wc.DownloadString(url); + } + } + catch (Exception ee) + { + LogHelper.Error("Error in ping", ee); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs new file mode 100644 index 0000000000..cea4d1b01c --- /dev/null +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -0,0 +1,77 @@ +using System; +using System.Web; +using System.Web.Caching; +using umbraco.BusinessLogic; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Scheduling +{ + //TODO: Refactor this to use a normal scheduling processor! + + internal class LogScrubber + { + // this is a raw copy of the legacy code in all its uglyness + + CacheItemRemovedCallback _onCacheRemove; + const string LogScrubberTaskName = "ScrubLogs"; + + public void Start() + { + // log scrubbing + AddTask(LogScrubberTaskName, GetLogScrubbingInterval()); + } + + private static int GetLogScrubbingInterval() + { + int interval = 24 * 60 * 60; //24 hours + try + { + if (global::umbraco.UmbracoSettings.CleaningMiliseconds > -1) + interval = global::umbraco.UmbracoSettings.CleaningMiliseconds; + } + catch (Exception e) + { + LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); + } + return interval; + } + + private static int GetLogScrubbingMaximumAge() + { + int maximumAge = 24 * 60 * 60; + try + { + if (global::umbraco.UmbracoSettings.MaxLogAge > -1) + maximumAge = global::umbraco.UmbracoSettings.MaxLogAge; + } + catch (Exception e) + { + LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); + } + return maximumAge; + + } + + private void AddTask(string name, int seconds) + { + _onCacheRemove = new CacheItemRemovedCallback(CacheItemRemoved); + HttpRuntime.Cache.Insert(name, seconds, null, + DateTime.Now.AddSeconds(seconds), System.Web.Caching.Cache.NoSlidingExpiration, + CacheItemPriority.NotRemovable, _onCacheRemove); + } + + public void CacheItemRemoved(string k, object v, CacheItemRemovedReason r) + { + if (k.Equals(LogScrubberTaskName)) + { + ScrubLogs(); + } + AddTask(k, Convert.ToInt32(v)); + } + + private static void ScrubLogs() + { + Log.CleanLogs(GetLogScrubbingMaximumAge()); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs new file mode 100644 index 0000000000..ef8d5ef65a --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; +using Umbraco.Core.Sync; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Scheduling +{ + internal class ScheduledPublishing + { + private static bool _isPublishingRunning = false; + + public void Start(object sender) + { + //NOTE: sender will be the umbraco ApplicationContext + + var appContext = sender as ApplicationContext; + if (appContext == null) return; + + if (_isPublishingRunning) return; + + _isPublishingRunning = true; + + try + { + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); + var url = string.Format("{0}/RestServices/ScheduledPublish/", umbracoBaseUrl); + using (var wc = new WebClient()) + { + //pass custom the authorization header + wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(appContext)); + + var result = wc.UploadString(url, ""); + } + } + catch (Exception ee) + { + LogHelper.Error("An error occurred with the scheduled publishing", ee); + } + finally + { + _isPublishingRunning = false; + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs new file mode 100644 index 0000000000..9abf7d9f69 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Net; +using System.Xml; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; +using Umbraco.Core.Sync; + +namespace Umbraco.Web.Scheduling +{ + //TODO: No scheduled task (i.e. URL) would be secured, so if people are actually using these each task + // would need to be a publicly available task (URL) which isn't really very good :( + + internal class ScheduledTasks + { + private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); + private static bool _isPublishingRunning = false; + + public void Start(object sender) + { + //NOTE: sender will be the umbraco ApplicationContext + + if (_isPublishingRunning) return; + + _isPublishingRunning = true; + + try + { + ProcessTasks(); + } + catch (Exception ee) + { + LogHelper.Error("Error executing scheduled task", ee); + } + finally + { + _isPublishingRunning = false; + } + } + + private static void ProcessTasks() + { + + + var scheduledTasks = UmbracoSettings.ScheduledTasks; + if (scheduledTasks != null) + { + var tasks = scheduledTasks.SelectNodes("./task"); + if (tasks == null) return; + + foreach (XmlNode task in tasks) + { + var runTask = false; + if (ScheduledTaskTimes.ContainsKey(task.Attributes.GetNamedItem("alias").Value) == false) + { + runTask = true; + ScheduledTaskTimes.Add(task.Attributes.GetNamedItem("alias").Value, DateTime.Now); + } + // Add 1 second to timespan to compensate for differencies in timer + else if (new TimeSpan( + DateTime.Now.Ticks - ((DateTime)ScheduledTaskTimes[task.Attributes.GetNamedItem("alias").Value]).Ticks).TotalSeconds + 1 + >= int.Parse(task.Attributes.GetNamedItem("interval").Value)) + { + runTask = true; + ScheduledTaskTimes[task.Attributes.GetNamedItem("alias").Value] = DateTime.Now; + } + + if (runTask) + { + bool taskResult = GetTaskByHttp(task.Attributes.GetNamedItem("url").Value); + if (bool.Parse(task.Attributes.GetNamedItem("log").Value)) + LogHelper.Info(string.Format("{0} has been called with response: {1}", task.Attributes.GetNamedItem("alias").Value, taskResult)); + } + } + } + } + + private static bool GetTaskByHttp(string url) + { + var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); + + try + { + using (var response = (HttpWebResponse)myHttpWebRequest.GetResponse()) + { + return response.StatusCode == HttpStatusCode.OK; + } + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling web task for url: " + url, ex); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs new file mode 100644 index 0000000000..75930fed4c --- /dev/null +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -0,0 +1,82 @@ +using System.Threading; +using Umbraco.Core; +using Umbraco.Core.Sync; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Used to do the scheduling for tasks, publishing, etc... + /// + /// + /// + /// TODO: Much of this code is legacy and needs to be updated, there are a few new/better ways to do scheduling + /// in a web project nowadays. + /// + /// + internal sealed class Scheduler : ApplicationEventHandler + { + private Timer _pingTimer; + private Timer _schedulingTimer; + private LogScrubber _scrubber; + + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + if (umbracoApplication.Context == null) + return; + + // time to setup the tasks + + // these are the legacy tasks + // just copied over here for backward compatibility + // of course we should have a proper scheduler, see #U4-809 + + // ping/keepalive + _pingTimer = new Timer(KeepAlive.Start, applicationContext, 60000, 300000); + + // scheduled publishing/unpublishing + + _schedulingTimer = new Timer(PerformScheduling, applicationContext, 30000, 60000); + + //log scrubbing + _scrubber = new LogScrubber(); + _scrubber.Start(); + } + + /// + /// This performs all of the scheduling on the one timer + /// + /// + /// + /// No processing will be done if this server is a slave + /// + private static void PerformScheduling(object sender) + { + + //get the current server status to see if this server should execute the scheduled publishing + var serverStatus = ServerEnvironmentHelper.GetStatus(); + + switch (serverStatus) + { + case CurrentServerEnvironmentStatus.Single: + case CurrentServerEnvironmentStatus.Master: + case CurrentServerEnvironmentStatus.Unknown: + //if it's a single server install, a master or it cannot be determined + // then we will process the scheduling + + //do the scheduled publishing + var scheduledPublishing = new ScheduledPublishing(); + scheduledPublishing.Start(sender); + + //do the scheduled tasks + var scheduledTasks = new ScheduledTasks(); + scheduledTasks.Start(sender); + + break; + case CurrentServerEnvironmentStatus.Slave: + //do not process + break; + } + } + + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f49ad3177e..d921243e65 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -358,6 +358,7 @@ + @@ -540,6 +541,10 @@ + + + + @@ -781,7 +786,7 @@ ASPXCodeBehind - + @@ -1828,6 +1833,7 @@ + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index b52e30edac..021138c940 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -31,6 +31,7 @@ using Umbraco.Web.PropertyEditors.ValueConverters; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; +using Umbraco.Web.Scheduling; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; diff --git a/src/Umbraco.Web/WebServices/ScheduledPublishController.cs b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs new file mode 100644 index 0000000000..0ab76738dc --- /dev/null +++ b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs @@ -0,0 +1,58 @@ +using System; +using System.Web.Mvc; +using umbraco; +using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.WebServices +{ + /// + /// A REST controller used for running the scheduled publishing, this is called from the background worker timer + /// + [AdminTokenAuthorize] + public class ScheduledPublishController : UmbracoController + { + private static bool _isPublishingRunning = false; + + [HttpPost] + public JsonResult Index() + { + if (_isPublishingRunning) + return null; + _isPublishingRunning = true; + + try + { + // DO not run publishing if content is re-loading + if (content.Instance.isInitializing == false) + { + var publisher = new ScheduledPublisher(Services.ContentService); + publisher.CheckPendingAndProcess(); + } + + return Json(new + { + success = true + }); + + } + catch (Exception ee) + { + LogHelper.Error("Error executing scheduled task", ee); + + Response.StatusCode = 400; + + return Json(new + { + success = false, + message = ee.Message + }); + } + finally + { + _isPublishingRunning = false; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs index 4ab1646a9e..6d80d8bedd 100644 --- a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs +++ b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs @@ -7,9 +7,7 @@ using Umbraco.Core.Logging; namespace umbraco.presentation { - /// - /// Makes a call to /umbraco/ping.aspx which is used to keep the web app alive - /// + [Obsolete("This is no longer used and will be removed in future versions")] public class keepAliveService { //NOTE: sender will be the umbraco ApplicationContext @@ -20,6 +18,8 @@ namespace umbraco.presentation var appContext = (ApplicationContext) sender; + //TODO: This won't always work, in load balanced scenarios ping will not work because + // this original request url will be public and not internal to the server. var url = string.Format("http://{0}/ping.aspx", appContext.OriginalRequestUrl); try { diff --git a/src/Umbraco.Web/umbraco.presentation/publishingService.cs b/src/Umbraco.Web/umbraco.presentation/publishingService.cs index 45af834543..e9becdc5ef 100644 --- a/src/Umbraco.Web/umbraco.presentation/publishingService.cs +++ b/src/Umbraco.Web/umbraco.presentation/publishingService.cs @@ -5,15 +5,15 @@ using System.Net; using System.Web; using System.Xml; using Umbraco.Core.Configuration; +using Umbraco.Core; using Umbraco.Core.Logging; using umbraco.BusinessLogic; using umbraco.cms.businesslogic.web; +using Umbraco.Core.Publishing; namespace umbraco.presentation { - /// - /// Summary description for publishingService. - /// + [Obsolete("This is no longer used and will be removed in future versions")] public class publishingService { private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); @@ -27,37 +27,10 @@ namespace umbraco.presentation _isPublishingRunning = true; try { - // DO not run publishing if content is re-loading - if(!content.Instance.isInitializing) - { - - foreach (var d in Document.GetDocumentsForRelease()) - { - try - { - d.ReleaseDate = DateTime.MinValue; //new DateTime(1, 1, 1); // Causes release date to be null - d.SaveAndPublish(d.User); - } - catch(Exception ee) - { - LogHelper.Error(string.Format("Error publishing node {0}", d.Id), ee); - } - } - foreach(Document d in Document.GetDocumentsForExpiration()) - { - try - { - d.ExpireDate = DateTime.MinValue; + //run the scheduled publishing - we need to determine if this server - d.UnPublish(); - } - catch (Exception ee) - { - LogHelper.Error(string.Format("Error unpublishing node {0}", d.Id), ee); - } - - } - } + var publisher = new ScheduledPublisher(ApplicationContext.Current.Services.ContentService); + publisher.CheckPendingAndProcess(); // run scheduled url tasks try