diff --git a/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj b/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj index 2456da972..ab32431f2 100644 --- a/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj +++ b/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj @@ -169,6 +169,8 @@ + + @@ -254,6 +256,10 @@ {79AED36E-ABD0-4747-93D3-8722B042454B} Orchard.Users + + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD} + Orchard.Warmup + {194D3CCC-1153-474D-8176-FDE8D7D0D0BD} Orchard.Widgets diff --git a/src/Orchard.Tests.Modules/Warmup/WarmupUpdaterTests.cs b/src/Orchard.Tests.Modules/Warmup/WarmupUpdaterTests.cs new file mode 100644 index 000000000..71a704791 --- /dev/null +++ b/src/Orchard.Tests.Modules/Warmup/WarmupUpdaterTests.cs @@ -0,0 +1,281 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Xml; +using Autofac; +using Moq; +using NUnit.Framework; +using Orchard.Environment.Warmup; +using Orchard.FileSystems.AppData; +using Orchard.FileSystems.LockFile; +using Orchard.Services; +using Orchard.Tests.FileSystems.AppData; +using Orchard.Tests.Stubs; +using Orchard.Tests.UI.Navigation; +using Orchard.Warmup.Models; +using Orchard.Warmup.Services; + +namespace Orchard.Tests.Modules.Warmup { + public class WarmupUpdaterTests { + protected IContainer _container; + private IWarmupUpdater _warmupUpdater; + private IAppDataFolder _appDataFolder; + private ILockFileManager _lockFileManager; + private StubClock _clock; + private Mock _webDownloader; + private IOrchardServices _orchardServices; + private WarmupSettingsPart _settings; + + private readonly string _basePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + private string _warmupFilename, _lockFilename; + private const string WarmupFolder = "Warmup"; + + [TestFixtureTearDown] + public void Clean() { + if (Directory.Exists(_basePath)) { + Directory.Delete(_basePath, true); + } + } + + [SetUp] + public void Init() { + if (Directory.Exists(_basePath)) { + Directory.Delete(_basePath, true); + } + + Directory.CreateDirectory(_basePath); + _appDataFolder = AppDataFolderTests.CreateAppDataFolder(_basePath); + _webDownloader = new Mock(); + _orchardServices = new StubOrchardServices(); + ((StubWorkContextAccessor.WorkContextImpl.StubSite) _orchardServices.WorkContext.CurrentSite).BaseUrl = "http://orchardproject.net"; + + _settings = new WarmupSettingsPart { Record = new WarmupSettingsPartRecord() }; + _orchardServices.WorkContext.CurrentSite.ContentItem.Weld(_settings); + + var builder = new ContainerBuilder(); + builder.RegisterInstance(_appDataFolder).As(); + builder.RegisterInstance(_orchardServices).As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterInstance(_clock = new StubClock()).As(); + builder.RegisterInstance(_webDownloader.Object).As(); + _container = builder.Build(); + + _lockFileManager = _container.Resolve(); + _warmupUpdater = _container.Resolve(); + + _warmupFilename = _appDataFolder.Combine(WarmupFolder, "warmup.txt"); + _lockFilename = _appDataFolder.Combine(WarmupFolder, "warmup.txt.lock"); + } + + [Test] + public void ShouldDoNothingWhenNoUrlsAreSpecified() { + _warmupUpdater.EnsureGenerate(); + Assert.That(_appDataFolder.ListFiles(WarmupFolder).Count(), Is.EqualTo(0)); + } + + [Test] + public void StampFileShouldBeDeletedToForceAnUpdate() { + _appDataFolder.CreateFile(_warmupFilename, ""); + _warmupUpdater.Generate(); + Assert.That(_appDataFolder.ListFiles(WarmupFolder).Count(), Is.EqualTo(0)); + } + + [Test] + public void GenerateShouldNotRunIfLocked() { + _appDataFolder.CreateFile(_warmupFilename, ""); + ILockFile lockFile = null; + _lockFileManager.TryAcquireLock(_lockFilename, ref lockFile); + using(lockFile) { + _warmupUpdater.Generate(); + // warmup file + lock file + Assert.That(_appDataFolder.ListFiles(WarmupFolder).Count(), Is.EqualTo(2)); + } + + _warmupUpdater.Generate(); + Assert.That(_appDataFolder.ListFiles(WarmupFolder).Count(), Is.EqualTo(0)); + } + + [Test] + public void ShouldDownloadConfiguredUrls() { + _settings.Urls = @" / + /About"; + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/About")) + .Returns(new DownloadResult { Content = "Bar", StatusCode = HttpStatusCode.OK }); + + _warmupUpdater.Generate(); + var files = _appDataFolder.ListFiles(WarmupFolder).ToList(); + + // warmup + content files + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, "warmup.txt"))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/")))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About")))); + + var homepageContent = _appDataFolder.ReadFile(_appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/"))); + var aboutcontent = _appDataFolder.ReadFile(_appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About"))); + + Assert.That(homepageContent, Is.EqualTo("Foo")); + Assert.That(aboutcontent, Is.EqualTo("Bar")); + } + + [Test] + public void ShouldCreateFilesForOkStatusOnly() { + _settings.Urls = @" / + /About"; + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/About")) + .Returns(new DownloadResult { Content = "Bar", StatusCode = HttpStatusCode.NotFound }); + + _warmupUpdater.Generate(); + var files = _appDataFolder.ListFiles(WarmupFolder).ToList(); + + // warmup + content file + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, "warmup.txt"))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/")))); + Assert.That(files, Has.None.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About")))); + } + + [Test] + public void ShouldProcessValidRequestsOnly() { + _settings.Urls = @" / + <>@\\"; + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _warmupUpdater.Generate(); + var files = _appDataFolder.ListFiles(WarmupFolder).ToList(); + + // warmup + content file + Assert.That(files.Count, Is.EqualTo(2)); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, "warmup.txt"))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/")))); + } + + [Test] + public void WarmupFileShouldContainUtcNow() { + _settings.Urls = @"/"; + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _warmupUpdater.Generate(); + + var warmupContent = _appDataFolder.ReadFile(_warmupFilename); + Assert.That(warmupContent, Is.EqualTo(XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))); + } + + [Test] + public void ShouldNotProcessIfDelayHasNotExpired() { + _settings.Urls = @"/"; + _settings.Delay = 90; + _settings.Scheduled = true; + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/About")) + .Returns(new DownloadResult { Content = "Bar", StatusCode = HttpStatusCode.OK }); + + _warmupUpdater.Generate(); + + var warmupContent = _appDataFolder.ReadFile(_warmupFilename); + Assert.That(warmupContent, Is.EqualTo(XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))); + + _settings.Urls = @" / + /About"; + _clock.Advance(TimeSpan.FromMinutes(89)); + _warmupUpdater.EnsureGenerate(); + + var files = _appDataFolder.ListFiles(WarmupFolder).ToList(); + Assert.That(files, Has.None.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About")))); + + warmupContent = _appDataFolder.ReadFile(_warmupFilename); + Assert.That(warmupContent, Is.Not.EqualTo(XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))); + } + + [Test] + public void ShouldProcessIfDelayHasExpired() { + _settings.Urls = @"/"; + _settings.Delay = 90; + _settings.Scheduled = true; + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _webDownloader + .Setup(w => w.Download("http://orchardproject.net/About")) + .Returns(new DownloadResult { Content = "Bar", StatusCode = HttpStatusCode.OK }); + + _warmupUpdater.Generate(); + + var warmupContent = _appDataFolder.ReadFile(_warmupFilename); + Assert.That(warmupContent, Is.EqualTo(XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))); + + _settings.Urls = @" / + /About"; + _clock.Advance(TimeSpan.FromMinutes(91)); + _warmupUpdater.EnsureGenerate(); + + var files = _appDataFolder.ListFiles(WarmupFolder).ToList(); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About")))); + + warmupContent = _appDataFolder.ReadFile(_warmupFilename); + Assert.That(warmupContent, Is.EqualTo(XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))); + } + + [Test] + public void ShouldGenerateNonWwwVersions() { + _settings.Urls = @" / + /About"; + + ((StubWorkContextAccessor.WorkContextImpl.StubSite)_orchardServices.WorkContext.CurrentSite).BaseUrl = "http://www.orchardproject.net"; + + _webDownloader + .Setup(w => w.Download("http://www.orchardproject.net/")) + .Returns(new DownloadResult { Content = "Foo", StatusCode = HttpStatusCode.OK }); + + _webDownloader + .Setup(w => w.Download("http://www.orchardproject.net/About")) + .Returns(new DownloadResult { Content = "Bar", StatusCode = HttpStatusCode.OK }); + + _warmupUpdater.Generate(); + var files = _appDataFolder.ListFiles(WarmupFolder).ToList(); + + // warmup + content files + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, "warmup.txt"))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://www.orchardproject.net/")))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://www.orchardproject.net/About")))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/")))); + Assert.That(files, Has.Some.Matches(x => x == _appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About")))); + + var homepageContent = _appDataFolder.ReadFile(_appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/"))); + var aboutcontent = _appDataFolder.ReadFile(_appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://orchardproject.net/About"))); + + var wwwhomepageContent = _appDataFolder.ReadFile(_appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://www.orchardproject.net/"))); + var wwwaboutcontent = _appDataFolder.ReadFile(_appDataFolder.Combine(WarmupFolder, WarmupUtility.EncodeUrl("http://www.orchardproject.net/About"))); + + Assert.That(homepageContent, Is.EqualTo("Foo")); + Assert.That(wwwhomepageContent, Is.EqualTo("Foo")); + Assert.That(aboutcontent, Is.EqualTo("Bar")); + Assert.That(wwwaboutcontent, Is.EqualTo("Bar")); + } + + } +} diff --git a/src/Orchard.Tests.Modules/Warmup/WebDownloaderTests.cs b/src/Orchard.Tests.Modules/Warmup/WebDownloaderTests.cs new file mode 100644 index 000000000..66fbd993b --- /dev/null +++ b/src/Orchard.Tests.Modules/Warmup/WebDownloaderTests.cs @@ -0,0 +1,37 @@ +using System.Net; +using NUnit.Framework; +using Orchard.Warmup.Services; + +namespace Orchard.Tests.Modules.Warmup { + public class WebDownloaderTests { + private readonly IWebDownloader _webDownloader = new WebDownloader(); + + [Test] + public void ShouldReturnNullWhenUrlIsEmpty() { + Assert.That(_webDownloader.Download(null), Is.Null); + Assert.That(_webDownloader.Download(""), Is.Null); + Assert.That(_webDownloader.Download(" "), Is.Null); + } + + [Test] + public void ShouldReturnNullWhenUrlIsInvalid() { + Assert.That(_webDownloader.Download("froutfrout|yepyep"), Is.Null); + } + + [Test] + public void StatusCodeShouldBe404ForUnexistingResources() { + var download = _webDownloader.Download("http://www.microsoft.com/yepyep"); + Assert.That(download, Is.Not.Null); + Assert.That(download.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + Assert.That(download.Content, Is.Null); + } + + [Test] + public void StatusCodeShouldBe200ForValidRequests() { + var download = _webDownloader.Download("http://www.microsoft.com/"); + Assert.That(download, Is.Not.Null); + Assert.That(download.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(download.Content, Is.Not.Empty); + } + } +} diff --git a/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs b/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs new file mode 100644 index 000000000..d402adc42 --- /dev/null +++ b/src/Orchard.Tests/Environment/WarmUp/WarmUpUtilityTests.cs @@ -0,0 +1,28 @@ +using System; +using NUnit.Framework; +using Orchard.Environment.Warmup; + +namespace Orchard.Tests.Environment.Warmup { + [TestFixture] + public class WarmUpUtilityTests { + + [Test] + public void EmptyStringsAreNotAllowed() { + Assert.Throws(() => WarmupUtility.EncodeUrl("")); + Assert.Throws(() => WarmupUtility.EncodeUrl(null)); + } + + [Test] + public void EncodedUrlsShouldBeValidFilenames() { + Assert.That(WarmupUtility.EncodeUrl("http://www.microsoft.com"), Is.EqualTo("http_3A_2F_2Fwww_2Emicrosoft_2Ecom")); + Assert.That(WarmupUtility.EncodeUrl("http://www.microsoft.com/foo?bar=baz"), Is.EqualTo("http_3A_2F_2Fwww_2Emicrosoft_2Ecom_2Ffoo_3Fbar_3Dbaz")); + } + + [Test] + public void EncodedUrlsShouldPreserveQueryStrings() { + Assert.That(WarmupUtility.EncodeUrl("http://www.microsoft.com/foo?bar=baz"), Is.StringContaining("bar")); + Assert.That(WarmupUtility.EncodeUrl("http://www.microsoft.com/foo?bar=baz"), Is.StringContaining("baz")); + Assert.That(WarmupUtility.EncodeUrl("http://www.microsoft.com/foo?bar=baz"), Is.StringContaining("foo")); + } + } +} diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 9d1c806f2..57f4a1b23 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -239,6 +239,7 @@ + diff --git a/src/Orchard.Tests/Stubs/StubWorkContextAccessor.cs b/src/Orchard.Tests/Stubs/StubWorkContextAccessor.cs index cae6eb4f4..807ffc968 100644 --- a/src/Orchard.Tests/Stubs/StubWorkContextAccessor.cs +++ b/src/Orchard.Tests/Stubs/StubWorkContextAccessor.cs @@ -71,10 +71,12 @@ namespace Orchard.Tests.Stubs { set { throw new NotImplementedException(); } } - public int PageSize { + public int PageSize{ get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + + public string BaseUrl { get; set;} } public class StubUser : IUser { diff --git a/src/Orchard.Tests/UI/Navigation/NavigationManagerTests.cs b/src/Orchard.Tests/UI/Navigation/NavigationManagerTests.cs index 1aa26edec..304bd1203 100644 --- a/src/Orchard.Tests/UI/Navigation/NavigationManagerTests.cs +++ b/src/Orchard.Tests/UI/Navigation/NavigationManagerTests.cs @@ -140,8 +140,15 @@ namespace Orchard.Tests.UI.Navigation { get { throw new NotImplementedException(); } } + private WorkContext _workContext; public WorkContext WorkContext { - get { return new StubWorkContextAccessor(_lifetimeScope).GetContext(); } + get { + if(_workContext == null) { + _workContext = new StubWorkContextAccessor(_lifetimeScope).GetContext(); + } + + return _workContext; + } } } } diff --git a/src/Orchard.Web/Core/Settings/Controllers/AdminController.cs b/src/Orchard.Web/Core/Settings/Controllers/AdminController.cs index 99d768df8..af64ffb51 100644 --- a/src/Orchard.Web/Core/Settings/Controllers/AdminController.cs +++ b/src/Orchard.Web/Core/Settings/Controllers/AdminController.cs @@ -64,27 +64,26 @@ namespace Orchard.Core.Settings.Controllers { var site = _siteService.GetSiteSettings(); dynamic model = Services.ContentManager.UpdateEditor(site, this, groupInfoId); + GroupInfo groupInfo = null; + if (!string.IsNullOrWhiteSpace(groupInfoId)) { if (model == null) { Services.TransactionManager.Cancel(); return HttpNotFound(); } - var groupInfo = Services.ContentManager.GetEditorGroupInfo(site, groupInfoId); + groupInfo = Services.ContentManager.GetEditorGroupInfo(site, groupInfoId); if (groupInfo == null) { Services.TransactionManager.Cancel(); return HttpNotFound(); } - - if (!ModelState.IsValid) { - Services.TransactionManager.Cancel(); - model.GroupInfo = groupInfo; - - // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation. - return View((object) model); - } } - else { + + if (!ModelState.IsValid) { + Services.TransactionManager.Cancel(); + model.GroupInfo = groupInfo; + + // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation. return View((object)model); } diff --git a/src/Orchard.Web/Core/Settings/Drivers/SiteSettingsPartDriver.cs b/src/Orchard.Web/Core/Settings/Drivers/SiteSettingsPartDriver.cs index 29b0d6204..54fc9437d 100644 --- a/src/Orchard.Web/Core/Settings/Drivers/SiteSettingsPartDriver.cs +++ b/src/Orchard.Web/Core/Settings/Drivers/SiteSettingsPartDriver.cs @@ -1,9 +1,11 @@ -using JetBrains.Annotations; +using System.Net; +using JetBrains.Annotations; using Orchard.ContentManagement; using Orchard.ContentManagement.Drivers; using Orchard.Core.Settings.Models; using Orchard.Core.Settings.ViewModels; using Orchard.Localization.Services; +using Orchard.Logging; using Orchard.Settings; using System; using Orchard.Security; @@ -16,16 +18,24 @@ namespace Orchard.Core.Settings.Drivers { private readonly ISiteService _siteService; private readonly ICultureManager _cultureManager; private readonly IMembershipService _membershipService; + private readonly INotifier _notifier; - public SiteSettingsPartDriver(ISiteService siteService, ICultureManager cultureManager, IMembershipService membershipService, INotifier notifier) { + public SiteSettingsPartDriver( + ISiteService siteService, + ICultureManager cultureManager, + IMembershipService membershipService, + INotifier notifier) { _siteService = siteService; _cultureManager = cultureManager; _membershipService = membershipService; + _notifier = notifier; T = NullLocalizer.Instance; + Logger = NullLogger.Instance; } public Localizer T { get; set; } + public ILogger Logger { get; set; } protected override string Prefix { get { return "SiteSettings"; } } @@ -48,6 +58,8 @@ namespace Orchard.Core.Settings.Drivers { SiteCultures = _cultureManager.ListCultures() }; + var previousBaseUrl = model.Site.BaseUrl; + updater.TryUpdateModel(model, Prefix, null, null); // ensures the super user is fully empty @@ -62,6 +74,27 @@ namespace Orchard.Core.Settings.Drivers { } } + + // ensure the base url is absolute if provided + if (!String.IsNullOrWhiteSpace(model.Site.BaseUrl)) { + if (!model.Site.BaseUrl.ToLower().StartsWith("http")) { + updater.AddModelError("BaseUrl", T("The base url must be absolute.")); + } + // if the base url has been modified, try to ping it + else if (!String.Equals(previousBaseUrl, model.Site.BaseUrl, StringComparison.OrdinalIgnoreCase)) { + try { + var request = WebRequest.Create(model.Site.BaseUrl) as HttpWebRequest; + if (request != null) { + using (request.GetResponse() as HttpWebResponse) {} + } + } + catch (Exception e) { + _notifier.Warning(T("The base url you entered could not be requested from current location.")); + Logger.Warning(e, "Could not query base url: {0}", model.Site.BaseUrl); + } + } + } + return ContentShape("Parts_Settings_SiteSettingsPart", () => shapeHelper.EditorTemplate(TemplateName: "Parts.Settings.SiteSettingsPart", Model: model, Prefix: Prefix)); } diff --git a/src/Orchard.Web/Core/Settings/Migrations.cs b/src/Orchard.Web/Core/Settings/Migrations.cs index 57d45a144..8af6255f5 100644 --- a/src/Orchard.Web/Core/Settings/Migrations.cs +++ b/src/Orchard.Web/Core/Settings/Migrations.cs @@ -95,5 +95,14 @@ namespace Orchard.Core.Settings { return 1; } + + public int UpdateFrom1() { + SchemaBuilder.AlterTable("SiteSettingsPartRecord", + table => table + .AddColumn("BaseUrl", c => c.WithLength(255)) + ); + + return 2; + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Settings/Models/SiteSettingsPart.cs b/src/Orchard.Web/Core/Settings/Models/SiteSettingsPart.cs index 3e5094699..46beb3444 100644 --- a/src/Orchard.Web/Core/Settings/Models/SiteSettingsPart.cs +++ b/src/Orchard.Web/Core/Settings/Models/SiteSettingsPart.cs @@ -1,4 +1,5 @@ -using Orchard.ContentManagement; +using System.ComponentModel.DataAnnotations; +using Orchard.ContentManagement; using Orchard.Settings; namespace Orchard.Core.Settings.Models { @@ -42,5 +43,11 @@ namespace Orchard.Core.Settings.Models { get { return Record.PageSize; } set { Record.PageSize = value; } } + + [StringLength(255)] + public string BaseUrl { + get { return Record.BaseUrl; } + set { Record.BaseUrl = value; } + } } } diff --git a/src/Orchard.Web/Core/Settings/Models/SiteSettingsPartRecord.cs b/src/Orchard.Web/Core/Settings/Models/SiteSettingsPartRecord.cs index eb87877f5..7eb2764e0 100644 --- a/src/Orchard.Web/Core/Settings/Models/SiteSettingsPartRecord.cs +++ b/src/Orchard.Web/Core/Settings/Models/SiteSettingsPartRecord.cs @@ -1,4 +1,5 @@ -using Orchard.ContentManagement.Records; +using System.ComponentModel.DataAnnotations; +using Orchard.ContentManagement.Records; using Orchard.Settings; namespace Orchard.Core.Settings.Models { @@ -24,5 +25,8 @@ namespace Orchard.Core.Settings.Models { public virtual ResourceDebugMode ResourceDebugMode { get; set; } public virtual int PageSize { get; set; } + + [StringLength(255)] + public virtual string BaseUrl { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Settings/ViewModels/SiteSettingsPartViewModel.cs b/src/Orchard.Web/Core/Settings/ViewModels/SiteSettingsPartViewModel.cs index 8bbb87d40..e140e8841 100644 --- a/src/Orchard.Web/Core/Settings/ViewModels/SiteSettingsPartViewModel.cs +++ b/src/Orchard.Web/Core/Settings/ViewModels/SiteSettingsPartViewModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Web.Mvc; -using Orchard.ContentManagement; using Orchard.Core.Settings.Models; using Orchard.Settings; @@ -8,7 +7,6 @@ namespace Orchard.Core.Settings.ViewModels { public class SiteSettingsPartViewModel { public SiteSettingsPart Site { get; set; } public IEnumerable SiteCultures { get; set; } - [HiddenInput(DisplayValue = false)] public int Id { @@ -16,33 +14,38 @@ namespace Orchard.Core.Settings.ViewModels { } public string PageTitleSeparator { - get { return Site.Record.PageTitleSeparator; } - set { Site.Record.PageTitleSeparator = value; } + get { return Site.PageTitleSeparator; } + set { Site.PageTitleSeparator = value; } } public string SiteName { - get { return Site.Record.SiteName; } - set { Site.Record.SiteName = value; } + get { return Site.SiteName; } + set { Site.SiteName = value; } } public string SiteCulture { - get { return Site.Record.SiteCulture; } - set { Site.Record.SiteCulture = value; } + get { return Site.SiteCulture; } + set { Site.SiteCulture = value; } } public string SuperUser { - get { return Site.As().Record.SuperUser; } - set { Site.As().Record.SuperUser = value; } + get { return Site.SuperUser; } + set { Site.SuperUser = value; } } public ResourceDebugMode ResourceDebugMode { - get { return Site.As().ResourceDebugMode; } - set { Site.As().ResourceDebugMode = value; } + get { return Site.ResourceDebugMode; } + set { Site.ResourceDebugMode = value; } } public int PageSize { - get { return Site.As().PageSize; } - set { Site.As().PageSize = value; } + get { return Site.PageSize; } + set { Site.PageSize = value; } + } + + public string BaseUrl { + get { return Site.BaseUrl; } + set { Site.BaseUrl = value; } } } } diff --git a/src/Orchard.Web/Core/Settings/Views/EditorTemplates/Parts.Settings.SiteSettingsPart.cshtml b/src/Orchard.Web/Core/Settings/Views/EditorTemplates/Parts.Settings.SiteSettingsPart.cshtml index e8d36de86..027ace416 100644 --- a/src/Orchard.Web/Core/Settings/Views/EditorTemplates/Parts.Settings.SiteSettingsPart.cshtml +++ b/src/Orchard.Web/Core/Settings/Views/EditorTemplates/Parts.Settings.SiteSettingsPart.cshtml @@ -38,7 +38,13 @@
- @Html.TextBoxFor(m => m.PageSize, new { @class = "textMedium" }) + @Html.TextBoxFor(m => m.PageSize, new { @class = "text-small" }) @T("Determines the default number of items that are shown per page.")
+
+ + @Html.TextBoxFor(m => m.BaseUrl, new { @class = "textMedium" }) + @T("Enter the fully qualified base url of your website.") + @T("e.g., http://localhost:30320/orchardlocal, http://www.yourdomain.com") +
\ No newline at end of file diff --git a/src/Orchard.Web/Global.asax.cs b/src/Orchard.Web/Global.asax.cs index 996bc4420..07f80f67e 100644 --- a/src/Orchard.Web/Global.asax.cs +++ b/src/Orchard.Web/Global.asax.cs @@ -1,35 +1,106 @@ -using System.Web; +using System; +using System.IO; +using System.Threading; +using System.Web; +using System.Web.Hosting; using System.Web.Mvc; using System.Web.Routing; using Autofac; using Orchard.Environment; +using Orchard.Environment.Warmup; +using Orchard.Utility.Extensions; namespace Orchard.Web { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : HttpApplication { - private static IOrchardHost _host; + private static StartupResult _startupResult; + private static EventWaitHandle _waitHandle; public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); } protected void Application_Start() { - RegisterRoutes(RouteTable.Routes); + LaunchStartupThread(); + } - _host = OrchardStarter.CreateHost(MvcSingletons); - _host.Initialize(); + /// + /// Initializes Orchard's Host in a separate thread + /// + private static void LaunchStartupThread() { + _startupResult = new StartupResult(); + _waitHandle = new AutoResetEvent(false); + + ThreadPool.QueueUserWorkItem( + state => { + try { + RegisterRoutes(RouteTable.Routes); + var host = OrchardStarter.CreateHost(MvcSingletons); + host.Initialize(); + _startupResult.Host = host; + } + catch (Exception e) { + _startupResult.Error = e; + } + finally { + _waitHandle.Set(); + } + }); } protected void Application_BeginRequest() { - Context.Items["originalHttpContext"] = Context; + // Host is still starting up? + if (_startupResult.Host == null && _startupResult.Error == null) { - _host.BeginRequest(); + // use the url as it was requested by the client + // the real url might be different if it has been translated (proxy, load balancing, ...) + var url = Request.ToUrlString(); + var virtualFileCopy = "~/App_Data/WarmUp/" + WarmupUtility.EncodeUrl(url.Trim('/')); + var localCopy = HostingEnvironment.MapPath(virtualFileCopy); + + if (File.Exists(localCopy)) { + + // result should not be cached, even on proxies + Context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); + Context.Response.Cache.SetValidUntilExpires(false); + Context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + Context.Response.Cache.SetCacheability(HttpCacheability.NoCache); + Context.Response.Cache.SetNoStore(); + + Context.Response.ContentType = "text/html"; + + Context.Response.WriteFile(localCopy); + Context.Response.End(); + } + else { + // there is no local copy and the host is not running + // wait for the host to initialize + _waitHandle.WaitOne(); + } + } + else { + if (_startupResult.Error != null) { + // Host startup resulted in an error + + // Throw error once, and restart launch (machine state may have changed + // so we need to simulate a "restart". + var error = _startupResult.Error; + LaunchStartupThread(); + throw error; + } + + Context.Items["originalHttpContext"] = Context; + _startupResult.Host.BeginRequest(); + } } protected void Application_EndRequest() { - _host.EndRequest(); + // Only notify if the host has started up + if (_startupResult.Host != null) { + _startupResult.Host.EndRequest(); + } } static void MvcSingletons(ContainerBuilder builder) { @@ -37,6 +108,5 @@ namespace Orchard.Web { builder.Register(ctx => ModelBinders.Binders).SingleInstance(); builder.Register(ctx => ViewEngines.Engines).SingleInstance(); } - } } diff --git a/src/Orchard.Web/Modules/Orchard.Setup/SetupMode.cs b/src/Orchard.Web/Modules/Orchard.Setup/SetupMode.cs index 117178e41..8e3bf32d0 100644 --- a/src/Orchard.Web/Modules/Orchard.Setup/SetupMode.cs +++ b/src/Orchard.Web/Modules/Orchard.Setup/SetupMode.cs @@ -182,6 +182,10 @@ namespace Orchard.Setup { get { return SiteSettingsPartRecord.DefaultPageSize; } set { throw new NotImplementedException(); } } + + public string BaseUrl { + get { return ""; } + } } } } diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.Warmup/AdminMenu.cs new file mode 100644 index 000000000..006cb26e0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/AdminMenu.cs @@ -0,0 +1,17 @@ +using Orchard.Localization; +using Orchard.Security; +using Orchard.UI.Navigation; + +namespace Orchard.Warmup { + public class AdminMenu : INavigationProvider { + public Localizer T { get; set; } + public string MenuName { get { return "admin"; } } + + public void GetNavigation(NavigationBuilder builder) { + builder + .Add(T("Settings"), menu => menu + .Add(T("Warmup" ), "10.0", item => item.Action("Index", "Admin", new { area = "Orchard.Warmup" }).Permission(StandardPermissions.SiteOwner)) + ); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Commands/WarmupCommands.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Commands/WarmupCommands.cs new file mode 100644 index 000000000..16ebaaf9e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Commands/WarmupCommands.cs @@ -0,0 +1,29 @@ +using Orchard.Commands; +using Orchard.Warmup.Services; + +namespace Orchard.Warmup.Commands { + public class WarmupCommands : DefaultOrchardCommandHandler { + private readonly IWarmupUpdater _warmupUpdater; + + [OrchardSwitch] + public bool Force { get; set; } + + public WarmupCommands(IWarmupUpdater warmupUpdater) { + _warmupUpdater = warmupUpdater; + } + + [CommandName("warmup generate")] + [CommandHelp("warmup generate [/Force:true] \r\n\t Generates all the static pages for the warmup feature.")] + [OrchardSwitches("Force")] + public string Generate() { + if(Force) { + _warmupUpdater.Generate(); + } + else { + _warmupUpdater.EnsureGenerate(); + } + + return "Generation finished"; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Controllers/AdminController.cs new file mode 100644 index 000000000..d5abdeb6e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Controllers/AdminController.cs @@ -0,0 +1,78 @@ +using System.Web.Mvc; +using Orchard.ContentManagement; +using Orchard.Core.Contents.Controllers; +using Orchard.Localization; +using Orchard.Security; +using Orchard.Warmup.Models; +using Orchard.UI.Notify; +using Orchard.Warmup.Services; + +namespace Orchard.Warmup.Controllers { + public class AdminController : Controller, IUpdateModel { + private readonly IWarmupUpdater _warmupUpdater; + + public AdminController(IOrchardServices services, IWarmupUpdater warmupUpdater) { + _warmupUpdater = warmupUpdater; + Services = services; + + T = NullLocalizer.Instance; + } + + public IOrchardServices Services { get; set; } + public Localizer T { get; set; } + + public ActionResult Index() { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage settings"))) + return new HttpUnauthorizedResult(); + + var warmupPart = Services.WorkContext.CurrentSite.As(); + return View(warmupPart); + } + + [FormValueRequired("submit")] + [HttpPost, ActionName("Index")] + public ActionResult IndexPost() { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage settings"))) + return new HttpUnauthorizedResult(); + + var warmupPart = Services.WorkContext.CurrentSite.As(); + + if(TryUpdateModel(warmupPart)) { + Services.Notifier.Information(T("Warmup updated successfully.")); + } + + if (warmupPart.Scheduled) { + if (warmupPart.Delay <= 0) { + AddModelError("Delay", T("Delay must be greater than zero.")); + } + } + + return View(warmupPart); + } + + [FormValueRequired("submit.Generate")] + [HttpPost, ActionName("Index")] + public ActionResult IndexPostGenerate() { + var result = IndexPost(); + + if (ModelState.IsValid) { + _warmupUpdater.Generate(); + Services.Notifier.Information(T("Static pages have been generated.")); + } + + return result; + } + + bool IUpdateModel.TryUpdateModel(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) { + return TryUpdateModel(model, prefix, includeProperties, excludeProperties); + } + + void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) { + ModelState.AddModelError(key, errorMessage.ToString()); + } + + public void AddModelError(string key, LocalizedString errorMessage) { + ModelState.AddModelError(key, errorMessage.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Handlers/WarmupContentHandler.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Handlers/WarmupContentHandler.cs new file mode 100644 index 000000000..27544c9f9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Handlers/WarmupContentHandler.cs @@ -0,0 +1,28 @@ +using Orchard.ContentManagement.Handlers; +using Orchard.ContentManagement; +using Orchard.Warmup.Models; +using Orchard.Warmup.Services; + +namespace Orchard.Warmup.Handlers { + /// + /// Intercepts the ContentHandler events to create warmup static pages + /// whenever some content is published + /// + public class WarmupContentHandler : ContentHandler { + private readonly IOrchardServices _orchardServices; + private readonly IWarmupUpdater _warmupUpdater; + + public WarmupContentHandler(IOrchardServices orchardServices, IWarmupUpdater warmupUpdater) { + _orchardServices = orchardServices; + _warmupUpdater = warmupUpdater; + + OnPublished(Generate); + } + + void Generate(PublishContentContext context, ContentPart part) { + if(_orchardServices.WorkContext.CurrentSite.As().OnPublish) { + _warmupUpdater.Generate(); + } + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Handlers/WarmupSettingsPartHandler.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Handlers/WarmupSettingsPartHandler.cs new file mode 100644 index 000000000..37456d4bc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Handlers/WarmupSettingsPartHandler.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; +using Orchard.Warmup.Models; + +namespace Orchard.Warmup.Handlers { + [UsedImplicitly] + public class WarmupSettingsPartHandler : ContentHandler { + public WarmupSettingsPartHandler(IRepository repository) { + Filters.Add(new ActivatingFilter("Site")); + Filters.Add(StorageFilter.For(repository)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Migrations.cs new file mode 100644 index 000000000..c12b4ee63 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Migrations.cs @@ -0,0 +1,18 @@ +using Orchard.Data.Migration; + +namespace Orchard.Warmup { + public class Migrations : DataMigrationImpl { + public int Create() { + SchemaBuilder.CreateTable("WarmupSettingsPartRecord", + table => table + .ContentPartRecord() + .Column("Urls", column => column.Unlimited()) + .Column("Scheduled") + .Column("Delay") + .Column("OnPublish") + ); + + return 1; + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Models/WarmupSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Models/WarmupSettingsPart.cs new file mode 100644 index 000000000..fab011abe --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Models/WarmupSettingsPart.cs @@ -0,0 +1,26 @@ +using Orchard.ContentManagement; + +namespace Orchard.Warmup.Models { + public class WarmupSettingsPart : ContentPart { + + public string Urls { + get { return Record.Urls; } + set { Record.Urls = value; } + } + + public bool Scheduled { + get { return Record.Scheduled; } + set { Record.Scheduled = value; } + } + + public int Delay { + get { return Record.Delay; } + set { Record.Delay = value; } + } + + public bool OnPublish { + get { return Record.OnPublish; } + set { Record.OnPublish = value; } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Models/WarmupSettingsPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Models/WarmupSettingsPartRecord.cs new file mode 100644 index 000000000..410c7a6bd --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Models/WarmupSettingsPartRecord.cs @@ -0,0 +1,16 @@ +using Orchard.ContentManagement.Records; +using Orchard.Data.Conventions; + +namespace Orchard.Warmup.Models { + public class WarmupSettingsPartRecord : ContentPartRecord { + public WarmupSettingsPartRecord() { + Delay = 90; + } + + [StringLengthMax] + public virtual string Urls { get; set; } + public virtual bool Scheduled { get; set; } + public virtual int Delay { get; set; } + public virtual bool OnPublish { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Module.txt b/src/Orchard.Web/Modules/Orchard.Warmup/Module.txt new file mode 100644 index 000000000..1fe630fc7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Module.txt @@ -0,0 +1,12 @@ +Name: Orchard.Warmup +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.0 +OrchardVersion: 1.0 +Description: Provides a mecanism to generate a static version of pages for being used during application warm up. +Features: + Orchard.Warmup: + Description: Generates the static version of specific pages periodically. + Name: Warmup + Category: Hosting \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Orchard.Warmup.csproj b/src/Orchard.Web/Modules/Orchard.Warmup/Orchard.Warmup.csproj new file mode 100644 index 000000000..e9be6c5f1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Orchard.Warmup.csproj @@ -0,0 +1,142 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.Warmup + Orchard.Warmup + v4.0 + false + + + 3.5 + + + false + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + 3.5 + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Placement.info b/src/Orchard.Web/Modules/Orchard.Warmup/Placement.info new file mode 100644 index 000000000..dc7ef4eab --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Placement.info @@ -0,0 +1,3 @@ + + + diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b655c3bbb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Warmup")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3484b5d3-de81-4a46-bfa6-c1d02a029184")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.Warmup/Scripts/Web.config new file mode 100644 index 000000000..178ff35ba --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Scripts/Web.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Services/IWarmupUpdater.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Services/IWarmupUpdater.cs new file mode 100644 index 000000000..c133d56b8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Services/IWarmupUpdater.cs @@ -0,0 +1,13 @@ +namespace Orchard.Warmup.Services { + public interface IWarmupUpdater : IDependency { + /// + /// Forces a regeneration of all static pages + /// + void Generate(); + + /// + /// Generates static pages if needed + /// + void EnsureGenerate(); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Services/IWebDownloader.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Services/IWebDownloader.cs new file mode 100644 index 000000000..6911494db --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Services/IWebDownloader.cs @@ -0,0 +1,13 @@ + +using System.Net; + +namespace Orchard.Warmup.Services { + public class DownloadResult { + public HttpStatusCode StatusCode { get; set; } + public string Content { get; set; } + } + + public interface IWebDownloader : IDependency { + DownloadResult Download(string url); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Services/SettingsBanner.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Services/SettingsBanner.cs new file mode 100644 index 000000000..b51da54f0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Services/SettingsBanner.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Orchard.ContentManagement; +using Orchard.Environment.Configuration; +using Orchard.Localization; +using Orchard.UI.Admin.Notification; +using Orchard.UI.Notify; + +namespace Orchard.Warmup.Services { + public class SettingsBanner: INotificationProvider { + private readonly IOrchardServices _orchardServices; + + public SettingsBanner(IOrchardServices orchardServices) { + _orchardServices = orchardServices; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public IEnumerable GetNotifications() { + if ( string.IsNullOrWhiteSpace(_orchardServices.WorkContext.CurrentSite.BaseUrl)) { + yield return new NotifyEntry { Message = T("The Warmup feature needs the Base Url site setting to be set." ), Type = NotifyType.Warning }; + } + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Services/WarmupTask.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Services/WarmupTask.cs new file mode 100644 index 000000000..549ea5199 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Services/WarmupTask.cs @@ -0,0 +1,25 @@ +using Orchard.ContentManagement; +using Orchard.Tasks; +using Orchard.Warmup.Models; + +namespace Orchard.Warmup.Services { + public class WarmupTask : IBackgroundTask { + private readonly IOrchardServices _orchardServices; + private readonly IWarmupUpdater _warmupUpdater; + + public WarmupTask(IOrchardServices orchardServices, IWarmupUpdater warmupUpdater) { + _orchardServices = orchardServices; + _warmupUpdater = warmupUpdater; + } + + public void Sweep() { + var part = _orchardServices.WorkContext.CurrentSite.As(); + + if (!part.Scheduled) { + return; + } + + _warmupUpdater.EnsureGenerate(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Services/WarmupUpdater.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Services/WarmupUpdater.cs new file mode 100644 index 000000000..580bee76b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Services/WarmupUpdater.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using System.Net; +using System.Web; +using System.Xml; +using Orchard.ContentManagement; +using Orchard.Environment.Warmup; +using Orchard.FileSystems.AppData; +using Orchard.FileSystems.LockFile; +using Orchard.Logging; +using Orchard.Services; +using Orchard.Warmup.Models; + +namespace Orchard.Warmup.Services { + public class WarmupUpdater : IWarmupUpdater { + private readonly IOrchardServices _orchardServices; + private readonly ILockFileManager _lockFileManager; + private readonly IClock _clock; + private readonly IAppDataFolder _appDataFolder; + private readonly IWebDownloader _webDownloader; + private const string BaseFolder = "Warmup"; + private const string WarmupFilename = "warmup.txt"; + private readonly string _lockFilename; + + public WarmupUpdater( + IOrchardServices orchardServices, + ILockFileManager lockFileManager, + IClock clock, + IAppDataFolder appDataFolder, + IWebDownloader webDownloader) { + _orchardServices = orchardServices; + _lockFileManager = lockFileManager; + _clock = clock; + _appDataFolder = appDataFolder; + _webDownloader = webDownloader; + _lockFilename = _appDataFolder.Combine(BaseFolder, WarmupFilename + ".lock"); + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void EnsureGenerate() { + var baseUrl = _orchardServices.WorkContext.CurrentSite.BaseUrl; + var part = _orchardServices.WorkContext.CurrentSite.As(); + + // do nothing while the base url setting is not defined, or if there is no page defined + if (String.IsNullOrWhiteSpace(baseUrl) || String.IsNullOrWhiteSpace(part.Urls)) { + return; + } + + // prevent multiple appdomains from rebuilding the static page concurrently (e.g., command line) + ILockFile lockFile = null; + if (!_lockFileManager.TryAcquireLock(_lockFilename, ref lockFile)) { + return; + } + + using (lockFile) { + + // check if we need to regenerate the pages by reading the last time it has been done + // 1- if the warmup file doesn't exists, generate the pages + // 2- otherwise, if the scheduled generation option is on, check if the delay is over + var warmupPath = _appDataFolder.Combine(BaseFolder, WarmupFilename); + if(_appDataFolder.FileExists(warmupPath)) { + try { + var warmupContent = _appDataFolder.ReadFile(warmupPath); + var expired = XmlConvert.ToDateTimeOffset(warmupContent).AddMinutes(part.Delay); + if (expired > _clock.UtcNow) { + return; + } + } + catch { + // invalid file, delete continue processing + _appDataFolder.DeleteFile(warmupPath); + } + } + + // delete existing static page files + foreach (var filename in _appDataFolder.ListFiles(BaseFolder)) { + var prefix = _appDataFolder.Combine(BaseFolder, "http"); + + // delete only static page files + if (!filename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { + continue; + } + + try { + _appDataFolder.DeleteFile(filename); + } + catch(Exception e) { + // ignore files which could not be deleted + Logger.Error(e, "Could not delete file {0}", filename); + } + } + + // loop over every relative url to generate the contents + using (var urlReader = new StringReader(part.Urls)) { + string relativeUrl; + while (null != (relativeUrl = urlReader.ReadLine())) { + string url = null; + relativeUrl = relativeUrl.Trim(); + + try { + url = VirtualPathUtility.RemoveTrailingSlash(baseUrl) + relativeUrl; + var download = _webDownloader.Download(url); + + if (download != null && download.StatusCode == HttpStatusCode.OK) { + var filename = WarmupUtility.EncodeUrl(url); + var path = _appDataFolder.Combine(BaseFolder, filename); + _appDataFolder.CreateFile(path, download.Content); + + // if the base url contains http://www, then also render the www-less one + + if (url.StartsWith("http://www.", StringComparison.OrdinalIgnoreCase)) { + url = "http://" + url.Substring("http://www.".Length); + filename = WarmupUtility.EncodeUrl(url); + path = _appDataFolder.Combine(BaseFolder, filename); + _appDataFolder.CreateFile(path, download.Content); + } + + } + } + catch (Exception e) { + Logger.Error(e, "Could not extract warmup page content for: ", url); + } + } + } + + // finally write the time the generation has been executed + _appDataFolder.CreateFile(warmupPath, XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc)); + } + } + + public void Generate() { + // prevent multiple appdomains from rebuilding the static page concurrently (e.g., command line) + ILockFile lockFile = null; + if (!_lockFileManager.TryAcquireLock(_lockFilename, ref lockFile)) { + return; + } + + using (lockFile) { + var warmupPath = _appDataFolder.Combine(BaseFolder, WarmupFilename); + if (_appDataFolder.FileExists(warmupPath)) { + _appDataFolder.DeleteFile(warmupPath); + } + } + + EnsureGenerate(); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Services/WebDownloader.cs b/src/Orchard.Web/Modules/Orchard.Warmup/Services/WebDownloader.cs new file mode 100644 index 000000000..012089c03 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Services/WebDownloader.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Net; +using Orchard.Logging; + +namespace Orchard.Warmup.Services { + public class WebDownloader : IWebDownloader { + public WebDownloader() { + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public DownloadResult Download(string url) { + if(String.IsNullOrWhiteSpace(url)) { + return null; + } + + try { + var request = WebRequest.Create(url) as HttpWebRequest; + if (request != null) { + using (var response = request.GetResponse() as HttpWebResponse) { + if (response != null) { + using (var stream = response.GetResponseStream()) { + if (stream != null) { + using (var sr = new StreamReader(stream)) { + return new DownloadResult {Content = sr.ReadToEnd(), StatusCode = response.StatusCode}; + } + } + } + } + } + } + return null; + } + catch (WebException e) { + if(e.Response as HttpWebResponse != null) { + return new DownloadResult { StatusCode = ((HttpWebResponse)e.Response).StatusCode }; + } + + return null; + } + catch(Exception e) { + Logger.Error(e, "An error occured while downloading url: {0}", url); + return null; + } + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.Warmup/Styles/Web.config new file mode 100644 index 000000000..178ff35ba --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Styles/Web.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.Warmup/Views/Admin/Index.cshtml new file mode 100644 index 000000000..a0d0fd5bd --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Views/Admin/Index.cshtml @@ -0,0 +1,37 @@ +@model Orchard.Warmup.Models.WarmupSettingsPart +@using Orchard.Utility.Extensions; +@using Orchard.Warmup.Models; + +@{ Layout.Title = T("Warmup").ToString(); } + +@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() + +
+
+ + @Html.TextAreaFor(m => m.Urls, new { @class = "textMedium" }) + @T("This must be a set of relative paths, e.g., /, /About") +
+
+
+
+ @Html.EditorFor(m => m.Scheduled) + +
+
+ @T("Every") + @Html.TextBoxFor(m => m.Delay, new { @class = "text-small" }) + @T("minutes") + @Html.ValidationMessage("Delay", "*") +
+
+ @Html.EditorFor(m => m.OnPublish) + +
+
+
+ + +
+} diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Views/EditorTemplates/Parts.Warmup.SiteSettings.cshtml b/src/Orchard.Web/Modules/Orchard.Warmup/Views/EditorTemplates/Parts.Warmup.SiteSettings.cshtml new file mode 100644 index 000000000..6fb7c7a40 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Views/EditorTemplates/Parts.Warmup.SiteSettings.cshtml @@ -0,0 +1,28 @@ +@model Orchard.Warmup.Models.WarmupSettingsPartRecord +@using Orchard.Utility.Extensions; +@using Orchard.Warmup.Models; + +
+ @T("Warmup") + +
+ + @Html.TextAreaFor(m => m.Urls, new { @class = "textMedium" }) + @Html.ValidationMessage("Urls", "*") + @T("This must be a set of virtual paths, e.g., ~/, ~/About") +
+ +
+ @Html.EditorFor(m => m.Scheduled) + +
+
m.Urls)">@T("Delay to generate pages") + @Html.TextBoxFor(m => m.Delay, new { @class = "" }) @T("minutes") + @Html.ValidationMessage("Delay", "*") +
+
+ @Html.EditorFor(m => m.OnPublish) + +
+
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Views/Web.config b/src/Orchard.Web/Modules/Orchard.Warmup/Views/Web.config new file mode 100644 index 000000000..b7d215131 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Views/Web.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Warmup/Web.config b/src/Orchard.Web/Modules/Orchard.Warmup/Web.config new file mode 100644 index 000000000..5884c5879 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Warmup/Web.config @@ -0,0 +1,39 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.jQuery/Orchard.jQuery.csproj b/src/Orchard.Web/Modules/Orchard.jQuery/Orchard.jQuery.csproj index 748ab92fa..058377aa9 100644 --- a/src/Orchard.Web/Modules/Orchard.jQuery/Orchard.jQuery.csproj +++ b/src/Orchard.Web/Modules/Orchard.jQuery/Orchard.jQuery.csproj @@ -18,6 +18,7 @@ 3.5 + false true @@ -48,11 +49,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Orchard.Web.csproj b/src/Orchard.Web/Orchard.Web.csproj index ec7b050fa..db45cf2f6 100644 --- a/src/Orchard.Web/Orchard.Web.csproj +++ b/src/Orchard.Web/Orchard.Web.csproj @@ -18,6 +18,7 @@ 3.5 + true true @@ -199,8 +200,7 @@ False 30320 /OrchardLocal - - + http://localhost:30320/OrchardLocal False False diff --git a/src/Orchard.sln b/src/Orchard.sln index 8d35901cb..57da0789e 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -112,6 +112,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Recipes", "Orchard. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.ImportExport", "Orchard.Web\Modules\Orchard.ImportExport\Orchard.ImportExport.csproj", "{FE5C5947-D2D5-42C5-992A-13D672946135}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Warmup", "Orchard.Web\Modules\Orchard.WarmUp\Orchard.Warmup.csproj", "{9CD5C81F-5828-4384-8474-2E2BE71D5EDD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeCoverage|Any CPU = CodeCoverage|Any CPU @@ -595,6 +597,13 @@ Global {FE5C5947-D2D5-42C5-992A-13D672946135}.FxCop|Any CPU.Build.0 = Release|Any CPU {FE5C5947-D2D5-42C5-992A-13D672946135}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE5C5947-D2D5-42C5-992A-13D672946135}.Release|Any CPU.Build.0 = Release|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -634,6 +643,7 @@ Global {43D0EC0B-1955-4566-8D31-7B9102DA1703} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {FC1D74E8-7A4D-48F4-83DE-95C6173780C4} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {FE5C5947-D2D5-42C5-992A-13D672946135} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {9CD5C81F-5828-4384-8474-2E2BE71D5EDD} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} diff --git a/src/Orchard/Environment/Warmup/StartupResult.cs b/src/Orchard/Environment/Warmup/StartupResult.cs new file mode 100644 index 000000000..0a1400f90 --- /dev/null +++ b/src/Orchard/Environment/Warmup/StartupResult.cs @@ -0,0 +1,8 @@ +using System; + +namespace Orchard.Environment.Warmup { + public class StartupResult { + public IOrchardHost Host { get; set; } + public Exception Error { get; set; } + } +} diff --git a/src/Orchard/Environment/Warmup/WarmupUtility.cs b/src/Orchard/Environment/Warmup/WarmupUtility.cs new file mode 100644 index 000000000..e13003fd4 --- /dev/null +++ b/src/Orchard/Environment/Warmup/WarmupUtility.cs @@ -0,0 +1,19 @@ +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Orchard.Environment.Warmup { + public static class WarmupUtility { + private const string EncodingPattern = "[^a-z0-9]"; + + public static string EncodeUrl(string url) { + if(String.IsNullOrWhiteSpace(url)) { + throw new ArgumentException("url can't be empty"); + } + + return Regex.Replace(url.ToLower(), EncodingPattern, m => "_" + Encoding.UTF8.GetBytes(m.Value).Select(b => b.ToString("X")).Aggregate((a, b) => a + b)); + } + + } +} diff --git a/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs b/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs index 117f6f03a..30198072c 100644 --- a/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs +++ b/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs @@ -58,8 +58,7 @@ namespace Orchard.FileSystems.LockFile { var content = _appDataFolder.ReadFile(path); DateTime creationUtc; - if (DateTime.TryParse(content, out creationUtc)) - { + if (DateTime.TryParse(content, out creationUtc)) { // if expired the file is not removed // it should be automatically as there is a finalizer in LockFile // or the next taker can do it, unless it also fails, again diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 4979e9f29..cd1120190 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -183,6 +183,8 @@ + + diff --git a/src/Orchard/Settings/ISite.cs b/src/Orchard/Settings/ISite.cs index f215673e7..e37d47e63 100644 --- a/src/Orchard/Settings/ISite.cs +++ b/src/Orchard/Settings/ISite.cs @@ -13,5 +13,6 @@ namespace Orchard.Settings { string SiteCulture { get; set; } ResourceDebugMode ResourceDebugMode { get; set; } int PageSize { get; set; } + string BaseUrl { get; } } }