diff --git a/src/CMS/Business/Channels/KhaiTestDisplayChannel.cs b/src/CMS/Business/Channels/KhaiTestDisplayChannel.cs new file mode 100644 index 00000000..dd41acfc --- /dev/null +++ b/src/CMS/Business/Channels/KhaiTestDisplayChannel.cs @@ -0,0 +1,20 @@ +using EPiServer.Framework.Web; +using EPiServer.Web; + +namespace CMS.Business.Channels; + +public class KhaiTestDisplayChannel : DisplayChannel +{ + public override bool IsActive(HttpContext context) + { + return true; + //The sample code uses package 'Wangkanai.Detection' for device detection + //var detection = context.RequestServices.GetRequiredService(); + //return detection.Device.Type == DeviceType.Mobile; + } + + public override string ChannelName + { + get { return "Khai Test DisplayChannel"; } + } +} \ No newline at end of file diff --git a/src/CMS/Business/Channels/MobileDisplayChannel.cs b/src/CMS/Business/Channels/MobileDisplayChannel.cs new file mode 100644 index 00000000..3fbfa540 --- /dev/null +++ b/src/CMS/Business/Channels/MobileDisplayChannel.cs @@ -0,0 +1,28 @@ +using EPiServer.Framework.Web; +using EPiServer.Web; + +namespace CMS.Business.Channels; + +public class MobileDisplayChannel : DisplayChannel +{ + public override bool IsActive(HttpContext context) + { + return true; + //The sample code uses package 'Wangkanai.Detection' for device detection + //var detection = context.RequestServices.GetRequiredService(); + //return detection.Device.Type == DeviceType.Mobile; + } + + public override string ChannelName + { + get { return RenderingTags.Mobile; } + } + + public override string ResolutionId + { + get + { + return typeof(MobileResolution).FullName; + } + } +} \ No newline at end of file diff --git a/src/CMS/Business/Channels/MobileResolution.cs b/src/CMS/Business/Channels/MobileResolution.cs new file mode 100644 index 00000000..5641e080 --- /dev/null +++ b/src/CMS/Business/Channels/MobileResolution.cs @@ -0,0 +1,26 @@ +using EPiServer.Web; + +namespace CMS.Business.Channels; + +public class MobileResolution : IDisplayResolution +{ + public int Height + { + get { return 568; } + } + + public string Id + { + get { return GetType().FullName; } + } + + public string Name + { + get { return "Mobile (320x568)"; } + } + + public int Width + { + get { return 320; } + } +} \ No newline at end of file diff --git a/src/CMS/Business/Initialization/CustomLocalizationProviderInitialization.cs b/src/CMS/Business/Initialization/CustomLocalizationProviderInitialization.cs new file mode 100644 index 00000000..4ac4e1d6 --- /dev/null +++ b/src/CMS/Business/Initialization/CustomLocalizationProviderInitialization.cs @@ -0,0 +1,19 @@ +using EPiServer.Framework.Initialization; +using EPiServer.Framework; +using EPiServer.ServiceLocation; + +namespace CMS.Business.Initialization; + +[InitializableModule] +[ModuleDependency(typeof(FrameworkInitialization))] +public class CustomLocalizationProviderInitialization : IConfigurableModule +{ + public void ConfigureContainer(ServiceConfigurationContext context) + { + // ClassInMyAssembly can be any class in the Assembly where the resources are embedded + //context.Services.AddEmbeddedLocalization(); + } + + public void Initialize(InitializationEngine context) { } + public void Uninitialize(InitializationEngine context) { } +} \ No newline at end of file diff --git a/src/CMS/Business/ViewTemplateModelRegistrator.cs b/src/CMS/Business/ViewTemplateModelRegistrator.cs new file mode 100644 index 00000000..3a5757af --- /dev/null +++ b/src/CMS/Business/ViewTemplateModelRegistrator.cs @@ -0,0 +1,36 @@ +using CMS.Models.Blocks; +using EPiServer.Framework.Web; +using EPiServer.Web.Mvc; + +namespace CMS.Business; + +public class ViewTemplateModelRegistrator : IViewTemplateModelRegistrator +{ + public void Register(TemplateModelCollection viewTemplateModelRegistrator) + { + viewTemplateModelRegistrator.Add(typeof(ThirdBlock), + new TemplateModel() + { + Name = "SidebarTeaserRight", + Description = "Displays a teaser for a page.", + Path = "~/Views/Shared/SidebarThirdBlockRight.cshtml", + AvailableWithoutTag = true + }, + new TemplateModel() + { + Name = "SidebarTeaserLeft", + Description = "Displays a teaser for a page.", + Path = "~/Views/Shared/SidebarThirdBlockLeft.cshtml", + Tags = new string[] { RenderingTags.Sidebar } + }); + + viewTemplateModelRegistrator.Add(typeof(FourthBlock), + new TemplateModel() + { + Name = "SidebarTeaser", + Description = "Displays a teaser of a page.", + Path = "~/Views/Shared/FourthBlock.cshtml", + Tags = new string[] { RenderingTags.Sidebar } + }); + } +} \ No newline at end of file diff --git a/src/CMS/CMS.csproj b/src/CMS/CMS.csproj index e140543d..14969061 100644 --- a/src/CMS/CMS.csproj +++ b/src/CMS/CMS.csproj @@ -1,4 +1,4 @@ - + net6.0 enable @@ -6,17 +6,22 @@ - - - - + + + + + + + + + diff --git a/src/CMS/Components/PagePartialComponent.cs b/src/CMS/Components/PagePartialComponent.cs new file mode 100644 index 00000000..cb2b6375 --- /dev/null +++ b/src/CMS/Components/PagePartialComponent.cs @@ -0,0 +1,15 @@ +using CMS.Models.Pages; +using EPiServer.Framework.DataAnnotations; +using EPiServer.Web.Mvc; +using Microsoft.AspNetCore.Mvc; + +namespace CMS.Components; + +[TemplateDescriptor(Inherited = true)] +public class PagePartialComponent : PartialContentComponent +{ + protected override IViewComponentResult InvokeComponent(SitePageData currentContent) + { + return View("/Views/Shared/Components/PagePartial/Default.cshtml", currentContent); + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/DefaultPageController.cs b/src/CMS/Controllers/DefaultPageController.cs new file mode 100644 index 00000000..1ae14bab --- /dev/null +++ b/src/CMS/Controllers/DefaultPageController.cs @@ -0,0 +1,15 @@ +using CMS.Models.Pages; +using EPiServer.Framework.DataAnnotations; +using EPiServer.Web.Mvc; +using Microsoft.AspNetCore.Mvc; + +namespace CMS.Controllers; + +[TemplateDescriptor(Inherited = true)] +public class DefaultPageController : PageController +{ + public ViewResult Index(SitePageData currentPage) + { + return View($"~/Views/{currentPage.GetOriginalType().Name}/Index.cshtml", currentPage); + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/FirstPageController.cs b/src/CMS/Controllers/FirstPageController.cs new file mode 100644 index 00000000..dc3b6719 --- /dev/null +++ b/src/CMS/Controllers/FirstPageController.cs @@ -0,0 +1,36 @@ +using CMS.Models.Pages; +using EPiServer.Framework.DataAnnotations; +using EPiServer.Framework.Localization; +using EPiServer.Framework.Web; +using EPiServer.Web.Mvc; +using Microsoft.AspNetCore.Mvc; +using System.Text; + +namespace CMS.Controllers; + +[TemplateDescriptor( + Inherited = false, + Description = "Default template to be used by first pages")] +public class FirstPageController : PageControllerBase +{ + public ActionResult Index(FirstPage currentPage) + { + // Implementation of action view the page. + + return View(currentPage); + } +} + +[TemplateDescriptor(Tags = new string[] { RenderingTags.Mobile })] +public class FirstPageMobileController : PageController +{ + public ActionResult Index(FirstPage currentPage) + { + // Implementation of action view the page. + StringBuilder builder = new(); + builder.Append(LocalizationService.Current.GetString("/mystring")); + builder.Append(LocalizationService.Current.GetString("/subnode/myotherstring")); + + return View(currentPage); + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/ImagePageController.cs b/src/CMS/Controllers/ImagePageController.cs new file mode 100644 index 00000000..a003a96c --- /dev/null +++ b/src/CMS/Controllers/ImagePageController.cs @@ -0,0 +1,55 @@ +using CMS.Models.Pages; +using EPiServer.Framework.Blobs; +using EPiServer.ServiceLocation; +using EPiServer.Web.Mvc; +using Microsoft.AspNetCore.Mvc; + +namespace CMS.Controllers; + +public class ImagePageController : PageController +{ + public async Task Index(ImagePage currentPage) + { + await ReadWriteBlobs(currentPage.BlobPathToReadWrite); + + return View(currentPage); + } + + public async Task ReadWriteBlobs(string path) + { + var blobFactory = ServiceLocator.Current.GetInstance(); + + //Define a container + var container = Blob.GetContainerIdentifier(Guid.NewGuid()); + + //Uploading a file to a blob + var blob1 = blobFactory.CreateBlob(container, ".jpg"); + using (var fs = new FileStream(path, FileMode.Open)) + { + blob1.Write(fs); + } + + //Writing custom data to a blob + var blob2 = blobFactory.CreateBlob(container, ".txt"); + using (var s = blob2.OpenWrite()) + { + var w = new StreamWriter(s); + await w.WriteLineAsync("Hello World!"); + await w.FlushAsync(); + } + + //Reading from a blob based on ID + var blobID = blob2.ID; + var blob3 = blobFactory.GetBlob(blobID); + using (var s = blob3.OpenRead()) + { + var helloWorld = await new StreamReader(s).ReadToEndAsync(); + } + + //Delete single blob + blobFactory.Delete(blobID); + + //Delete container + blobFactory.Delete(container); + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/MobileTemplate.cs b/src/CMS/Controllers/MobileTemplate.cs new file mode 100644 index 00000000..9b3efb12 --- /dev/null +++ b/src/CMS/Controllers/MobileTemplate.cs @@ -0,0 +1,55 @@ +using CMS.Models.Pages; +using EPiServer.Framework; +using EPiServer.Framework.DataAnnotations; +using EPiServer.Framework.Initialization; +using EPiServer.Web; +using EPiServer.Web.Mvc; + +namespace CMS.Controllers; + +[TemplateDescriptor(Name = "MobileTemplate")] +public partial class MobileTemplate : PageController +{ +} + +[InitializableModule] +public class MobileRedirectSample : IInitializableModule +{ + private IHttpContextAccessor _httpContextAccessor; + public void Initialize(InitializationEngine context) + { + _httpContextAccessor = context.Locate.Advanced.GetRequiredService(); + context.Locate.Advanced.GetRequiredService().TemplateResolved + += new EventHandler(MobileRedirectSample_TemplateResolved); + } + + public void Uninitialize(InitializationEngine context) + { + //context.Locate.Advanced.GetRequiredService().TemplateResolved + // -= new EventHandlerMobileRedirectSample_TemplateResolved); + } + + void MobileRedirectSample_TemplateResolved(object sender, TemplateResolverEventArgs eventArgs) + { + if (eventArgs.ItemToRender != null && eventArgs.ItemToRender is FirstPage) + { + //The sample code uses package 'Wangkanai.Detection' for device detection + //var detection = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService(); + //if (detection.Device.Type == DeviceType.Mobile) + //{ + // var mobileRender = eventArgs.SupportedTemplates + // .SingleOrDefault(r => r.Name.Contains("Mobile") && + // r.TemplateTypeCategory == eventArgs.SelectedTemplate.TemplateTypeCategory); + + // if (mobileRender != null) + // { + // eventArgs.SelectedTemplate = mobileRender; + // } + //} + } + } + + public void Preload(string[] parameters) + { + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/PageControllerBase.cs b/src/CMS/Controllers/PageControllerBase.cs new file mode 100644 index 00000000..e77359a9 --- /dev/null +++ b/src/CMS/Controllers/PageControllerBase.cs @@ -0,0 +1,15 @@ +using CMS.Models.Pages; +using EPiServer.Web.Mvc; +using Microsoft.AspNetCore.Mvc; + +namespace CMS.Controllers; + +public abstract class PageControllerBase : PageController where T : SitePageData +{ + // Providing a logout action for the page. + public ActionResult Logout() + { + // LKTODO: FormsAuthentication.SignOut(); + return RedirectToAction("Index"); + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/PreviewController.cs b/src/CMS/Controllers/PreviewController.cs new file mode 100644 index 00000000..ea24fac2 --- /dev/null +++ b/src/CMS/Controllers/PreviewController.cs @@ -0,0 +1,42 @@ +using EPiServer.Framework.DataAnnotations; +using EPiServer.Framework.Web.Mvc; +using EPiServer.Framework.Web; +using EPiServer.Web.Mvc; +using EPiServer.Web; +using Microsoft.AspNetCore.Mvc; +using CMS.Models.ViewModels; +using CMS.Models.Pages; + +namespace CMS.Controllers; + +[TemplateDescriptor( + Inherited = true, + TemplateTypeCategory = TemplateTypeCategories.MvcController, //Required as controllers for blocks are registered as MvcPartialController by default + Tags = new[] { RenderingTags.Preview, RenderingTags.Edit }, + AvailableWithoutTag = false)] +[VisitorGroupImpersonation] +[RequireClientResources] +public class PreviewController : ActionControllerBase, IRenderTemplate//, IModifyLayout +{ + private readonly IContentLoader _contentLoader; + + public PreviewController(IContentLoader contentLoader) + { + _contentLoader = contentLoader; + } + + public IActionResult Index(IContent currentContent) + { + //As the layout requires a page for title etc we "borrow" the start page + var startPage = _contentLoader.Get(SiteDefinition.Current.StartPage); + var model = new PreviewModel(startPage, currentContent); + + return View(model); + } + + public void ModifyLayout(LayoutModel layoutModel) + { + layoutModel.HideHeader = true; + layoutModel.HideFooter = true; + } +} \ No newline at end of file diff --git a/src/CMS/Controllers/SecondPageController.cs b/src/CMS/Controllers/SecondPageController.cs new file mode 100644 index 00000000..4d525684 --- /dev/null +++ b/src/CMS/Controllers/SecondPageController.cs @@ -0,0 +1,26 @@ +using CMS.Models.Pages; +using EPiServer.Framework.DataAnnotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using static CMS.Globals; + +namespace CMS.Controllers; + +[TemplateDescriptor(Default = true)] +public class SecondPageController : PageControllerBase +{ + //[Authorize(AuthenticationSchemes = Schemes.Oidc)] + public ActionResult Index(SecondPage currentPage) + { + // Implementation of action view the page. + + return View(currentPage); + } + + public async Task Logout() + { + await HttpContext.SignOutAsync(Schemes.Another); + return View(); + } +} \ No newline at end of file diff --git a/src/CMS/Globals.cs b/src/CMS/Globals.cs new file mode 100644 index 00000000..fd82452d --- /dev/null +++ b/src/CMS/Globals.cs @@ -0,0 +1,80 @@ +using System.ComponentModel.DataAnnotations; + +namespace CMS; + +public class Globals +{ + public const string LoginPath = "/util/login"; + + /// + /// Group names for content types and properties + /// + [GroupDefinitions] + public static class GroupNames + { + [Display(Name = "Default", Order = 10)] + public const string Default = "Default"; + + [Display(Name = SystemTabNames.Content, Order = 20)] + public const string Content = SystemTabNames.Content; + + [Display(Name = "Contact", Order = 30)] + public const string Contact = "Contact"; + + [Display(Name = "Metadata", Order = 40)] + public const string MetaData = "Metadata"; + + [Display(Name = "News", Order = 50)] + public const string News = "News"; + + [Display(Name = "Products", Order = 60)] + public const string Products = "Products"; + + [Display(Name = SystemTabNames.Settings, Order = 70)] + public const string Settings = SystemTabNames.Settings; + + [Display(Name = "SiteSettings", Order = 80)] + public const string SiteSettings = "SiteSettings"; + + [Display(Name = "Specialized", Order = 90)] + public const string Specialized = "Specialized"; + } + + /// + /// Tags to use for the main widths used in the Bootstrap HTML framework + /// + public static class ContentAreaTags + { + public const string FullWidth = "full"; + public const string WideWidth = "wide"; + public const string HalfWidth = "half"; + public const string NarrowWidth = "narrow"; + public const string NoRenderer = "norenderer"; + public const string TwoThirdsWidth = "Wide"; + public const string OneThirdWidth = "Narrow"; + } + + /// + /// Names used for UIHint attributes to map specific rendering controls to page properties + /// + public static class SiteUIHints + { + public const string Contact = "contact"; + public const string Strings = "StringList"; + public const string StringsCollection = "StringsCollection"; + } + + /// + /// Virtual path to folder with static graphics, such as "/gfx/" + /// + public const string StaticGraphicsFolderPath = "/gfx/"; + + public static class Schemes + { + public const string Oidc = "oidc"; + + public const string Default = "a-scheme"; + public const string Another = "another-scheme"; + public const string Policy = "policy-scheme"; + } +} \ No newline at end of file diff --git a/src/CMS/Models/Blocks/FourthBlock.cs b/src/CMS/Models/Blocks/FourthBlock.cs new file mode 100644 index 00000000..93a97e79 --- /dev/null +++ b/src/CMS/Models/Blocks/FourthBlock.cs @@ -0,0 +1,23 @@ +using EPiServer.Framework.DataAnnotations; +using EPiServer.Framework.Web; +using EPiServer.Web.Mvc; +using Microsoft.AspNetCore.Mvc; + +namespace CMS.Models.Blocks; + +[ContentType(DisplayName = "FourthBlock", GUID = "5ed39f97-0978-451d-9785-0c8fff767c87")] +public class FourthBlock : BlockData +{ +} + +[TemplateDescriptor] +public partial class FourthBlockTemplate : BlockComponent +{ + protected override IViewComponentResult InvokeComponent(FourthBlock currentContent) => throw new NotImplementedException(); +} + +[TemplateDescriptor(Tags = new string[] { RenderingTags.Mobile })] +public partial class FourthBlockMobileTemplate : BlockComponent +{ + protected override IViewComponentResult InvokeComponent(FourthBlock currentContent) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/CMS/Models/Blocks/SiteBlockData.cs b/src/CMS/Models/Blocks/SiteBlockData.cs new file mode 100644 index 00000000..599708c9 --- /dev/null +++ b/src/CMS/Models/Blocks/SiteBlockData.cs @@ -0,0 +1,8 @@ +namespace CMS.Models.Blocks; + +/// +/// Base class for all block types on the site +/// +public abstract class SiteBlockData : BlockData +{ +} \ No newline at end of file diff --git a/src/CMS/Models/Blocks/SiteLogotypeBlock.cs b/src/CMS/Models/Blocks/SiteLogotypeBlock.cs new file mode 100644 index 00000000..c0893510 --- /dev/null +++ b/src/CMS/Models/Blocks/SiteLogotypeBlock.cs @@ -0,0 +1,37 @@ +using EPiServer.Shell.ObjectEditing; +using EPiServer.Web; +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models.Blocks; + +/// +/// Used to provide a composite property on the start page to set site logotype settings +/// +[SiteContentType( + GUID = "09854019-91A5-4B93-8623-17F038346001", + AvailableInEditMode = false)] // Should not be created and added to content areas by editors, the SiteLogotypeBlock is only used as a property type +[SiteImageUrl] +public class SiteLogotypeBlock : SiteBlockData +{ + /// + /// Gets the site logotype URL + /// + /// If not specified a default logotype will be used + [DefaultDragAndDropTarget] + [UIHint(UIHint.Image)] + public virtual Url Url + { + get + { + var url = this.GetPropertyValue(b => b.Url); + + return url == null || url.IsEmpty() + ? new Url("/gfx/logotype.png") + : url; + } + set => this.SetPropertyValue(b => b.Url, value); + } + + [CultureSpecific] + public virtual string Title { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Models/Blocks/ThirdBlock.cs b/src/CMS/Models/Blocks/ThirdBlock.cs new file mode 100644 index 00000000..b860be15 --- /dev/null +++ b/src/CMS/Models/Blocks/ThirdBlock.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models.Blocks; + +[ContentType(DisplayName = "ThirdBlock", GUID = "38d57768-e09e-4da9-90df-54c73c61b270", Description = "Heading and image.")] +public class ThirdBlock : BlockData +{ + [CultureSpecific] + [Display( + Name = "Heading", + Description = "Add a heading.", + GroupName = SystemTabNames.Content, + Order = 1)] + public virtual string Heading { get; set; } + + [Display( + Name = "Image", Description = "Add an image (optional)", + GroupName = SystemTabNames.Content, + Order = 2)] + public virtual ContentReference Image { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Models/Entities/CustomUser.cs b/src/CMS/Models/Entities/CustomUser.cs new file mode 100644 index 00000000..5b44102b --- /dev/null +++ b/src/CMS/Models/Entities/CustomUser.cs @@ -0,0 +1,35 @@ +using EPiServer.Shell.Security; +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CMS.Models.Entities; + +public class CustomUser : IdentityUser, IUIUser +{ + public string Comment { get; set; } + public bool IsApproved { get; set; } + public bool IsLockedOut { get; set; } + + [Column(TypeName = "datetime2")] + public DateTime CreationDate { get; set; } + + [Column(TypeName = "datetime2")] + public DateTime? LastLockoutDate { get; set; } + + [Column(TypeName = "datetime2")] + public DateTime? LastLoginDate { get; set; } + + public string PasswordQuestion { get; } + + public string ProviderName + { + get { return "MyProviderName"; } + } + + [NotMapped] + public string Username + { + get { return base.UserName; } + set { base.UserName = value; } + } +} \ No newline at end of file diff --git a/src/CMS/Models/Media/ImagesMedia.cs b/src/CMS/Models/Media/ImagesMedia.cs new file mode 100644 index 00000000..11039c84 --- /dev/null +++ b/src/CMS/Models/Media/ImagesMedia.cs @@ -0,0 +1,10 @@ +using EPiServer.Framework.DataAnnotations; + +namespace CMS.Models.Media; + +[ContentType(DisplayName = "ImagesMedia", GUID = "a4afb648-f7c0-4207-8ac0-f0c532de99ca", + Description = "Used for generic image types")] +[MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")] +public class ImagesMedia : ImageData +{ +} \ No newline at end of file diff --git a/src/CMS/Models/Pages/FirstPage.cs b/src/CMS/Models/Pages/FirstPage.cs new file mode 100644 index 00000000..13e3f8b7 --- /dev/null +++ b/src/CMS/Models/Pages/FirstPage.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models.Pages; + +[ContentType(DisplayName = "FirstPage", GUID = "5448b99f-4e2b-4b8b-ae8c-63a3e94b09ec", Description = "First page type for creating pages.")] +public class FirstPage : SitePageData +{ + public virtual string Heading { get; set; } + + public virtual string MainIntro { get; set; } + + [CultureSpecific] + [Display( + Name = "Main body", + Description = "The main body editor area lets you insert text and images into a page.", + GroupName = SystemTabNames.Content, + Order = 10)] + public virtual XhtmlString MainBody { get; set; } + + [Display( + GroupName = FirstPageSystemTabNames.Content, + Name = "This text can you have in XML instead", + Description = "This text can you have in XML instead" + )] + public virtual ContentArea MainContentArea { get; set; } + + public virtual string ReuseName { get; set; } +} + +public static class FirstPageSystemTabNames +{ + public const string Content = "KhaiTestInformation"; +} \ No newline at end of file diff --git a/src/CMS/Models/Pages/ImagePage.cs b/src/CMS/Models/Pages/ImagePage.cs new file mode 100644 index 00000000..316aced2 --- /dev/null +++ b/src/CMS/Models/Pages/ImagePage.cs @@ -0,0 +1,22 @@ +using EPiServer.Web; +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models.Pages; + +[ContentType( + DisplayName = "ImagePage", + GUID = "61f028c8-be3d-4942-b58c-65ea8b28e7b6", + Description = "Description for this image page type")] +public class ImagePage : PageData +{ + [CultureSpecific] + [Display( + Name = "Page image", + Description = "Link to image that will be displayed on the page.", + GroupName = SystemTabNames.Content, + Order = 1)] + [UIHint(UIHint.Image)] + public virtual ContentReference Image { get; set; } + + public virtual string BlobPathToReadWrite { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Models/Pages/SecondPage.cs b/src/CMS/Models/Pages/SecondPage.cs new file mode 100644 index 00000000..ec605514 --- /dev/null +++ b/src/CMS/Models/Pages/SecondPage.cs @@ -0,0 +1,34 @@ +using EPiServer.SpecializedProperties; +using EPiServer.Web; +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models.Pages; + +[ContentType(DisplayName = "SecondPage", GUID = "d2398c66-3f86-405a-bc24-8a47daa6cc1d", Description = "Second page type for creating pages.")] +public class SecondPage : SitePageData +{ + [CultureSpecific] + [Display( + Name = "Main body", + Description = "The main body editor area lets you insert text and images into a page.", + GroupName = SystemTabNames.Content, + Order = 10)] + public virtual XhtmlString MainBody { get; set; } + + public virtual string Heading { get; set; } + + public virtual CategoryList CategoryList { get; set; } + public virtual ContentArea ContentArea { get; set; } + public virtual IList ContentReferenceList { get; set; } + public virtual ContentReference ContentReference { get; set; } + public virtual ContentReference TargetPage { get; set; } + + [UIHint(UIHint.Image)] + public virtual ContentReference Image { get; set; } + public virtual PageReference PageReference { get; set; } + public virtual LinkItem LinkItem { get; set; } + public virtual LinkItemCollection LinkItemCollection { get; set; } + public virtual Url Url { get; set; } + public virtual XhtmlString XhtmlString { get; set; } + // other properties +} \ No newline at end of file diff --git a/src/CMS/Models/Pages/SitePageData.cs b/src/CMS/Models/Pages/SitePageData.cs new file mode 100644 index 00000000..65b9a037 --- /dev/null +++ b/src/CMS/Models/Pages/SitePageData.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models.Pages; + +public abstract class SitePageData : PageData +{ + [Display(GroupName = "SEO", Order = 200, Name = "Search keywords")] + public virtual string MetaKeywords { get; set; } + + public virtual ContentReference Image { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Models/SiteContentType.cs b/src/CMS/Models/SiteContentType.cs new file mode 100644 index 00000000..b6b68d1c --- /dev/null +++ b/src/CMS/Models/SiteContentType.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace CMS.Models; + +/// +/// Attribute used for site content types to set default attribute values +/// +public class SiteContentType : ContentTypeAttribute +{ + public SiteContentType() + { + GroupName = Globals.GroupNames.Default; + } +} \ No newline at end of file diff --git a/src/CMS/Models/SiteImageUrl.cs b/src/CMS/Models/SiteImageUrl.cs new file mode 100644 index 00000000..d1304c63 --- /dev/null +++ b/src/CMS/Models/SiteImageUrl.cs @@ -0,0 +1,20 @@ +namespace CMS.Models; + +/// +/// Attribute to set the default thumbnail for site page and block types +/// +public class SiteImageUrl : ImageUrlAttribute +{ + /// + /// The parameterless constructor will initialize a SiteImageUrl attribute with a default thumbnail + /// + public SiteImageUrl() + : base("/gfx/page-type-thumbnail.png") + { + } + + public SiteImageUrl(string path) + : base(path) + { + } +} \ No newline at end of file diff --git a/src/CMS/Models/ViewModels/IPageViewModel.cs b/src/CMS/Models/ViewModels/IPageViewModel.cs new file mode 100644 index 00000000..a9c84b0f --- /dev/null +++ b/src/CMS/Models/ViewModels/IPageViewModel.cs @@ -0,0 +1,12 @@ +using CMS.Models.Pages; + +namespace CMS.Models.ViewModels; + +public interface IPageViewModel where T : SitePageData +{ + T CurrentPage { get; } + + LayoutModel Layout { get; set; } + + IContent Section { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Models/ViewModels/LayoutModel.cs b/src/CMS/Models/ViewModels/LayoutModel.cs new file mode 100644 index 00000000..9d750132 --- /dev/null +++ b/src/CMS/Models/ViewModels/LayoutModel.cs @@ -0,0 +1,34 @@ +using CMS.Models.Blocks; +using EPiServer.SpecializedProperties; +using Microsoft.AspNetCore.Html; + +namespace CMS.Models.ViewModels; + +public class LayoutModel +{ + public SiteLogotypeBlock Logotype { get; set; } + + public IHtmlContent LogotypeLinkUrl { get; set; } + + public bool HideHeader { get; set; } + + public bool HideFooter { get; set; } + + public LinkItemCollection ProductPages { get; set; } + + public LinkItemCollection CompanyInformationPages { get; set; } + + public LinkItemCollection NewsPages { get; set; } + + public LinkItemCollection CustomerZonePages { get; set; } + + public bool LoggedIn { get; set; } + + public HtmlString LoginUrl { get; set; } + + public HtmlString LogOutUrl { get; set; } + + public HtmlString SearchActionUrl { get; set; } + + public bool IsInReadonlyMode { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Models/ViewModels/PageViewModel.cs b/src/CMS/Models/ViewModels/PageViewModel.cs new file mode 100644 index 00000000..3465a99f --- /dev/null +++ b/src/CMS/Models/ViewModels/PageViewModel.cs @@ -0,0 +1,29 @@ +using CMS.Models.Pages; + +namespace CMS.Models.ViewModels; + +public class PageViewModel : IPageViewModel where T : SitePageData +{ + public PageViewModel(T currentPage) + { + CurrentPage = currentPage; + } + + public T CurrentPage { get; private set; } + + public LayoutModel Layout { get; set; } + + public IContent Section { get; set; } +} + +public static class PageViewModel +{ + /// + /// Returns a PageViewModel of type . + /// + /// + /// Convenience method for creating PageViewModels without having to specify the type as methods can use type inference while constructors cannot. + /// + public static PageViewModel Create(T page) + where T : SitePageData => new(page); +} \ No newline at end of file diff --git a/src/CMS/Models/ViewModels/PreviewModel.cs b/src/CMS/Models/ViewModels/PreviewModel.cs new file mode 100644 index 00000000..1a0ec613 --- /dev/null +++ b/src/CMS/Models/ViewModels/PreviewModel.cs @@ -0,0 +1,17 @@ +using CMS.Models.Pages; + +namespace CMS.Models.ViewModels; + +public class PreviewModel : PageViewModel +{ + public PreviewModel(SitePageData currentPage, IContent previewContent) : base(currentPage) + { + PreviewContentArea = new ContentArea(); + PreviewContentArea.Items.Add(new ContentAreaItem + { + ContentLink = previewContent.ContentLink + }); + } + + public ContentArea PreviewContentArea { get; set; } +} \ No newline at end of file diff --git a/src/CMS/Resources/Translations/ContentTypeNames.xml b/src/CMS/Resources/Translations/ContentTypeNames.xml new file mode 100644 index 00000000..2cf0c57a --- /dev/null +++ b/src/CMS/Resources/Translations/ContentTypeNames.xml @@ -0,0 +1,62 @@ + + + + + + Group name from XML file + + + + + + + + + Reuse Name text from ContentTypeNames.xml + + + + + + + + + + + Disable indexing + Prevents the page from being indexed by search engines + + + + + + + + + + A description of the page type + + + Name text from XML + Description text from XML + + + + + + + + + + Mô tả của loại trang + + + Tên văn bản từ XML + Văn bản mô tả từ XML + + + + + + + \ No newline at end of file diff --git a/src/CMS/Resources/Translations/Views.xml b/src/CMS/Resources/Translations/Views.xml index 85c41378..50c048cf 100644 --- a/src/CMS/Resources/Translations/Views.xml +++ b/src/CMS/Resources/Translations/Views.xml @@ -1,4 +1,9 @@ - + + my string + + my other string + + diff --git a/src/CMS/Startup.cs b/src/CMS/Startup.cs index 3b537787..ea2d35a2 100644 --- a/src/CMS/Startup.cs +++ b/src/CMS/Startup.cs @@ -1,8 +1,18 @@ using EPiServer.Cms.Shell; using EPiServer.Cms.UI.AspNetIdentity; +using EPiServer.Framework.Localization.XmlResources; using EPiServer.Scheduler; +using EPiServer.Security; using EPiServer.ServiceLocation; +using EPiServer.Web; using EPiServer.Web.Routing; +using Microsoft.AspNetCore.Authentication; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using System.Collections.Specialized; +using System.Security.Claims; +using System.Text; +using static CMS.Globals; namespace CMS { @@ -22,13 +32,41 @@ public void ConfigureServices(IServiceCollection services) AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(_webHostingEnvironment.ContentRootPath, "App_Data")); services.Configure(options => options.Enabled = false); + + services.Configure(options => + { + options + .Add("full", "/displayoptions/full", ContentAreaTags.FullWidth, "", "epi-icon__layout--full") + .Add("wide", "/displayoptions/wide", ContentAreaTags.TwoThirdsWidth, "", "epi-icon__layout--two-thirds") + .Add("narrow", "/displayoptions/narrow", ContentAreaTags.OneThirdWidth, "", "epi-icon__layout--one-third"); + }); + + services.AddEmbeddedLocalization(); + + //services.AddLocalizationProvider(o => + // { + // o[FileXmlLocalizationProvider.PhysicalPathKey] = @"c:\temp\resourceFolder"; + // }); + + //services.AddFileBlobProvider("myFileBlobProvider", @"c:\path\to\file\blobs"); + //services.AddBlobProvider("myCustomBlobProvider", defaultProvider: false); + //services.Configure(o => + //{ + // o.AddProvider("anotherCustomBlobProvider"); + // o.DefaultProvider = "anotherCustomBlobProvider"; + //}); + + ConfigureAzureADService(services); + //ConfigureMixedAuthenticationService(services); } services - .AddCmsAspNetIdentity() + .AddCmsAspNetIdentity()//(configureSqlServerOptions: o => o.MigrationsAssembly("CMS")); .AddCms() .AddAdminUserRegistration() - .AddEmbeddedLocalization(); + .AddEmbeddedLocalization() + .AddCmsTagHelpers(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -48,5 +86,100 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) endpoints.MapContent(); }); } + + private void ConfigureAzureADService(IServiceCollection services) + { + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = "azure-cookie"; + options.DefaultChallengeScheme = "azure"; + }) + .AddCookie("azure-cookie", options => + { + options.Events.OnSignedIn = async ctx => + { + if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity) + { + // Syncs user and roles so they are available to the CMS + var synchronizingUserService = ctx.HttpContext.RequestServices.GetRequiredService(); + await synchronizingUserService.SynchronizeAsync(claimsIdentity); + } + }; + }) + .AddOpenIdConnect("azure", options => + { + options.SignInScheme = "azure-cookie"; + options.SignOutScheme = "azure-cookie"; + options.ResponseType = OpenIdConnectResponseType.Code; + options.CallbackPath = "/signin-oidc"; + options.UsePkce = true; + + // If Azure AD is register for multi-tenant + //options.Authority = "https://login.microsoftonline.com/" + "common" + "/v2.0"; + options.Authority = "https://login.microsoftonline.com/" + "tenant id" + "/v2.0"; + options.ClientId = "client id"; + + options.Scope.Clear(); + options.Scope.Add(OpenIdConnectScope.OpenIdProfile); + options.Scope.Add(OpenIdConnectScope.OfflineAccess); + options.Scope.Add(OpenIdConnectScope.Email); + options.MapInboundClaims = false; + + options.TokenValidationParameters = new TokenValidationParameters + { + RoleClaimType = ClaimTypes.Role, + NameClaimType = "preferred_username", + ValidateIssuer = false + }; + + options.Events.OnRedirectToIdentityProvider = ctx => + { + // Prevent redirect loop + if (ctx.Response.StatusCode == 401) + { + ctx.HandleResponse(); + } + + return Task.CompletedTask; + }; + + options.Events.OnAuthenticationFailed = context => + { + context.HandleResponse(); + context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message)); + return Task.CompletedTask; + }; + }); + } + + private void ConfigureMixedAuthenticationService(IServiceCollection services) + { + services + .AddAuthentication(options => + { + options.DefaultScheme = Schemes.Policy; + options.DefaultAuthenticateScheme = Schemes.Policy; + }) + .AddCookie(Schemes.Default) + .AddCookie(Schemes.Another) + .AddOpenIdConnect(Schemes.Oidc, options => + { + options.SignInScheme = Schemes.Another; + options.SignOutScheme = Schemes.Another; + }) + .AddPolicyScheme(Schemes.Policy, null, options => + { + options.ForwardDefaultSelector = ctx => + { + if (ctx.Request.Path.StartsWithSegments("episerver", StringComparison.OrdinalIgnoreCase)) + { + return Schemes.Oidc; + } + return Schemes.Default; + }; + }) + ; + } } } \ No newline at end of file diff --git a/src/CMS/Views/FirstPage/Index.cshtml b/src/CMS/Views/FirstPage/Index.cshtml new file mode 100644 index 00000000..6d29e21f --- /dev/null +++ b/src/CMS/Views/FirstPage/Index.cshtml @@ -0,0 +1,10 @@ +@using EPiServer.Web.Mvc.Html; +@model CMS.Models.Blocks.ThirdBlock + +
+ +

@Html.PropertyFor(x => x.Heading)

+ + + +
\ No newline at end of file diff --git a/src/CMS/Views/FirstPageMobile/Index.cshtml b/src/CMS/Views/FirstPageMobile/Index.cshtml new file mode 100644 index 00000000..6d29e21f --- /dev/null +++ b/src/CMS/Views/FirstPageMobile/Index.cshtml @@ -0,0 +1,10 @@ +@using EPiServer.Web.Mvc.Html; +@model CMS.Models.Blocks.ThirdBlock + +
+ +

@Html.PropertyFor(x => x.Heading)

+ + + +
\ No newline at end of file diff --git a/src/CMS/Views/ImagePage/Index.cshtml b/src/CMS/Views/ImagePage/Index.cshtml new file mode 100644 index 00000000..0aec1111 --- /dev/null +++ b/src/CMS/Views/ImagePage/Index.cshtml @@ -0,0 +1,8 @@ +@using EPiServer.Core +@using EPiServer.Web.Mvc.Html + +@model CMS.Models.Pages.ImagePage + +
+ +
\ No newline at end of file diff --git a/src/CMS/Views/Preview/Index.cshtml b/src/CMS/Views/Preview/Index.cshtml new file mode 100644 index 00000000..777ccb21 --- /dev/null +++ b/src/CMS/Views/Preview/Index.cshtml @@ -0,0 +1,23 @@ +@using CMS.Models.ViewModels; +@using EPiServer.Web.Mvc.Html; + +@model PreviewModel + +Preview: +
+
+ @Html.PropertyFor(m => m.PreviewContentArea) +
+
+ +
+
+ @Html.PropertyFor(m => m.PreviewContentArea) +
+
+ +
+
+ @Html.PropertyFor(m => m.PreviewContentArea) +
+
\ No newline at end of file diff --git a/src/CMS/Views/SecondPage/Index.cshtml b/src/CMS/Views/SecondPage/Index.cshtml new file mode 100644 index 00000000..9a2e48e3 --- /dev/null +++ b/src/CMS/Views/SecondPage/Index.cshtml @@ -0,0 +1,120 @@ +@using EPiServer.Web.Mvc.Html; + +@model CMS.Models.Pages.SecondPage; + +ShowBanner: @Html.FullRefreshPropertiesMetaData(new[] { "ShowBanner" }) + +

x.Heading)> + /* Render the Heading property, unless it it empty, then use PageName as fallback */ + @Model.Heading ?? @Model.PageName +

+ +@Html.PropertyFor(x => x.MainBody) + +Category list examples + + + +ContentReference examples + + + + + +Custom text + + +

+ + + + + My Image + + +PageReference examples + + + + +

+ +ContentReference list examples + +