oqtane.framework/Oqtane.Client/UI/SiteRouter.razor

657 lines
27 KiB
Plaintext

@using System.Diagnostics.CodeAnalysis
@using System.Net
@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 ICookieConsentService CookieConsentService
@inject IJSRuntime JSRuntime
@implements IHandleAfterRender
@implements IDisposable
@if (!string.IsNullOrEmpty(_error))
{
<ModuleMessage Message="@_error" Type="@MessageType.Warning" />
}
@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<PageState> 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<Module> 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 settingName = $"PersonalizedPagePath:{page.SiteId}:{page.PageId}";
var path = (user.Settings.ContainsKey(settingName)) ? user.Settings[settingName] : Utilities.GetFriendlyUrl(user.Username);
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + path, 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, action);
// load additional metadata for modules
(page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
//cookie consent
var _allowCookies = PageState?.AllowCookies;
if(!_allowCookies.HasValue)
{
var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
_allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout");
}
// 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,
AllowCookies = _allowCookies.GetValueOrDefault(true)
};
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, string action)
{
try
{
page.Panes = new List<string>();
page.Resources = new List<Resource>();
// 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), theme.Fingerprint);
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, theme.Fingerprint);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
var settingsType = Type.GetType(theme.ThemeSettingsType);
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint);
}
}
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<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias)
{
var paneindex = new Dictionary<string, int>();
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), module.ModuleDefinition.Fingerprint);
// 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, module.ModuleDefinition?.Fingerprint);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{
// module settings component
var settingsType = "";
if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
settingsType = module.ModuleDefinition.SettingsType;
}
else
{
// legacy support - module settings type determined by convention
settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
}
moduletype = Type.GetType(settingsType, false, true);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint);
}
// container settings component
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
moduletype = Type.GetType(theme.ContainerSettingsType);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint);
}
}
}
// 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<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint)
{
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, fingerprint));
}
}
}
}
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);
}
}
}
}