@using System.Diagnostics.CodeAnalysis @using System.Net @using Microsoft.AspNetCore.Http @using System.Globalization @using System.Security.Claims @namespace Oqtane.UI @inject AuthenticationStateProvider AuthenticationStateProvider @inject SiteState SiteState @inject NavigationManager NavigationManager @inject INavigationInterception NavigationInterception @inject ISyncService SyncService @inject ISiteService SiteService @inject IPageService PageService @inject IUserService UserService @inject IUrlMappingService UrlMappingService @inject ILogService LogService @inject ISettingService SettingService @inject IJSRuntime JSRuntime @implements IHandleAfterRender @implements IDisposable @if (!string.IsNullOrEmpty(_error)) { } @DynamicComponent @code { private string _absoluteUri; private bool _isInternalNavigation = false; private bool _navigationInterceptionEnabled; private PageState _pagestate; private string _error = ""; [Parameter] public string RenderMode { get; set; } [Parameter] public string Runtime { get; set; } [CascadingParameter] PageState PageState { get; set; } [Parameter] public Action OnStateChange { get; set; } private RenderFragment DynamicComponent { get; set; } protected override void OnInitialized() { _absoluteUri = NavigationManager.Uri; NavigationManager.LocationChanged += LocationChanged; DynamicComponent = builder => { if (PageState != null && !PageState.Refresh) { builder.OpenComponent(0, typeof(ThemeBuilder)); builder.CloseComponent(); } }; } public void Dispose() { NavigationManager.LocationChanged -= LocationChanged; } protected override async Task OnParametersSetAsync() { if (PageState == null || PageState.Refresh) { await Refresh(); } } private async void LocationChanged(object sender, LocationChangedEventArgs args) { _absoluteUri = args.Location; _isInternalNavigation = true; await Refresh(); } Task IHandleAfterRender.OnAfterRenderAsync() { if (!_navigationInterceptionEnabled) { _navigationInterceptionEnabled = true; return NavigationInterception.EnableNavigationInterceptionAsync(); } return Task.CompletedTask; } [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] private async Task Refresh() { Site site = null; Page page = null; List modules = null; User user = null; var editmode = false; var refresh = false; var lastsyncdate = DateTime.MinValue; var visitorId = -1; _error = ""; Route route = new Route(_absoluteUri, SiteState.Alias.Path); int moduleid = int.Parse(route.ModuleId, CultureInfo.InvariantCulture); var action = route.Action; var querystring = Utilities.ParseQueryString(route.Query); var returnurl = ""; if (querystring.ContainsKey("returnurl")) { returnurl = WebUtility.UrlDecode(querystring["returnurl"]); if (!returnurl.StartsWith("/")) { // urls which are not relative are vulnerable to open redirects or XSS returnurl = ""; querystring["returnurl"] = ""; } } // reload the client application from the server if there is a forced reload if (querystring.ContainsKey("reload")) { if (querystring.ContainsKey("reload") && querystring["reload"] == "post") { if (PageState.RenderMode == RenderModes.Interactive) { // post back so that the cookies are set correctly - required on any change to the principal var interop = new Interop(JSRuntime); var fields = new { returnurl = "/" + NavigationManager.ToBaseRelativePath(_absoluteUri) }; string url = Utilities.TenantUrl(SiteState.Alias, "/pages/external/"); await interop.SubmitForm(url, fields); return; } else { NavigationManager.NavigateTo(_absoluteUri.Replace("?reload=post", "").Replace("&reload=post", ""), true); return; } } else { NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", "").Replace("&reload", ""), true); return; } } // the refresh parameter is used to refresh the client-side PageState if (querystring.ContainsKey("refresh")) { refresh = true; } // verify user is authenticated for current site var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey)) { // get user var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value); user = await UserService.GetUserAsync(userid, SiteState.Alias.SiteId); if (user != null) { user.IsAuthenticated = authState.User.Identity.IsAuthenticated; } } if (PageState != null) { editmode = PageState.EditMode; lastsyncdate = PageState.LastSyncDate; visitorId = PageState.VisitorId; } if (PageState != null && PageState.RenderMode == RenderModes.Interactive) { // process any sync events (for synchronizing the client application with the server) var sync = await SyncService.GetSyncEventsAsync(lastsyncdate); lastsyncdate = sync.SyncDate; if (sync.SyncEvents.Any()) { // reload client application if server was restarted if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.Host)) { NavigationManager.NavigateTo(_absoluteUri, true); return; } // refresh PageState when site information has changed if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Refresh && item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId)) { refresh = true; } } } // get site if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId) { site = await SiteService.GetSiteAsync(SiteState.Alias.SiteId); refresh = true; } else { site = PageState.Site; } if (site != null) { if (Runtime == Runtimes.Hybrid && !site.Hybrid) { _error = "Hybrid Integration Is Not Enabled For This Site"; return; } if (refresh || PageState == null || PageState.Page.Path != route.PagePath) { page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); } else { page = PageState.Page; } if (page == null && route.PagePath == "") // naked path refers to site home page { if (site.HomePageId != null) { page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId); } if (page == null) { // fallback to use the first page in the collection page = site.Pages.FirstOrDefault(); } } if (page == null) { page = await PageService.GetPageAsync(route.PagePath, site.SiteId); } else { // look for personalized page if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)) { var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId); if (personalized != null) { // redirect to the personalized page NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, personalized.Path, ""), false); return; } } } // check if user is authorized to view page if (page != null && UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))) { // edit mode if (user != null) { var editpageid = user.Settings.ContainsKey("CP-editmode") ? int.Parse(user.Settings["CP-editmode"], CultureInfo.InvariantCulture) : -1; if ((querystring.ContainsKey("edit") && querystring["edit"] == "true") || page.PageId == editpageid) { editmode = true; } else { if (editpageid != -1) { // reset edit mode page await SettingService.AddOrUpdateSettingAsync(EntityNames.User, user.UserId, "CP-editmode", "-1", false); } } } // get modules for current page if (refresh || PageState.Modules == null || !PageState.Modules.Any() || PageState.Modules.First().PageId != page.PageId) { modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId); } else { modules = PageState.Modules; } // load additional metadata for current page page = ProcessPage(page, site, user, SiteState.Alias); // load additional metadata for modules (page, modules) = ProcessModules(page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias); // populate page state (which acts as a client-side cache for subsequent requests) _pagestate = new PageState { Alias = SiteState.Alias, Site = site, Page = page, Modules = modules, User = user, Uri = new Uri(_absoluteUri, UriKind.Absolute), Route = route, QueryString = querystring, UrlParameters = route.UrlParameters, ModuleId = moduleid, Action = action, EditMode = editmode, LastSyncDate = lastsyncdate, RenderMode = RenderMode, Runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime), VisitorId = visitorId, RemoteIPAddress = SiteState.RemoteIPAddress, ReturnUrl = returnurl, IsInternalNavigation = _isInternalNavigation, RenderId = Guid.NewGuid(), Refresh = false }; OnStateChange?.Invoke(_pagestate); if (PageState.RenderMode == RenderModes.Interactive) { await ScrollToFragment(_pagestate.Uri); } } else { if (page == null) { // check for url mapping var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath); if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) { var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query; NavigationManager.NavigateTo(url, false); return; } } else { if (user == null) { // redirect to login page if user not logged in as they may need to be authenticated NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery))); return; } } // page not found or user does not have sufficient access if (route.PagePath != "404") { // redirect to 404 page NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", "")); } else { // redirect to home page as a fallback NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", "")); } } } else { // site does not exist } } private Page ProcessPage(Page page, Site site, User user, Alias alias) { try { page.Panes = new List(); page.Resources = new List(); // validate theme if (string.IsNullOrEmpty(page.ThemeType)) { page.ThemeType = site.DefaultThemeType; } var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType)); if (theme == null) { // fallback to default Oqtane theme page.ThemeType = Constants.DefaultTheme; theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType)); } Type themetype = Type.GetType(page.ThemeType); string panes = ""; if (themetype != null) { // get resources for theme (ITheme) page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); var themeobject = Activator.CreateInstance(themetype) as IThemeControl; if (themeobject != null) { if (!string.IsNullOrEmpty(themeobject.Panes)) { panes = themeobject.Panes; } // get resources for theme control page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace); } } if (!string.IsNullOrEmpty(panes)) { page.Panes = panes.Replace(";", ",").Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); if (!page.Panes.Contains(PaneNames.Default) && !page.Panes.Contains(PaneNames.Admin)) { _error = "The Current Theme Does Not Contain A Default Or Admin Pane"; } } else { page.Panes.Add(PaneNames.Admin); _error = "The Current Theme Does Not Contain Any Panes"; } } catch { // error loading theme or layout } return page; } private (Page Page, List Modules) ProcessModules(Page page, List modules, int moduleid, string action, string defaultcontainertype, Alias alias) { var paneindex = new Dictionary(); foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid)) { var typename = Constants.ErrorModule; // initialize module control properties module.SecurityAccessLevel = SecurityAccessLevel.Host; module.ControlTitle = ""; module.Actions = ""; module.UseAdminContainer = false; module.PaneModuleIndex = -1; module.PaneModuleCount = 0; if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) { page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); // handle default action if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) { action = module.ModuleDefinition.DefaultAction; } // get typename template typename = module.ModuleDefinition.ControlTypeTemplate; if (module.ModuleDefinition.ControlTypeRoutes != "") { // process custom action routes foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(';', StringSplitOptions.RemoveEmptyEntries)) { if (route.StartsWith(action + "=")) { typename = route.Replace(action + "=", ""); break; } } } } // create typename if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase)) { typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action); } else { typename = typename.Replace(Constants.ActionToken, action); } // ensure component exists and implements IModuleControl module.ModuleType = ""; Type moduletype = Type.GetType(typename, false, true); // case insensitive if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl))) { module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name } if (moduletype != null && module.ModuleType != "") { // retrieve module component resources var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; module.RenderMode = moduleobject.RenderMode; module.Prerender = moduleobject.Prerender; page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); if (action.ToLower() == "settings" && module.ModuleDefinition != null) { // settings components are embedded within a framework settings module moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); } } // additional metadata needed for admin components if (module.ModuleId == moduleid && action != "") { module.ControlTitle = moduleobject.Title; module.SecurityAccessLevel = moduleobject.SecurityAccessLevel; module.Actions = moduleobject.Actions; module.UseAdminContainer = moduleobject.UseAdminContainer; } } // validate that module's pane exists in current page if (page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1) { // fallback to default pane if it exists if (page.Panes.FindIndex(item => item.Equals(PaneNames.Default, StringComparison.OrdinalIgnoreCase)) != -1) { module.Pane = PaneNames.Default; } else // otherwise admin pane (legacy) { module.Pane = PaneNames.Admin; } } // calculate module position within pane if (paneindex.ContainsKey(module.Pane.ToLower())) { paneindex[module.Pane.ToLower()] += 1; } else { paneindex.Add(module.Pane.ToLower(), 0); } module.PaneModuleIndex = paneindex[module.Pane.ToLower()]; // container fallback if (string.IsNullOrEmpty(module.ContainerType)) { module.ContainerType = defaultcontainertype; } } foreach (Module module in modules.Where(item => item.PageId == page.PageId)) { if (paneindex.ContainsKey(module.Pane.ToLower())) { module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1; } } return (page, modules); } private List ManagePageResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name) { if (resources != null) { foreach (var resource in resources) { if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site) { if (resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); } if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) { resource.Url = alias.BaseUrl + resource.Url; } // ensure resource does not exist already if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) { pageresources.Add(resource.Clone(level, name)); } } } } return pageresources; } private async Task ScrollToFragment(Uri uri) { var fragment = uri.Fragment; if (fragment.StartsWith('#')) { // handle text fragment (https://example.org/#test:~:text=foo) var id = fragment.Substring(1); var index = id.IndexOf(":~:", StringComparison.Ordinal); if (index > 0) { id = id.Substring(0, index); } if (!string.IsNullOrEmpty(id)) { var interop = new Interop(JSRuntime); await interop.ScrollToId(id); } } } }