diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4966d1f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Bjarke Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a296d35 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Page Not Found Manager + +Page not found + +[![NuGet](https://img.shields.io/nuget/v/HotChilli.Umbraco.PageNotFound.svg)](https://www.nuget.org/packages/HotChilli.Umbraco.PageNotFound/) +[![Our Umbraco](https://img.shields.io/badge/our-umbraco-orange.svg)](https://our.umbraco.com/packages/backoffice-extensions/hot-chilli-page-not-found-manager/) + +## Getting Started + +**This is currently a beta package** + +Page Not Found Manager supports Umbraco v9 RC1+. + +Umbraco v9 Packages are only available via Nuget, although there is a page on [Our.Umbraco.com](https://our.umbraco.com/packages/backoffice-extensions/hot-chilli-page-not-found-manager/) to aid discoverability. + +## Installation + +To [install from NuGet](https://www.nuget.org/packages/HotChilli.Umbraco.PageNotFound/), run the following command in your instance of Visual Studio. + + PM> Install-Package HotChilli.Umbraco.PageNotFound + +## Credits + +The logo uses [404](https://thenounproject.com/term/404/3283006 ) from the [Noun Project](https://thenounproject.com) by [Ilham Fitrotul Hayat](https://thenounproject.com/fhilham), licensed under [CC BY 3.0 US](https://creativecommons.org/licenses/by/3.0/us/). diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000..e9ec9b1 Binary files /dev/null and b/docs/img/logo.png differ diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..64d5187 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,41 @@ +# Ignore Umbraco files/folders generated for each instance +## +## Get latest from https://github.com/github/gitignore/blob/master/Umbraco.gitignore + +# Note: VisualStudio gitignore rules may also be relevant + +# Umbraco +# Ignore unimportant folders generated by Umbraco +**/App_Data/Logs/ +**/App_Data/[Pp]review/ +**/App_Data/TEMP/ +**/App_Data/NuGetBackup/ + +# Ignore Umbraco content cache file +**/App_Data/umbraco.config + +## this [Uu]mbraco/ folder should be created by cmd like `Install-Package UmbracoCms -Version 8.5.3` +## you can find your Umbraco version in your Web.config. (i.e. ) +## Uncomment this line if you think it fits the way you work on your project. +## **/[Uu]mbraco/ + +# Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder) +# Make sure to include details from VisualStudio.gitignore BEFORE this +!**/App_Data/[Pp]ackages/* +!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages/* +!**/[Uu]mbraco/[Vv]iews/[Pp]ackages/* + +# ImageProcessor DiskCache +**/App_Data/cache/ + +# Ignore the Models Builder models out of date flag +**/ood.flag + +# NEW for version 9 .Net 5 (Core) +# Umbraco 9 now stores it's unimportant folders within the umbraco directory +**/[Uu]mbraco/Data/* +# we want to keep the packages directory +!**/[Uu]mbraco/Data/[Pp]ackages/* +**/[Uu]mbraco/Logs/* +**/[Uu]mbraco/mediacache/* +**/wwwroot/umbraco/* \ No newline at end of file diff --git a/src/HCS.PageNotFoundManager/Backoffice/MenuController.cs b/src/HC.PageNotFoundManager.Core/Backoffice/MenuController.cs similarity index 85% rename from src/HCS.PageNotFoundManager/Backoffice/MenuController.cs rename to src/HC.PageNotFoundManager.Core/Backoffice/MenuController.cs index dd7345a..9155bce 100644 --- a/src/HCS.PageNotFoundManager/Backoffice/MenuController.cs +++ b/src/HC.PageNotFoundManager.Core/Backoffice/MenuController.cs @@ -1,13 +1,13 @@ -using HCS.PageNotFoundManager.Core.Caching; -using HCS.PageNotFoundManager.Core.Config; -using HCS.PageNotFoundManager.Core.Models; +using HC.PageNotFoundManager.Core.Caching; +using HC.PageNotFoundManager.Core.Config; +using HC.PageNotFoundManager.Core.Models; using Microsoft.AspNetCore.Mvc; using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Attributes; -namespace HCS.PageNotFoundManager.Core.Backoffice +namespace HC.PageNotFoundManager.Core.Backoffice { [PluginController(Constants.BackOffice)] public class MenuController : UmbracoAuthorizedJsonController diff --git a/src/HC.PageNotFoundManager.Core/Backoffice/MenuRenderingNotificationHandler.cs b/src/HC.PageNotFoundManager.Core/Backoffice/MenuRenderingNotificationHandler.cs new file mode 100644 index 0000000..735eabb --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Backoffice/MenuRenderingNotificationHandler.cs @@ -0,0 +1,50 @@ +using HC.PageNotFoundManager.Core.Config; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Trees; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Security; +using Umbraco.Extensions; + +namespace HC.PageNotFoundManager.Core.Backoffice +{ + public class MenuRenderingNotificationHandler : INotificationHandler + { + private readonly List UserGroupNames; + private readonly IBackOfficeSecurityAccessor backOfficeSecurity; + + public MenuRenderingNotificationHandler(IOptions settings, IBackOfficeSecurityAccessor backOfficeSecurity) + { + if (settings is null) throw new System.ArgumentNullException(nameof(settings)); + + UserGroupNames = new List { Umbraco.Cms.Core.Constants.Security.AdminGroupAlias }; + if (settings.Value.UserGroups != null && settings.Value.UserGroups.Length > 0) + UserGroupNames.AddRange(settings.Value.UserGroups); + this.backOfficeSecurity = backOfficeSecurity ?? throw new System.ArgumentNullException(nameof(backOfficeSecurity)); + } + + public void Handle(MenuRenderingNotification notification) + { + if (notification.TreeAlias != Umbraco.Cms.Core.Constants.Trees.Content + && int.TryParse(notification.NodeId, out var nodeId) && nodeId <= 0) return; + + if (backOfficeSecurity.BackOfficeSecurity.CurrentUser == null) return; + + if (backOfficeSecurity.BackOfficeSecurity.CurrentUser.Groups == null || + backOfficeSecurity.BackOfficeSecurity.CurrentUser.Groups.All(g => !UserGroupNames.InvariantContains(g.Alias))) return; + + var menuItem = new MenuItem(Constants.MenuAlias, Constants.MenuLabel) + { + Icon = Umbraco.Cms.Core.Constants.Icons.Content, + SeparatorBefore = true + }; + + menuItem.LaunchDialogView("/App_Plugins/HC.PageNotFound/Backoffice/Dialogs/dialog.html", Constants.MenuLabel); + + notification.Menu.Items.Add(menuItem); + + } + } +} diff --git a/src/HC.PageNotFoundManager.Core/Backoffice/PageNotFoundDialogControllerFile.cs b/src/HC.PageNotFoundManager.Core/Backoffice/PageNotFoundDialogControllerFile.cs new file mode 100644 index 0000000..45d49ea --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Backoffice/PageNotFoundDialogControllerFile.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.WebAssets; + +namespace HC.PageNotFoundManager.Core.Backoffice +{ + public class PageNotFoundDialogControllerFile : JavaScriptFile + { + public PageNotFoundDialogControllerFile() : base("/App_Plugins/HC.PageNotFound/js/dialog.controller.js") + { + + } + } +} diff --git a/src/HC.PageNotFoundManager.Core/Backoffice/PageNotFoundResourceFile.cs b/src/HC.PageNotFoundManager.Core/Backoffice/PageNotFoundResourceFile.cs new file mode 100644 index 0000000..4a8d8cd --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Backoffice/PageNotFoundResourceFile.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.WebAssets; + +namespace HC.PageNotFoundManager.Core.Backoffice +{ + public class PageNotFoundResourceFile : JavaScriptFile + { + public PageNotFoundResourceFile() : base("/App_Plugins/HC.PageNotFound/js/resource.js") + { + + } + } +} diff --git a/src/HCS.PageNotFoundManager/Caching/CacheRefresher.cs b/src/HC.PageNotFoundManager.Core/Caching/CacheRefresher.cs similarity index 90% rename from src/HCS.PageNotFoundManager/Caching/CacheRefresher.cs rename to src/HC.PageNotFoundManager.Core/Caching/CacheRefresher.cs index 0b4547e..6c8b22d 100644 --- a/src/HCS.PageNotFoundManager/Caching/CacheRefresher.cs +++ b/src/HC.PageNotFoundManager.Core/Caching/CacheRefresher.cs @@ -1,12 +1,12 @@ -using HCS.PageNotFoundManager.Core.Config; -using HCS.PageNotFoundManager.Core.Models; +using HC.PageNotFoundManager.Core.Config; +using HC.PageNotFoundManager.Core.Models; using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; -namespace HCS.PageNotFoundManager.Core.Caching +namespace HC.PageNotFoundManager.Core.Caching { public class PageNotFoundCacheRefresher : PayloadCacheRefresherBase { diff --git a/src/HCS.PageNotFoundManager/Caching/DistributedCacheExtensions.cs b/src/HC.PageNotFoundManager.Core/Caching/DistributedCacheExtensions.cs similarity index 78% rename from src/HCS.PageNotFoundManager/Caching/DistributedCacheExtensions.cs rename to src/HC.PageNotFoundManager.Core/Caching/DistributedCacheExtensions.cs index ded1c80..b8acfcd 100644 --- a/src/HCS.PageNotFoundManager/Caching/DistributedCacheExtensions.cs +++ b/src/HC.PageNotFoundManager.Core/Caching/DistributedCacheExtensions.cs @@ -1,8 +1,8 @@ -using HCS.PageNotFoundManager.Core.Models; +using HC.PageNotFoundManager.Core.Models; using System; using Umbraco.Cms.Core.Cache; -namespace HCS.PageNotFoundManager.Core.Caching +namespace HC.PageNotFoundManager.Core.Caching { public static class DistributedCacheExtensions { diff --git a/src/HC.PageNotFoundManager.Core/Config/PageNotFound.cs b/src/HC.PageNotFoundManager.Core/Config/PageNotFound.cs new file mode 100644 index 0000000..f28f717 --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Config/PageNotFound.cs @@ -0,0 +1,16 @@ +using System; + +namespace HC.PageNotFoundManager.Core.Config +{ + /// + /// Class to represent the settings from appsettings + /// + public class PageNotFound + { + public PageNotFound() + { + UserGroups = Array.Empty(); + } + public string[] UserGroups { get; set; } + } +} diff --git a/src/HCS.PageNotFoundManager/Config/PageNotFoundConfig.cs b/src/HC.PageNotFoundManager.Core/Config/PageNotFoundConfig.cs similarity index 75% rename from src/HCS.PageNotFoundManager/Config/PageNotFoundConfig.cs rename to src/HC.PageNotFoundManager.Core/Config/PageNotFoundConfig.cs index a78d4f8..ea296bd 100644 --- a/src/HCS.PageNotFoundManager/Config/PageNotFoundConfig.cs +++ b/src/HC.PageNotFoundManager.Core/Config/PageNotFoundConfig.cs @@ -1,5 +1,4 @@ -using HCS.PageNotFoundManager.Core.Models; -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Cache; @@ -7,7 +6,7 @@ using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace HCS.PageNotFoundManager.Core.Config +namespace HC.PageNotFoundManager.Core.Config { public class PageNotFoundConfig : IPageNotFoundConfig { @@ -45,12 +44,10 @@ public int GetNotFoundPage(Guid parentKey) public void SetNotFoundPage(int parentId, int pageNotFoundId, bool refreshCache) { - using (var umbracoContext = umbracoContextFactory.EnsureUmbracoContext()) - { - var parentPage = umbracoContext.UmbracoContext.Content.GetById(parentId); - var pageNotFoundPage = umbracoContext.UmbracoContext.Content.GetById(pageNotFoundId); - SetNotFoundPage(parentPage.Key, pageNotFoundPage != null ? pageNotFoundPage.Key : Guid.Empty, refreshCache); - } + using var umbracoContext = umbracoContextFactory.EnsureUmbracoContext(); + var parentPage = umbracoContext.UmbracoContext.Content.GetById(parentId); + var pageNotFoundPage = umbracoContext.UmbracoContext.Content.GetById(pageNotFoundId); + SetNotFoundPage(parentPage.Key, pageNotFoundPage != null ? pageNotFoundPage.Key : Guid.Empty, refreshCache); } public void SetNotFoundPage(Guid parentKey, Guid pageNotFoundKey, bool refreshCache) @@ -59,11 +56,11 @@ public void SetNotFoundPage(Guid parentKey, Guid pageNotFoundKey, bool refreshCa using (var scope = scopeProvider.CreateScope()) { var db = scope.Database; - var page = db.Query().Where(p => p.ParentId == parentKey).FirstOrDefault(); + var page = db.Query().Where(p => p.ParentId == parentKey).FirstOrDefault(); if (page == null && !Guid.Empty.Equals(pageNotFoundKey)) { // create the page - db.Insert(new PageNotFound { ParentId = parentKey, NotFoundPageId = pageNotFoundKey }); + db.Insert(new Models.PageNotFound { ParentId = parentKey, NotFoundPageId = pageNotFoundKey }); } else if (page != null) { @@ -73,7 +70,7 @@ public void SetNotFoundPage(Guid parentKey, Guid pageNotFoundKey, bool refreshCa { // update the existing page page.NotFoundPageId = pageNotFoundKey; - db.Update(PageNotFound.TableName, "ParentId", page); + db.Update(Models.PageNotFound.TableName, "ParentId", page); } } scope.Complete(); @@ -89,20 +86,20 @@ public void RefreshCache() appPolicyCache.Insert(CacheKey, LoadFromDb); } - private List ConfiguredPages + private List ConfiguredPages { get { - var us = (List)appPolicyCache.Get(CacheKey, LoadFromDb); + var us = (List)appPolicyCache.Get(CacheKey, LoadFromDb); return us; } } - private List LoadFromDb() + private List LoadFromDb() { using var scope = scopeProvider.CreateScope(autoComplete: true); - var sql = scope.SqlContext.Sql().Select("*").From(); - var pages = scope.Database.Fetch(sql); + var sql = scope.SqlContext.Sql().Select("*").From(); + var pages = scope.Database.Fetch(sql); scope.Complete(); return pages; } diff --git a/src/HC.PageNotFoundManager.Core/Constants/Constants.cs b/src/HC.PageNotFoundManager.Core/Constants/Constants.cs new file mode 100644 index 0000000..9cb515c --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Constants/Constants.cs @@ -0,0 +1,11 @@ +namespace HC.PageNotFoundManager.Core +{ + public static class Constants + { + public const string Base = "HCS"; + public const string BackOffice = Base + "PageNotFound"; + + public const string MenuAlias = "pageNotFoundManagement"; + public const string MenuLabel = "Manage 404 Page"; + } +} diff --git a/src/HCS.PageNotFoundManager/ContentFinders/PageNotFoundFinder.cs b/src/HC.PageNotFoundManager.Core/ContentFinders/PageNotFoundFinder.cs similarity index 97% rename from src/HCS.PageNotFoundManager/ContentFinders/PageNotFoundFinder.cs rename to src/HC.PageNotFoundManager.Core/ContentFinders/PageNotFoundFinder.cs index 9b899da..d1f8ac0 100644 --- a/src/HCS.PageNotFoundManager/ContentFinders/PageNotFoundFinder.cs +++ b/src/HC.PageNotFoundManager.Core/ContentFinders/PageNotFoundFinder.cs @@ -1,4 +1,4 @@ -using HCS.PageNotFoundManager.Core.Config; +using HC.PageNotFoundManager.Core.Config; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +9,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -namespace HCS.PageNotFoundManager.Core.ContentFinders +namespace HC.PageNotFoundManager.Core.ContentFinders { public class PageNotFoundFinder : IContentLastChanceFinder { diff --git a/src/HC.PageNotFoundManager.Core/HC.PageNotFoundManager.Core.csproj b/src/HC.PageNotFoundManager.Core/HC.PageNotFoundManager.Core.csproj new file mode 100644 index 0000000..cf497ed --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/HC.PageNotFoundManager.Core.csproj @@ -0,0 +1,36 @@ + + + + 1.0.0 + 1.0.0 + 1.0.0 + net5.0 + Nik Rimington + . + HotChilli.Umbraco.PageNotFound.Core + HotChilli.Umbraco.PageNotFound.Core + HotChilli.Umbraco.PageNotFound.Core + Umbraco package for setting 404 response pages + HotChilli.Umbraco.PageNotFound.Core + umbraco plugin package + https://github.com/NikRimington/HCS.Umbraco.PageNotFoundManager + logo.png + https://raw.githubusercontent.com/NikRimington/HCS.Umbraco.PageNotFoundManager/main/docs/img/logo.png + https://github.com/NikRimington/HCS.Umbraco.PageNotFoundManager + Git + LICENSE + + + + + + + + + + + + + + + diff --git a/src/HC.PageNotFoundManager.Core/Migrations/InitialMigration.cs b/src/HC.PageNotFoundManager.Core/Migrations/InitialMigration.cs new file mode 100644 index 0000000..a886b35 --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Migrations/InitialMigration.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Cms.Infrastructure.Migrations; + +namespace HC.PageNotFoundManager.Core.Migrations +{ + public class InitialMigration : MigrationBase + { + public const string MigrationName = "page-not-found-manager-migration-initial"; + private readonly ILogger logger; + + public InitialMigration(IMigrationContext context, ILogger logger) + : base(context) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + protected override void Migrate() + { + logger.LogDebug("Starting migration - {migrationName}", MigrationName); + + if (!TableExists(PageNotFoundInitialMigrationModel.TableName)) + Create.Table().Do(); + } + } + + public class PageNotFoundMigrationPlan : MigrationPlan + { + + public PageNotFoundMigrationPlan() + : base("PageNotFoundManager") => DefinePlan(); + + /// + public override string InitialState => "{pagenotfound-init-state}"; + + /// + /// Defines the plan. + /// + protected void DefinePlan() + { + From("{pagenotfound-init-state}") + .To("{pagenotfound-init-complete}"); + } + } + +} diff --git a/src/HC.PageNotFoundManager.Core/Migrations/PageNotFoundInitialMigrationModel.cs b/src/HC.PageNotFoundManager.Core/Migrations/PageNotFoundInitialMigrationModel.cs new file mode 100644 index 0000000..8809758 --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Migrations/PageNotFoundInitialMigrationModel.cs @@ -0,0 +1,23 @@ +using NPoco; +using System; +using System.Runtime.Serialization; + +namespace HC.PageNotFoundManager.Core.Migrations +{ + [TableName(TableName)] + [PrimaryKey("ParentId", AutoIncrement = false)] + public sealed class PageNotFoundInitialMigrationModel + { + + [IgnoreDataMember] + public const string TableName = "PageNotFoundManagerConfig"; + + [Column("ParentId")] + public Guid ParentId { get; set; } + + [Column("NotFoundPageId")] + public Guid NotFoundPageId { get; set; } + + } + +} diff --git a/src/HCS.PageNotFoundManager/Models/PageNotFound.cs b/src/HC.PageNotFoundManager.Core/Models/PageNotFound.cs similarity index 76% rename from src/HCS.PageNotFoundManager/Models/PageNotFound.cs rename to src/HC.PageNotFoundManager.Core/Models/PageNotFound.cs index 5605619..a2447a7 100644 --- a/src/HCS.PageNotFoundManager/Models/PageNotFound.cs +++ b/src/HC.PageNotFoundManager.Core/Models/PageNotFound.cs @@ -2,7 +2,7 @@ using System; using System.Runtime.Serialization; -namespace HCS.PageNotFoundManager.Core.Models +namespace HC.PageNotFoundManager.Core.Models { [TableName(TableName)] @@ -11,7 +11,7 @@ public class PageNotFound { [IgnoreDataMember] - public const string TableName = "PageNotFoundManager.Config"; + public const string TableName = "PageNotFoundManagerConfig"; [Column("ParentId")] public Guid ParentId { get; set; } diff --git a/src/HCS.PageNotFoundManager/Models/PageNotFoundRequest.cs b/src/HC.PageNotFoundManager.Core/Models/PageNotFoundRequest.cs similarity index 74% rename from src/HCS.PageNotFoundManager/Models/PageNotFoundRequest.cs rename to src/HC.PageNotFoundManager.Core/Models/PageNotFoundRequest.cs index 7b86676..87c26ad 100644 --- a/src/HCS.PageNotFoundManager/Models/PageNotFoundRequest.cs +++ b/src/HC.PageNotFoundManager.Core/Models/PageNotFoundRequest.cs @@ -1,4 +1,4 @@ -namespace HCS.PageNotFoundManager.Core.Models +namespace HC.PageNotFoundManager.Core.Models { public class PageNotFoundRequest { diff --git a/src/HC.PageNotFoundManager.Core/Startup/StartupComposer.cs b/src/HC.PageNotFoundManager.Core/Startup/StartupComposer.cs new file mode 100644 index 0000000..a050126 --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Startup/StartupComposer.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace HC.PageNotFoundManager.Core.Startup +{ + public class StartupComposer : IUserComposer + { + public void Compose(IUmbracoBuilder builder) + { + builder.UsePageNotFoundManager(); + } + } +} diff --git a/src/HC.PageNotFoundManager.Core/Startup/UmbracoBuilderExtensions.cs b/src/HC.PageNotFoundManager.Core/Startup/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000..966a6f5 --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Startup/UmbracoBuilderExtensions.cs @@ -0,0 +1,31 @@ +using HC.PageNotFoundManager.Core.Backoffice; +using HC.PageNotFoundManager.Core.Config; +using HC.PageNotFoundManager.Core.ContentFinders; +using System.Linq; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace HC.PageNotFoundManager.Core.Startup +{ + public static class UmbracoBuilderExtensions + { + public static IUmbracoBuilder UsePageNotFoundManager(this IUmbracoBuilder builder) + { + + if (builder.Services.FirstOrDefault(x => x.ServiceType == typeof(IPageNotFoundConfig)) != null) + return builder; + + builder.Services.AddUnique(); + builder.SetContentLastChanceFinder(); + builder.BackOfficeAssets() + .Append() + .Append(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + return builder; + } + } +} diff --git a/src/HC.PageNotFoundManager.Core/Startup/UmbracoStartingNotificationHandler.cs b/src/HC.PageNotFoundManager.Core/Startup/UmbracoStartingNotificationHandler.cs new file mode 100644 index 0000000..855532c --- /dev/null +++ b/src/HC.PageNotFoundManager.Core/Startup/UmbracoStartingNotificationHandler.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using System; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +namespace HC.PageNotFoundManager.Core.Startup +{ + public class UmbracoStartingNotificationHandler : INotificationHandler + { + private readonly ILogger logger; + private readonly IRuntimeState runtimeState; + private readonly IScopeProvider scopeProvider; + private readonly IKeyValueService keyValueService; + private readonly IMigrationPlanExecutor migrationPlanExecutor; + + public UmbracoStartingNotificationHandler(ILogger logger, + IRuntimeState runtimeState, IScopeProvider scopeProvider, IKeyValueService keyValueService, IMigrationPlanExecutor migrationPlanExecutor) + { + + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); + this.scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); + this.keyValueService = keyValueService ?? throw new ArgumentNullException(nameof(keyValueService)); + this.migrationPlanExecutor = migrationPlanExecutor ?? throw new ArgumentNullException(nameof(migrationPlanExecutor)); + } + + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (runtimeState.Level < RuntimeLevel.Upgrade) + { + logger.LogInformation("Umbraco Runtime is not Run/Upgrade mode, so a database connection is unlikely to be available for migrations"); + return; + } + + ApplyMigration(); + } + + private void ApplyMigration() + { + var upgrader = new Upgrader(new Migrations.PageNotFoundMigrationPlan()); + upgrader.Execute(migrationPlanExecutor, scopeProvider, keyValueService); + } + } +} diff --git a/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/Backoffice/Dialogs/dialog.html b/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/Backoffice/Dialogs/dialog.html new file mode 100644 index 0000000..8dab037 --- /dev/null +++ b/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/Backoffice/Dialogs/dialog.html @@ -0,0 +1,91 @@ +
+
+
+

+ Choose the 404 page for {{currentNode.name}} and child pages +

+ +
+
+ +
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+ +
+
+ {{target.name}} is set as the 404 page for + {{currentNode.name}} and it's child pages +
+ +
+ +

+ Choose the 404 page for {{currentNode.name}} and child pages +

+ +
+ +
+ + + +
+
+ + + +
+ + + + +
+ + +
+
+ + + + +
+
+
+ + +
diff --git a/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/js/dialog.controller.js b/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/js/dialog.controller.js new file mode 100644 index 0000000..0af16d6 --- /dev/null +++ b/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/js/dialog.controller.js @@ -0,0 +1,135 @@ +(function() { + "use strict"; + function PageNotFoundManagerDialog($scope, pageNotFoundManagerResource, navigationService, userService, entityResource, iconHelper) { + var node = $scope.currentNode; + var vm = this; + var initUserDetails; + var initNotFound; + $scope.nav = navigationService; + $scope.dialogTreeApi = {}; + $scope.treeModel = { + hideHeader: false + }; + $scope.dialogTreeEventHandler = $({}); + + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + function init() { + initUserDetails = false; + initNotFound = false; + userService.getCurrentUser().then((userData) => { + $scope.treeModel.hideHeader = + userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) === -1; + initUserDetails = true; + vm.busy = !(initNotFound && initUserDetails); + }); + + pageNotFoundManagerResource.getNotFoundPage(node.id).then((resp) => { + const val = parseInt(resp.data); + if (!isNaN(val) && angular.isNumber(val) && val > 0) { + $scope.pageNotFoundId = val; + entityResource.getById(val, "Document").then(function(item) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + $scope.pageNotFoundNode = item; + }); + } + vm.loaded = true; + initNotFound = true; + vm.busy = !(initNotFound && initUserDetails); + }); + } + + + function nodeSelectHandler(args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + + $scope.hideSearch = function() { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + }; + + // method to select a search result + $scope.selectResult = function(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function(results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.setNotFoundPage = function() { + + vm.busy = true; + $scope.error = false; + + var parentId = 0; + if (node !== null) + parentId = node.id; + + var notFoundPageId = 0; + if ($scope.target !== undefined && $scope.target !== null) + notFoundPageId = $scope.target.id; + + pageNotFoundManagerResource.setNotFoundPage(parentId, notFoundPageId).then(function() { + $scope.error = false; + $scope.success = true; + vm.busy = false; + }, function(err) { + $scope.success = false; + $scope.error = err; + vm.busy = false; + }); + }; + + $scope.clear = () => $scope.pageNotFoundNode = null; + + function treeLoadedHandler() { + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); + } + } + + $scope.onTreeInit = function() { + $scope.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); + }; + + init(); + } + + angular.module("umbraco").controller("hc.pagenotfound.dialog.controller", + ['$scope', 'hc.pageNotFoundManagerResource', 'notificationsService', + 'userService', 'entityResource', 'iconHelper', PageNotFoundManagerDialog]); +})(); \ No newline at end of file diff --git a/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/js/resource.js b/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/js/resource.js new file mode 100644 index 0000000..1b69828 --- /dev/null +++ b/src/HC.PageNotFoundManager/App_Plugins/HC.PageNotFound/js/resource.js @@ -0,0 +1,20 @@ +function pageNotFoundManagerResource($http) { + + var apiRoot = "backoffice/HCSPageNotFound/Menu/"; + + return { + + getNotFoundPage: function (pageId) { + return $http.get(apiRoot + "GetNotFoundPage?pageId="+pageId); + }, + + setNotFoundPage: function (parentId, notFoundPageId) { + var pnf = {}; + pnf.parentId = parentId; + pnf.notFoundPageId = notFoundPageId; + return $http.post(apiRoot + "SetNotFoundPage", pnf); + } + }; +} + +angular.module('umbraco.resources').factory('hc.pageNotFoundManagerResource', pageNotFoundManagerResource); \ No newline at end of file diff --git a/src/HC.PageNotFoundManager/HC.PageNotFoundManager.csproj b/src/HC.PageNotFoundManager/HC.PageNotFoundManager.csproj new file mode 100644 index 0000000..a3ec7ee --- /dev/null +++ b/src/HC.PageNotFoundManager/HC.PageNotFoundManager.csproj @@ -0,0 +1,40 @@ + + + + 1.0.0 + 1.0.0 + 1.0.0 + net5.0 + Nik Rimington + . + HotChilli.Umbraco.PageNotFound + HotChilli.Umbraco.PageNotFound + HotChilli.Umbraco.PageNotFound + Umbraco packaged for setting 404 response pages. + HotChilli.Umbraco.PageNotFound + umbraco plugin package + https://github.com/NikRimington/HCS.Umbraco.PageNotFoundManager + logo.png + https://raw.githubusercontent.com/NikRimington/HCS.Umbraco.PageNotFoundManager/main/docs/img/logo.png + https://github.com/NikRimington/HCS.Umbraco.PageNotFoundManager + Git + LICENSE + + + + true + Always + + + True + buildTransitive + + + + + + + + + + diff --git a/src/HC.PageNotFoundManager/build/HotChilli.Umbraco.PageNotFound.targets b/src/HC.PageNotFoundManager/build/HotChilli.Umbraco.PageNotFound.targets new file mode 100644 index 0000000..b507195 --- /dev/null +++ b/src/HC.PageNotFoundManager/build/HotChilli.Umbraco.PageNotFound.targets @@ -0,0 +1,27 @@ + + + + $(MSBuildThisFileDirectory)..\App_Plugins\HC.PageNotFound\**\*.* + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/HCS.PageNotFoundManager/Constants/Constants.cs b/src/HCS.PageNotFoundManager/Constants/Constants.cs deleted file mode 100644 index 6d4a3f9..0000000 --- a/src/HCS.PageNotFoundManager/Constants/Constants.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace HCS.PageNotFoundManager.Core -{ - public static class Constants - { - public const string Base = "HCS"; - public const string BackOffice = Base + "PageNotFound"; - } -} diff --git a/src/HCS.PageNotFoundManager/HCS.PageNotFoundManager.Core.csproj b/src/HCS.PageNotFoundManager/HCS.PageNotFoundManager.Core.csproj deleted file mode 100644 index 6d36ab7..0000000 --- a/src/HCS.PageNotFoundManager/HCS.PageNotFoundManager.Core.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net5.0 - - - - - - - - - - - diff --git a/src/HCS.PageNotFoundManager/StartupComposer.cs b/src/HCS.PageNotFoundManager/StartupComposer.cs deleted file mode 100644 index 4ebbbaf..0000000 --- a/src/HCS.PageNotFoundManager/StartupComposer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using HCS.PageNotFoundManager.Core.Config; -using HCS.PageNotFoundManager.Core.ContentFinders; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Extensions; - -namespace HCS.PageNotFoundManager.Core -{ - public class StartupComposer : IUserComposer - { - public void Compose(IUmbracoBuilder builder) - { - builder.Services.AddUnique(); - builder.SetContentLastChanceFinder(); - } - } -} diff --git a/src/PageNotFoundManager.sln b/src/PageNotFoundManager.sln index 1404f3b..7efdf66 100644 --- a/src/PageNotFoundManager.sln +++ b/src/PageNotFoundManager.sln @@ -3,7 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31424.327 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HCS.PageNotFoundManager.Core", "HCS.PageNotFoundManager\HCS.PageNotFoundManager.Core.csproj", "{0CA20044-8BCC-44FF-93AD-3578036CA6A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HC.PageNotFoundManager.Core", "HC.PageNotFoundManager.Core\HC.PageNotFoundManager.Core.csproj", "{0CA20044-8BCC-44FF-93AD-3578036CA6A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HC.PageNotFoundManager", "HC.PageNotFoundManager\HC.PageNotFoundManager.csproj", "{705A76EA-F683-4D8C-8138-8AE3E6F14646}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution files", "Solution files", "{5114C3D3-99A7-4A55-8B86-D11292DA5346}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +22,10 @@ Global {0CA20044-8BCC-44FF-93AD-3578036CA6A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CA20044-8BCC-44FF-93AD-3578036CA6A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {0CA20044-8BCC-44FF-93AD-3578036CA6A5}.Release|Any CPU.Build.0 = Release|Any CPU + {705A76EA-F683-4D8C-8138-8AE3E6F14646}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {705A76EA-F683-4D8C-8138-8AE3E6F14646}.Debug|Any CPU.Build.0 = Debug|Any CPU + {705A76EA-F683-4D8C-8138-8AE3E6F14646}.Release|Any CPU.ActiveCfg = Release|Any CPU + {705A76EA-F683-4D8C-8138-8AE3E6F14646}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE