Compare commits

...

113 Commits
v6.1.0 ... lel

Author SHA1 Message Date
89a166ea9b 2 neue Felder hinzugefügt beim Registrieren ( Fachrichtung und Abschlussjahrgang) 2025-07-21 16:49:17 +02:00
391827222e Update README.md 2025-03-15 18:08:36 +00:00
c1721bd1a1 Update README.md 2025-03-15 18:08:00 +00:00
f6630ae241 Update README.md 2025-03-15 18:07:14 +00:00
424cab64a8 NEW: Docker builds 2025-03-15 18:59:18 +01:00
05d2096fb8 Update README.md 2025-03-12 14:34:31 -04:00
7683af81bc Update README.md 2025-03-12 14:22:43 -04:00
bad10b3812 6.1.1 Release
6.1.1 Release
2025-03-12 14:21:41 -04:00
9f9522c2ed 6.1.1 Release
6.1.1 Release
2025-03-12 14:21:15 -04:00
62879c3e52 Merge pull request #5162 from sbwalker/dev
upgrade to .NET 9.0.3
2025-03-11 14:36:14 -04:00
45610f8dd7 upgrade to .NET 9.0.3 2025-03-11 14:35:59 -04:00
18102cbd78 Merge pull request #5160 from sbwalker/dev
sort endpoints by route
2025-03-11 13:11:34 -04:00
262d6a1529 sort endpoints by route 2025-03-11 13:11:19 -04:00
1124ddaf90 Merge pull request #5157 from zyhfish/task/fix-5156
Fix #5156: update the bind event to oninput.
2025-03-11 11:51:38 -04:00
981add3872 Merge pull request #5158 from sbwalker/dev
rename Cache service to OutputCache
2025-03-11 11:48:58 -04:00
8d4b30140e rename Cache service to OutputCache 2025-03-11 11:48:43 -04:00
Ben
b9c59137a8 Fix #5156: update the bind event to oninput. 2025-03-11 22:58:06 +08:00
0b1c7e06ca Update README.md 2025-03-10 10:56:41 -04:00
fcaf80cba6 Update README.md 2025-03-10 10:56:11 -04:00
6358b9eabb Merge pull request #5155 from sbwalker/dev
added Logout Everywhere option to User Settings
2025-03-10 10:53:25 -04:00
70a3fab1ff added Logout Everywhere option to User Settings 2025-03-10 10:53:05 -04:00
bdf86ace86 Merge pull request #5152 from sbwalker/dev
upgrade to ImageSharp 3.1.7 due to security vulnerability
2025-03-07 14:17:02 -05:00
d57132d1e4 upgrade to ImageSharp 3.1.7 due to security vulnerability 2025-03-07 14:16:47 -05:00
a6e87abf99 Merge pull request #5151 from sbwalker/dev
allow site settings to be overidden at host level
2025-03-07 14:15:31 -05:00
f1771610fe allow site settings to be overidden at host level 2025-03-07 14:15:16 -05:00
a88ea9780f Update README.md 2025-03-06 15:45:30 -05:00
bca0866d72 Update README.md 2025-03-06 15:43:59 -05:00
cebed93abf Update README.md 2025-03-06 15:42:48 -05:00
ee2b2e3569 Update README.md 2025-03-06 15:40:07 -05:00
cb8e9ee244 Update README.md 2025-03-06 15:38:18 -05:00
486184b16c Update README.md 2025-03-06 15:36:28 -05:00
9f9bd1988f Merge pull request #5149 from sbwalker/dev
update based on changes suggested by @adefwebserver
2025-03-06 15:25:37 -05:00
ba1bfd1bc0 update based on changes suggested by @adefwebserver 2025-03-06 15:25:25 -05:00
e12926e971 Merge pull request #5148 from sbwalker/dev
allow page and module settings to be included in site templates, improve terms and privacy default content, add Settings for HtmlText module
2025-03-06 14:46:38 -05:00
5b4db0de3b allow page and module settings to be included in site templates, improve terms and privacy default content, add Settings for HtmlText module 2025-03-06 14:46:17 -05:00
70ff55faa6 Merge pull request #5147 from sbwalker/dev
modify terminology
2025-03-06 09:43:55 -05:00
f2bd47d8bc modify terminology 2025-03-06 09:43:40 -05:00
e2b9c9e98e Merge pull request #5146 from sbwalker/dev
prepare for 6.1.1
2025-03-05 15:02:39 -05:00
b0791a594f prepare for 6.1.1 2025-03-05 15:02:12 -05:00
ea5eaa6ed2 Merge pull request #5145 from sbwalker/dev
update to .NET 9.0.2
2025-03-05 14:55:00 -05:00
ec3fd1d585 update to .NET 9.0.2 2025-03-05 14:54:46 -05:00
d76de22977 Merge pull request #5144 from sbwalker/dev
modify localization text
2025-03-05 10:45:17 -05:00
c0e3483cc7 modify localization text 2025-03-05 10:44:34 -05:00
0994cdf3b6 Merge pull request #5141 from sbwalker/dev
sync interop.js changes with .NET MAUI
2025-03-04 16:25:24 -05:00
a76fd82262 sync interop.js changes with .NET MAUI 2025-03-04 16:25:10 -05:00
2f919c7d69 Merge pull request #5140 from sbwalker/dev
fix regression issue with Search component
2025-03-04 15:45:16 -05:00
f12592731b fix regression issue with Search component 2025-03-04 15:45:02 -05:00
4a20e1a25d Merge pull request #5138 from zyhfish/task/improve-middle-screen-view
improve the styles in middle screen size.
2025-03-04 15:42:37 -05:00
cc7111c3ff Merge pull request #5139 from sbwalker/dev
change module title for Terms
2025-03-04 15:40:58 -05:00
81972aed62 change module title for Terms 2025-03-04 15:40:44 -05:00
Ben
5e2092c6d4 improve the styles in middle screen size. 2025-03-04 23:31:21 +08:00
d136f8ac91 Merge pull request #5137 from sbwalker/dev
add nonce support
2025-03-03 16:49:41 -05:00
5b23917940 add nonce support 2025-03-03 16:49:28 -05:00
2cda0a3798 Merge pull request #5136 from sbwalker/dev
modify cookie consent text
2025-03-03 15:37:46 -05:00
f315ad1ce9 modify cookie consent text 2025-03-03 15:37:31 -05:00
49f1c273c2 Merge pull request #5134 from sbwalker/dev
add terms to upgrademanager
2025-03-03 13:36:47 -05:00
8518476c87 add terms to upgrademanager 2025-03-03 13:36:32 -05:00
a34ed756db Merge pull request #5132 from mdmontesinos/sitemap-cache
resolves #4899: output cache for sitemap
2025-03-03 13:22:46 -05:00
a48232c4e3 Merge pull request #5133 from sbwalker/dev
add default privacy and terms
2025-03-03 13:22:33 -05:00
ac65e38390 add default privacy and terms 2025-03-03 13:22:17 -05:00
eab3a753f5 resolves #4899: output cache for sitemap 2025-03-03 17:54:33 +01:00
9e5922e121 Merge pull request #5130 from zyhfish/task/fix-4936
Fix #4936: set the allow cookie value when refresh state.
2025-03-03 07:57:42 -05:00
Ben
bf57b23776 update the page state after policy changed. 2025-03-01 15:05:23 +08:00
Ben
8f4a20fd46 Fix #4936: set the allow cookie value when refresh state. 2025-03-01 14:16:42 +08:00
b3716da5ac Merge pull request #5128 from zyhfish/task/fix-4936-new
move the styles to app.css.
2025-02-28 13:36:58 -05:00
2c129fd800 Merge pull request #5129 from sbwalker/dev
localization text change
2025-02-28 13:36:49 -05:00
6fb18e7a25 localization text change 2025-02-28 13:36:34 -05:00
Ben
38d28d6944 move the styles to app.css. 2025-02-28 23:53:53 +08:00
a187e1a7a2 Merge pull request #5127 from sbwalker/dev
fix RESX issue
2025-02-28 10:52:18 -05:00
7d7a19c7c2 fix RESX issue 2025-02-28 10:52:06 -05:00
6a2ae2153a Merge pull request #5119 from zyhfish/task/fix-4936
update the cookie consent control.
2025-02-28 10:48:21 -05:00
f50ba1a91e Merge branch 'dev' into task/fix-4936 2025-02-28 10:47:24 -05:00
b09575dbd6 Merge pull request #5126 from sbwalker/dev
provide option to assign a theme to a site
2025-02-28 10:45:40 -05:00
c52ee3d91d provide option to assign a theme to a site 2025-02-28 10:45:25 -05:00
Ben
1ced5c0425 update the cookie consent control. 2025-02-28 23:32:17 +08:00
e399a5c9b1 Merge pull request #5124 from sbwalker/dev
add missing maxlength attributes
2025-02-27 10:51:37 -05:00
08dff5fb67 add missing maxlength attributes 2025-02-27 10:51:22 -05:00
912760f2a7 Update SECURITY.md 2025-02-26 15:29:49 -05:00
4b62fdbf93 Update SECURITY.md 2025-02-26 15:27:33 -05:00
df593d43a7 Update SECURITY.md 2025-02-26 15:25:25 -05:00
89b1fba771 Merge pull request #5123 from sbwalker/dev
remove package validation logic
2025-02-26 12:54:06 -05:00
5505c91ae0 remove package validation logic 2025-02-26 12:53:53 -05:00
cc720ff399 Merge pull request #5122 from sbwalker/dev
remove unnecessary log message
2025-02-26 12:12:14 -05:00
29f07f6c56 remove unnecessary log message 2025-02-26 12:11:56 -05:00
a69e197a1f Merge pull request #5121 from zyhfish/task/fix-5116
Fix #5116: parse the value as UTC time.
2025-02-26 07:45:55 -05:00
Ben
6dddd8eff8 only allow essential cookies when cookie consent not granted. 2025-02-26 19:41:58 +08:00
Ben
51aada8922 Fix #5116: parse the value as UTC time. 2025-02-26 18:25:24 +08:00
Ben
b47bf40e8f update the cookie consent control. 2025-02-25 11:36:47 +08:00
48151bf365 Merge pull request #5118 from sbwalker/dev
remove IJSRuntime reference as it was causing a compilation warning
2025-02-24 16:15:51 -05:00
659950996d remove IJSRuntime reference as it was causing a compilation warning 2025-02-24 16:15:35 -05:00
6e656a4d0a Merge pull request #5114 from zyhfish/task/fix-4936
Fix #4936: add the cookie consent theme control.
2025-02-24 16:08:22 -05:00
Ben
bf308dd13d enable child component of cookie consent control. 2025-02-24 22:32:19 +08:00
Ben
982f3b1943 Fix #4936: add the cookie consent theme control. 2025-02-22 09:49:33 +08:00
7a4ea8cf1b Merge pull request #5094 from zyhfish/task/set-fa-ir-culture
Fix #5054: resolve the issue in fa-IR language.
2025-02-19 07:36:16 -05:00
7c0482a87c Merge pull request #5111 from sbwalker/dev
remove warning message related to no jobs being registered
2025-02-18 11:50:57 -05:00
101ededd89 remove warning message related to no jobs being registered 2025-02-18 11:50:36 -05:00
a8cbc0040e Merge pull request #5108 from zyhfish/task/fix-5103
Fix #5103: return a copy of the assembly list.
2025-02-18 11:40:30 -05:00
ed91bb445b Merge pull request #5110 from sbwalker/dev
improve HostedServiceBase so that scheduled jobs can be registered during installation
2025-02-18 11:36:00 -05:00
f158a222f4 improve HostedServiceBase so that scheduled jobs can be registered during installation 2025-02-18 11:35:38 -05:00
46bcad1fca Merge pull request #5109 from sbwalker/dev
clean up scheduled jobs which have been uninstalled
2025-02-18 09:12:48 -05:00
5e147afb9f clean up scheduled jobs which have been uninstalled 2025-02-18 09:12:26 -05:00
Ben
b061d4593f Fix #5103: return a copy of the assembly list. 2025-02-18 22:02:25 +08:00
3fa520b4ef Merge pull request #5107 from sbwalker/dev
make purge job output more readable
2025-02-18 08:53:38 -05:00
2df05b4afd make purge job output more readable 2025-02-18 08:53:23 -05:00
e0569a6748 Merge pull request #5104 from sbwalker/dev
fix visitor purge logic
2025-02-17 13:22:11 -05:00
2e6ab398d9 fix visitor purge logic 2025-02-17 13:21:58 -05:00
Ben
94b03d2a6b update settings for all RTL languages. 2025-02-16 10:25:43 +08:00
f84fe30bb6 Merge pull request #5097 from sbwalker/dev
synchronize BlazorScriptReload changes
2025-02-14 09:17:58 -05:00
049ddef531 synchronize BlazorScriptReload changes 2025-02-14 09:17:44 -05:00
a1a214c742 Merge pull request #5096 from sbwalker/dev
add another constructor for Script class
2025-02-14 09:09:14 -05:00
c40a483ffa add another constructor for Script class 2025-02-14 09:09:00 -05:00
Ben
aff99acfae Fix #5054: resolve the issue in fa-IR language. 2025-02-12 20:18:10 +08:00
628129c08d Update README.md 2025-02-11 12:00:41 -05:00
102 changed files with 2512 additions and 813 deletions

View File

@ -0,0 +1,25 @@
name: build-docker-imge
on:
- push
jobs:
build:
name: Build the docker container
runs-on: ubuntu-latest
steps:
- name: "Git clone"
run: git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
- name: "Git checkout"
run: git checkout "${{ gitea.sha }}"
- uses: aevea/action-kaniko@master
name: Run Kaniko to build our api docker container.
with:
image: kocoded/oqtane.framework
tag: ${{ git.workflow_sha }}
tag_with_latest: github.ref == 'refs/heads/master'
registry: git.kocoder.xyz
username: ${{ secrets.CI_RUNNER_USER }}
password: ${{ secrets.CI_RUNNER_TOKEN }}
build_file: Dockerfile
target: deploy

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# Build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source
COPY --link . .
RUN dotnet restore /source/Oqtane.sln
RUN dotnet build "/source/Oqtane.sln" -c Release -o /source/build/
# Publish
FROM build AS publish
RUN dotnet publish "Oqtane.Server/Oqtane.Server.csproj" -c Release -o /source/publish/
# Deploy
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy
WORKDIR /app
COPY --from=publish /source/publish/ /app/
ENTRYPOINT ["dotnet", "Oqtane.Server.dll"]

View File

@ -52,6 +52,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -55,10 +55,6 @@ else
protected override async Task OnInitializedAsync()
{
await GetJobs();
if (_jobs.Count == 0)
{
AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning);
}
}
private async Task GetJobs()

View File

@ -29,12 +29,12 @@ else
{
<div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
</div>
<div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" @bind:event="oninput" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>

View File

@ -63,24 +63,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed. This value must be specified within the module's IModule interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
</div>
</div>
<div class="row mb-1 align-items-center">
@ -244,7 +227,6 @@
private string _moduledefinitionname = "";
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -445,27 +427,5 @@
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
private string Browse(Page page) => string.IsNullOrEmpty(page.Url) ? NavigateUrl(page.Path) : page.Url;
}

View File

@ -16,7 +16,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required />
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@ -101,13 +101,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" />
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="Optionally enter a url which this page should redirect to when a user navigates to it" ResourceKey="Redirect">Redirect: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" />
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -147,7 +147,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" />
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -186,13 +186,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
<div class="col-sm-9">
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3"></textarea>
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodycontent" HelpText="Optionally enter content to be included in the page body (ie. script tags)" ResourceKey="BodyContent">Body Content: </Label>
<div class="col-sm-9">
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3"></textarea>
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>

View File

@ -205,13 +205,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
<div class="col-sm-9">
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3"></textarea>
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodycontent" HelpText="Optionally enter content to be included in the page body (ie. script tags)" ResourceKey="BodyContent">Body Content: </Label>
<div class="col-sm-9">
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3"></textarea>
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>

View File

@ -56,6 +56,19 @@
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="subject" HelpText="Deine Studien- oder Ausbildungsrichtung" ResourceKey="Subject">Fachrichtung:</Label>
<div class="col-sm-9">
<input id="subject" class="form-control" @bind="@_subject" maxlength="100" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="graduationYear" HelpText="Dein voraussichtlicher Abschlussjahrgang" ResourceKey="GraduationYear">Abschlussjahrgang:</Label>
<div class="col-sm-9">
<input id="graduationYear" type="number" class="form-control" @bind="@_graduationYear" min="1950" max="2100" step="1" required />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
@ -89,6 +102,9 @@ else
private string _displayname = string.Empty;
private bool _userCreated = false;
private bool _allowsitelogin = true;
private string _subject = string.Empty;
private int _graduationYear;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
@ -124,7 +140,10 @@ else
Password = _password,
Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
PhotoFileId = null
PhotoFileId = null,
Subject = _subject,
GraduationYear = _graduationYear
};
user = await UserService.AddUserAsync(user);

View File

@ -14,6 +14,7 @@
@inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService
@if (_initialized)
{
@ -50,11 +51,12 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing" ResourceKey="SiteMap">Site Map: </Label>
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared." ResourceKey="SiteMap">Site Map: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="sitemap" class="form-control" @bind="@_sitemap" disabled />
<a href="@_sitemap" class="btn btn-secondary" target="_new">@Localizer["Browse"]</a>
<button type="button" class="btn btn-danger" @onclick="EvictSitemapOutputCache">@Localizer["SiteMap.EvictCache"]</button>
</div>
</div>
</div>
@ -72,20 +74,8 @@
</div>
</div>
<br />
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<Section Name="Theme" Heading="Theme" ResourceKey="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico,png,gif" @ref="_faviconfilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the sites default theme" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
@ -126,6 +116,32 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieconsent" HelpText="Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent." ResourceKey="CookieConsent">Cookie Consent: </Label>
<div class="col-sm-9">
<select id="cookieconsent" class="form-select" @bind="@_cookieconsent">
<option value="">@SharedLocalizer["Disabled"]</option>
<option value="optin">@Localizer["OptIn"]</option>
<option value="optout">@Localizer["OptOut"]</option>
</select>
</div>
</div>
</div>
</Section>
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico,png,gif" @ref="_faviconfilemanager" />
</div>
</div>
</div>
</Section>
<Section Name="Functionality" Heading="Functionality" ResourceKey="Functionality">
@ -415,6 +431,7 @@
private string _themetype = "";
private string _containertype = "";
private string _admincontainertype = "";
private string _cookieconsent = "";
private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = "";
@ -505,6 +522,7 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);
// functionality
var textEditors = ServiceProvider.GetServices<ITextEditor>();
@ -717,6 +735,9 @@
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
//cookie consent
settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent);
// functionality
settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
@ -913,4 +934,9 @@
_aliasname = "";
StateHasChanged();
}
private async Task EvictSitemapOutputCache() {
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
}
}

View File

@ -153,8 +153,10 @@
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
<br /><br />
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<a class="btn btn-secondary" href="api/endpoint" target="_new">@Localizer["Endpoints"]</a>
</TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log">
<div class="container">

View File

@ -45,24 +45,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
</div>
</div>
<div class="row mb-1 align-items-center">
@ -116,7 +99,6 @@
private string _name;
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -185,27 +167,4 @@
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
}

View File

@ -4,6 +4,7 @@
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IPackageService PackageService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -19,6 +20,7 @@ else
<Pager Items="@_themes">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
@ -36,6 +38,7 @@ else
<ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
}
</td>
<td><NavLink class="btn btn-secondary" href="@NavigateUrl("admin/site")">@Localizer["Assign"]</NavLink></td>
<td>@context.Name</td>
<td>@context.Version</td>
<td>

View File

@ -54,11 +54,13 @@ else
<Label Class="col-sm-3" For="allowregistration" HelpText="Do you want anonymous visitors to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<div class="col-sm-9">
<select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
@ -80,8 +82,6 @@ else
</div>
</div>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9">
@ -113,6 +113,15 @@ else
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logouteverywhere" HelpText="Do you want users to be logged out of every active session on any device, or only their current session?" ResourceKey="LogoutEverywhere">Logout Everywhere?</Label>
<div class="col-sm-9">
<select id="logouteverywhere" class="form-select" @bind="@_logouteverywhere">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
</Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -469,6 +478,7 @@ else
private string _cookiename;
private string _cookieexpiration;
private string _alwaysremember;
private string _logouteverywhere;
private string _minimumlength;
private string _uniquecharacters;
@ -529,7 +539,7 @@ else
await LoadUsersAsync(true);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -538,6 +548,7 @@ else
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", "");
_alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false");
_logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false");
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
@ -656,6 +667,7 @@ else
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false);
settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);

View File

@ -2,6 +2,7 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject IHtmlTextService HtmlTextService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@if (PageState.EditMode)
@ -36,6 +37,10 @@
{
content = htmltext.Content;
content = Utilities.FormatContent(content, PageState.Alias, "render");
if (bool.Parse(SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false")))
{
content = ReplaceTokens(content);
}
}
else
{

View File

@ -15,7 +15,7 @@ namespace Oqtane.Modules.HtmlText
Version = "1.0.1",
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
ReleaseVersions = "1.0.0,1.0.1",
SettingsType = string.Empty,
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client",
Resources = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }

View File

@ -0,0 +1,55 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="dynamictokens" ResourceKey="DynamicTokens" ResourceType="@resourceType" HelpText="Do you wish to allow tokens to be dynamically replaced? Please note that this will affect the performance of your site.">Dynamic Tokens? </Label>
<div class="col-sm-9">
<select id="dynamictokens" class="form-select" @bind="@_dynamictokens">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private ElementReference form;
private bool validated = false;
private string _dynamictokens;
protected override void OnInitialized()
{
try
{
_dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using System.Dynamic;
using System.Reflection;
namespace Oqtane.Modules
{
@ -35,7 +36,7 @@ namespace Oqtane.Modules
protected PageState PageState { get; set; }
[CascadingParameter]
protected Module ModuleState { get; set; }
protected Models.Module ModuleState { get; set; }
[Parameter]
public RenderModeBoundary RenderModeBoundary { get; set; }
@ -413,6 +414,79 @@ namespace Oqtane.Modules
await interop.ScrollTo(0, 0, "smooth");
}
public string ReplaceTokens(string content)
{
return ReplaceTokens(content, null);
}
public string ReplaceTokens(string content, object obj)
{
var tokens = new List<string>();
var pos = content.IndexOf("[");
if (pos != -1)
{
if (content.IndexOf("]", pos) != -1)
{
var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1);
if (token.Contains(":"))
{
tokens.Add(token.Substring(1, token.Length - 2));
}
}
pos = content.IndexOf("[", pos + 1);
}
if (tokens.Count != 0)
{
foreach (string token in tokens)
{
var segments = token.Split(":");
if (segments.Length >= 2 && segments.Length <= 3)
{
var objectName = string.Join(":", segments, 0, segments.Length - 1);
var propertyName = segments[segments.Length - 1];
var propertyValue = "";
switch (objectName)
{
case "ModuleState":
propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString();
break;
case "PageState":
propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString();
break;
case "PageState:Alias":
propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString();
break;
case "PageState:Site":
propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString();
break;
case "PageState:Page":
propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString();
break;
case "PageState:User":
propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString();
break;
case "PageState:Route":
propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString();
break;
default:
if (obj != null && obj.GetType().Name == objectName)
{
propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString();
}
break;
}
if (propertyValue != null)
{
content = content.Replace("[" + token + "]", propertyValue);
}
}
}
}
return content;
}
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{

View File

@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -22,10 +22,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -180,9 +180,6 @@
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
<data name="Message.NoJobs" xml:space="preserve">
<value>Please Note That After An Initial Installation You Must &lt;a href={0}&gt;Restart&lt;/a&gt; The Application In Order To Activate The Default Scheduled Jobs.</value>
</data>
<data name="Refresh.Text" xml:space="preserve">
<value>Refresh</value>
</data>

View File

@ -228,18 +228,6 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
<data name="Browse" xml:space="preserve">
<value>Browse</value>
</data>

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NoCriteria" xml:space="preserve">
<value>You Must Provide Some Search Criteria</value>
<value>Please Enter Some Search Criteria</value>
</data>
<data name="NoResult" xml:space="preserve">
<value>No Content Matches The Criteria Provided</value>

View File

@ -349,7 +349,7 @@
<value>Relay Configured?</value>
</data>
<data name="SiteMap.HelpText" xml:space="preserve">
<value>The site map url for this site which can be submitted to search engines for indexing</value>
<value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value>
</data>
<data name="SiteMap.Text" xml:space="preserve">
<value>Site Map:</value>
@ -426,4 +426,25 @@
<data name="System" xml:space="preserve">
<value>System</value>
</data>
<data name="CookieConsent.HelpText" xml:space="preserve">
<value>Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent.</value>
</data>
<data name="CookieConsent.Text" xml:space="preserve">
<value>Cookie Consent:</value>
</data>
<data name="OptIn" xml:space="preserve">
<value>Opt-In (GDPR)</value>
</data>
<data name="OptOut" xml:space="preserve">
<value>Opt-Out (CCPA)</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="SiteMap.EvictCache" xml:space="preserve">
<value>Clear Cache</value>
</data>
<data name="Success.SiteMap.CacheEvicted" xml:space="preserve">
<value>Site Map Cache Cleared</value>
</data>
</root>

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Swagger" xml:space="preserve">
<value>Access Swagger UI</value>
<value>Swagger UI</value>
</data>
<data name="FrameworkVersion.HelpText" xml:space="preserve">
<value>Framework Version</value>
@ -306,4 +306,7 @@
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled.</value>
</data>
<data name="Endpoints" xml:space="preserve">
<value>API Endpoints</value>
</data>
</root>

View File

@ -180,16 +180,4 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
</root>

View File

@ -156,4 +156,7 @@
<data name="Enabled" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Assign" xml:space="preserve">
<value>Assign</value>
</data>
</root>

View File

@ -507,4 +507,10 @@
<data name="Error.DeleteUser" xml:space="preserve">
<value>Error Deleting User</value>
</data>
<data name="LogoutEverywhere.Text" xml:space="preserve">
<value>Logout Everywhere?</value>
</data>
<data name="LogoutEverywhere.HelpText" xml:space="preserve">
<value>Do you want users to be logged out of every active session on any device, or only their current session?</value>
</data>
</root>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DynamicTokens.HelpText" xml:space="preserve">
<value>Do you wish to allow tokens to be dynamically replaced? Please note that this will affect the performance of your site.</value>
</data>
<data name="DynamicTokens.Text" xml:space="preserve">
<value>Dynamic Tokens?</value>
</data>
</root>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Confirm" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="ConsentNotice" xml:space="preserve">
<value>I agree to use cookies to provide the best possible user experience for this site. I understand that I can change these preferences at any time.</value>
</data>
<data name="Privacy" xml:space="preserve">
<value>Privacy</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema

View File

@ -0,0 +1,47 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Globalization;
namespace Oqtane.Services
{
/// <inheritdoc cref="ICookieConsentService" />
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class CookieConsentService : ServiceBase, ICookieConsentService
{
public CookieConsentService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("CookieConsent");
public async Task<bool> IsActionedAsync()
{
return await GetJsonAsync<bool>($"{ApiUrl}/IsActioned");
}
public async Task<bool> CanTrackAsync(bool optOut)
{
return await GetJsonAsync<bool>($"{ApiUrl}/CanTrack?optout=" + optOut);
}
public async Task<string> CreateActionedCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/CreateActionedCookie");
return cookie ?? string.Empty;
}
public async Task<string> CreateConsentCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/CreateConsentCookie");
return cookie ?? string.Empty;
}
public async Task<string> WithdrawConsentCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/WithdrawConsentCookie");
return cookie ?? string.Empty;
}
}
}

View File

@ -0,0 +1,42 @@
using Oqtane.Models;
using System;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to retrieve cookie consent information.
/// </summary>
public interface ICookieConsentService
{
/// <summary>
/// Get cookie consent bar actioned status
/// </summary>
/// <returns></returns>
Task<bool> IsActionedAsync();
/// <summary>
/// Get cookie consent status
/// </summary>
/// <returns></returns>
Task<bool> CanTrackAsync(bool optOut);
/// <summary>
/// create actioned cookie
/// </summary>
/// <returns></returns>
Task<string> CreateActionedCookieAsync();
/// <summary>
/// create consent cookie
/// </summary>
/// <returns></returns>
Task<string> CreateConsentCookieAsync();
/// <summary>
/// widhdraw consent cookie
/// </summary>
/// <returns></returns>
Task<string> WithdrawConsentCookieAsync();
}
}

View File

@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage cache
/// </summary>
public interface IOutputCacheService
{
/// <summary>
/// Evicts the output cache for a specific tag
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
Task EvictByTag(string tag);
}
}

View File

@ -0,0 +1,23 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
/// <inheritdoc cref="IOutputCacheService" />
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class OutputCacheService : ServiceBase, IOutputCacheService
{
public OutputCacheService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("OutputCache");
public async Task EvictByTag(string tag)
{
await DeleteAsync($"{ApiUrl}/{tag}");
}
}
}

View File

@ -1,11 +1,6 @@
@namespace Oqtane.Themes.BlazorTheme
@inherits ThemeBase
<div class="breadcrumbs">
<Breadcrumbs />
</div>
<div class="row flex-xl-nowrap gx-0">
<div class="sidebar">
<nav class="navbar">
@ -22,13 +17,18 @@
<Login />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
<div class="breadcrumbs">
<Breadcrumbs />
</div>
</div>
<div class="container">
<div class="row px-4">
<Pane Name="@PaneNames.Admin" />
<CookieConsent />
</div>
</div>
</div>
</div>
@code {

View File

@ -573,7 +573,7 @@
else
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url, everywhere = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false")) };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}

View File

@ -0,0 +1,168 @@
@namespace Oqtane.Themes.Controls
@inherits ThemeControlBase
@inject ISettingService SettingService
@inject ICookieConsentService CookieConsentService
@inject IStringLocalizer<CookieConsent> Localizer
@if (_enabled && !Hidden)
{
<div class="gdpr-consent-bar bg-light text-dark @(_showBanner ? "p-3" : "p-0") pe-5 fixed-bottom">
<form method="post" @formname="CookieConsentForm" @onsubmit="async () => await AcceptPolicy()" data-enhance>
@if (_showBanner)
{
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="container-fluid">
<div class="row">
<div class="col-9 col-xl-10">
@if (PageState.RenderMode == RenderModes.Static)
{
<input type="checkbox" name="cantrack" checked="@_canTrack" value="1" class="form-check-input me-2" />
}
else
{
<input type="checkbox" name="cantrack" @bind="@_canTrack" value="1" class="form-check-input me-2" />
}
@((MarkupString)Convert.ToString(Localizer["ConsentNotice"]))
</div>
<div class="col-3 col-xl-2">
<div class="row">
<div class="col-md-6 col-xs-6 text-center">
<button class="btn btn-primary mb-1 px-0 w-100" type="submit">@((MarkupString)Convert.ToString(Localizer["Confirm"]))</button>
</div>
@if (ShowPrivacyLink)
{
<div class="col-md-6 col-xs-6 text-center">
<a class="btn btn-secondary mb-1 px-0 w-100" href="/privacy" target="_blank">@((MarkupString)Convert.ToString(Localizer["Privacy"]))</a>
</div>
}
</div>
</div>
</div>
</div>
}
</form>
<form method="post" @formname="CookieConsentToggleForm" @onsubmit="async () => await ToggleBanner()" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
@if (_showBanner)
{
<input type="hidden" name="showbanner" value="false" />
<button type="submit" class="btn btn-light text-dark btn-sm position-absolute btn-hide">
<i class="oi oi-chevron-bottom"></i>
</button>
}
else
{
<input type="hidden" name="showbanner" value="true" />
<button type="submit" class="btn btn-light text-dark btn-sm position-absolute btn-show">
<i class="oi oi-chevron-top"></i>
</button>
}
</form>
</div>
}
@code {
private bool _showBanner;
private bool _enabled;
private bool _optout;
private bool _actioned;
private bool _canTrack;
private bool _consentPostback;
private bool _togglePostback;
[Parameter]
public bool Hidden { get; set; }
[Parameter]
public bool ShowPrivacyLink { get; set; } = true;
[SupplyParameterFromForm(FormName = "CookieConsentToggleForm")]
public string ShowBanner
{
get => "";
set
{
_showBanner = bool.Parse(value);
_togglePostback = true;
}
}
[SupplyParameterFromForm(FormName = "CookieConsentForm")]
public string CanTrack
{
get => "";
set
{
_canTrack = !string.IsNullOrEmpty(value);
_consentPostback = true;
}
}
protected override async Task OnInitializedAsync()
{
var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty);
_enabled = !string.IsNullOrEmpty(cookieConsentSetting);
_optout = cookieConsentSetting == "optout";
_actioned = await CookieConsentService.IsActionedAsync();
if (!_consentPostback)
{
_canTrack = await CookieConsentService.CanTrackAsync(_optout);
}
if (!_togglePostback)
{
_showBanner = !_actioned;
}
}
private async Task AcceptPolicy()
{
var cookieString = string.Empty;
if (_optout)
{
cookieString = _canTrack ? await CookieConsentService.WithdrawConsentCookieAsync() : await CookieConsentService.CreateConsentCookieAsync();
}
else
{
cookieString = _canTrack ? await CookieConsentService.CreateConsentCookieAsync() : await CookieConsentService.WithdrawConsentCookieAsync();
}
//update the page state
PageState.AllowCookies = _canTrack;
if (!string.IsNullOrEmpty(cookieString))
{
var interop = new Interop(JSRuntime);
await interop.SetCookieString(cookieString);
_actioned = true;
_showBanner = false;
StateHasChanged();
}
}
private async Task ToggleBanner()
{
if (!_actioned)
{
var cookieString = await CookieConsentService.CreateActionedCookieAsync();
if (!string.IsNullOrEmpty(cookieString))
{
var interop = new Interop(JSRuntime);
await interop.SetCookieString(cookieString);
_actioned = true;
}
}
if (PageState.RenderMode == RenderModes.Interactive)
{
_showBanner = !_showBanner;
StateHasChanged();
}
}
}

View File

@ -15,6 +15,7 @@
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<input type="hidden" name="everywhere" value="@everywhere" />
<button type="submit" class="@CssClass">@Localizer["Logout"]</button>
</form>
}

View File

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Providers;
using Oqtane.Security;
using Oqtane.Services;
@ -26,6 +25,7 @@ namespace Oqtane.Themes.Controls
protected string loginurl;
protected string logouturl;
protected string returnurl;
protected string everywhere;
protected override void OnParametersSet()
{
@ -57,6 +57,7 @@ namespace Oqtane.Themes.Controls
// set logout url
logouturl = Utilities.TenantUrl(PageState.Alias, "/pages/logout/");
everywhere = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false");
// verify anonymous users can access current page
if (UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList) && Utilities.IsEffectiveAndNotExpired(PageState.Page.EffectiveDate, PageState.Page.ExpiryDate))
@ -98,7 +99,7 @@ namespace Oqtane.Themes.Controls
else // this condition is only valid for legacy Login button inheriting from LoginBase
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = returnurl };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = returnurl, everywhere = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false")) };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(logouturl, fields);
}

View File

@ -107,6 +107,7 @@
{
<Pane Name="Footer" />
}
<CookieConsent />
</div>
</main>

View File

@ -37,6 +37,19 @@ namespace Oqtane.UI
}
}
public Task SetCookieString(string cookieString)
{
try
{
_jsRuntime.InvokeVoidAsync("Oqtane.Interop.setCookieString", cookieString);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
public ValueTask<string> GetCookie(string name)
{
try

View File

@ -27,6 +27,7 @@ namespace Oqtane.UI
public bool IsInternalNavigation { get; set; }
public Guid RenderId { get; set; }
public bool Refresh { get; set; }
public bool AllowCookies { get; set; }
public List<Page> Pages
{

View File

@ -14,6 +14,7 @@
@inject IUrlMappingService UrlMappingService
@inject ILogService LogService
@inject ISettingService SettingService
@inject ICookieConsentService CookieConsentService
@inject IJSRuntime JSRuntime
@implements IHandleAfterRender
@implements IDisposable
@ -293,6 +294,14 @@
// 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
{
@ -316,7 +325,8 @@
ReturnUrl = returnurl,
IsInternalNavigation = _isInternalNavigation,
RenderId = Guid.NewGuid(),
Refresh = false
Refresh = false,
AllowCookies = _allowCookies.GetValueOrDefault(true)
};
OnStateChange?.Invoke(_pagestate);

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,8 +34,8 @@
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -30,7 +30,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>6.1.0</ApplicationDisplayVersion>
<ApplicationDisplayVersion>6.1.1</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@ -67,14 +67,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.30" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.30" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.30" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.3" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.40" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.40" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.40" />
</ItemGroup>
<ItemGroup>

View File

@ -14,6 +14,9 @@ Oqtane.Interop = {
}
document.cookie = cookieString;
},
setCookieString: function (cookieString) {
document.cookie = cookieString;
},
getCookie: function (name) {
name = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>6.1.0</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Framework</id>
<version>6.1.0</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v6.1.0/Oqtane.Framework.6.1.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v6.1.1/Oqtane.Framework.6.1.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane framework</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Server</id>
<version>6.1.0</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>6.1.0</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>6.1.0</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.0.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.1.Install.zip" -Force

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.0.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.1.Upgrade.zip" -Force

View File

@ -31,6 +31,8 @@
@inject IUrlMappingRepository UrlMappingRepository
@inject IVisitorRepository VisitorRepository
@inject IJwtManager JwtManager
@inject ICookieConsentService CookieConsentService
@inject ISettingService SettingService
@if (_initialized)
{
@ -107,6 +109,7 @@
private string _styleSheets = "";
private string _scripts = "";
private string _message = "";
private bool _allowCookies;
private PageState _pageState;
// CascadingParameter is required to access HttpContext
@ -140,6 +143,9 @@
_prerender = site.Prerender;
_fingerprint = site.Fingerprint;
var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
_allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout");
var modules = new List<Module>();
Route route = new Route(url, alias.Path);
@ -170,7 +176,7 @@
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
if (site.VisitorTracking)
if (site.VisitorTracking && _allowCookies)
{
TrackVisitor(site.SiteId);
}
@ -245,7 +251,8 @@
ReturnUrl = "",
IsInternalNavigation = false,
RenderId = Guid.NewGuid(),
Refresh = true
Refresh = true,
AllowCookies = _allowCookies
};
}
else

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Shared;
using System;
using System.Globalization;
using Oqtane.Infrastructure;
using Oqtane.Services;
using System.Threading.Tasks;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class CookieConsentController : Controller
{
private readonly ICookieConsentService _cookieConsentService;
public CookieConsentController(ICookieConsentService cookieConsentService)
{
_cookieConsentService = cookieConsentService;
}
[HttpGet("IsActioned")]
public async Task<bool> IsActioned()
{
return await _cookieConsentService.IsActionedAsync();
}
[HttpGet("CanTrack")]
public async Task<bool> CanTrack(string optout)
{
return await _cookieConsentService.CanTrackAsync(bool.Parse(optout));
}
[HttpGet("CreateActionedCookie")]
public async Task<string> CreateActionedCookie()
{
return await _cookieConsentService.CreateActionedCookieAsync();
}
[HttpGet("CreateConsentCookie")]
public async Task<string> CreateConsentCookie()
{
return await _cookieConsentService.CreateConsentCookieAsync();
}
[HttpGet("WithdrawConsentCookie")]
public async Task<string> WithdrawConsentCookie()
{
return await _cookieConsentService.WithdrawConsentCookieAsync();
}
}
}

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using Oqtane.Shared;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class EndpointController : Controller
{
private readonly IEnumerable<EndpointDataSource> _endpointSources;
public EndpointController(IEnumerable<EndpointDataSource> endpointSources)
{
_endpointSources = endpointSources;
}
// GET api/<controller>
[HttpGet]
[Authorize(Roles = RoleNames.Host)]
public ActionResult Get()
{
var endpoints = _endpointSources
.SelectMany(item => item.Endpoints)
.OfType<RouteEndpoint>();
var output = endpoints.Select(
item =>
{
var controller = item.Metadata
.OfType<ControllerActionDescriptor>()
.FirstOrDefault();
var action = controller != null
? $"{controller.ControllerName}.{controller.ActionName}"
: null;
var controllerMethod = controller != null
? $"{controller.ControllerTypeInfo.FullName}:{controller.MethodInfo.Name}"
: null;
return new
{
Method = item.Metadata.OfType<HttpMethodMetadata>().FirstOrDefault()?.HttpMethods?[0],
Route = $"/{item.RoutePattern.RawText.TrimStart('/')}",
Action = action,
ControllerMethod = controllerMethod
};
}
).OrderBy(item => item.Route);
return Json(output);
}
}
}

View File

@ -171,7 +171,8 @@ namespace Oqtane.Controllers
}
}
return assemblyList;
});
}).ToList();
}
// GET api/<controller>/load?list=x,y

View File

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class OutputCacheController : Controller
{
private readonly IOutputCacheService _cacheService;
public OutputCacheController(IOutputCacheService cacheService)
{
_cacheService = cacheService;
}
// DELETE api/<controller>/{tag}
[HttpDelete("{tag}")]
[Authorize(Roles = RoleNames.Admin)]
public async Task EvictByTag(string tag)
{
await _cacheService.EvictByTag(tag);
}
}
}

View File

@ -34,7 +34,7 @@ namespace Oqtane.Controllers
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
return _visitors.GetVisitors(SiteId, DateTime.ParseExact(fromdate, "yyyy-MM-dd", CultureInfo.InvariantCulture));
return _visitors.GetVisitors(SiteId, DateTime.ParseExact(fromdate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal));
}
else
{

View File

@ -34,6 +34,14 @@ namespace Oqtane.Extensions
options.SetDefaultCulture(defaultCulture)
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
foreach(var culture in options.SupportedCultures)
{
if (culture.TextInfo.IsRightToLeft)
{
RightToLeftCulture.ResolveFormat(culture);
}
}
});
return app;
@ -47,6 +55,5 @@ namespace Oqtane.Extensions
public static IApplicationBuilder UseExceptionMiddleWare(this IApplicationBuilder builder)
=> builder.UseMiddleware<ExceptionMiddleware>();
}
}

View File

@ -103,6 +103,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISearchService, SearchService>();
services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
services.AddScoped<IImageService, ImageService>();
services.AddScoped<ICookieConsentService, ServerCookieConsentService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
@ -116,6 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection
// services
services.AddTransient<ISiteService, ServerSiteService>();
services.AddTransient<ILocalizationCookieService, ServerLocalizationCookieService>();
services.AddTransient<IOutputCacheService, ServerOutputCacheService>();
// repositories
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@ -45,28 +46,61 @@ namespace Oqtane.Infrastructure
protected async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield(); // required so that this method does not block startup
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
IConfigurationRoot _config = scope.ServiceProvider.GetRequiredService<IConfigurationRoot>();
ILogger<HostedServiceBase> _filelogger = scope.ServiceProvider.GetRequiredService<ILogger<HostedServiceBase>>();
// if framework is installed
if (IsInstalled(_config))
{
try
{
var jobs = scope.ServiceProvider.GetRequiredService<IJobRepository>();
// get name of job
string jobTypeName = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName);
// load jobs and find current job
Job job = jobs.GetJobs().Where(item => item.JobType == jobTypeName).FirstOrDefault();
if (job == null)
{
// auto registration
job = new Job { JobType = jobTypeName };
// optional HostedServiceBase properties
var jobType = Type.GetType(jobTypeName);
var jobObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, jobType) as HostedServiceBase;
if (jobObject.Name != "")
{
job.Name = jobObject.Name;
}
else
{
job.Name = Utilities.GetTypeName(job.JobType);
}
job.Frequency = jobObject.Frequency;
job.Interval = jobObject.Interval;
job.StartDate = jobObject.StartDate;
job.EndDate = jobObject.EndDate;
job.RetentionHistory = jobObject.RetentionHistory;
job.IsEnabled = jobObject.IsEnabled;
job.IsStarted = true;
job.IsExecuting = false;
job.NextExecution = null;
job = jobs.AddJob(job);
}
if (job != null && job.IsEnabled && !job.IsExecuting)
{
var jobLogs = scope.ServiceProvider.GetRequiredService<IJobLogRepository>();
var tenantRepository = scope.ServiceProvider.GetRequiredService<ITenantRepository>();
var tenantManager = scope.ServiceProvider.GetRequiredService<ITenantManager>();
// get name of job
string jobType = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName);
// load jobs and find current job
Job job = jobs.GetJobs().Where(item => item.JobType == jobType).FirstOrDefault();
if (job != null && job.IsEnabled && !job.IsExecuting)
{
// get next execution date
DateTime NextExecution;
if (job.NextExecution == null)
@ -146,9 +180,6 @@ namespace Oqtane.Infrastructure
}
}
catch (Exception ex)
{
// can occur during the initial installation because the database has not yet been created
if (!ex.Message.Contains("No database provider has been configured for this DbContext"))
{
_filelogger.LogError(Utilities.LogMessage(this, $"An Error Occurred Executing Scheduled Job: {Name} - {ex}"));
}
@ -208,9 +239,12 @@ namespace Oqtane.Infrastructure
{
using (var scope = _serviceScopeFactory.CreateScope())
{
IConfigurationRoot _config = scope.ServiceProvider.GetRequiredService<IConfigurationRoot>();
ILogger<HostedServiceBase> _filelogger = scope.ServiceProvider.GetRequiredService<ILogger<HostedServiceBase>>();
try
{
if (IsInstalled(_config))
{
string jobTypeName = Utilities.GetFullTypeName(GetType().AssemblyQualifiedName);
IJobRepository jobs = scope.ServiceProvider.GetRequiredService<IJobRepository>();
@ -222,43 +256,13 @@ namespace Oqtane.Infrastructure
job.IsExecuting = false;
jobs.UpdateJob(job);
}
else
{
// auto registration - job will not run on initial installation due to no DBContext but will run after restart
job = new Job { JobType = jobTypeName };
// optional HostedServiceBase properties
var jobType = Type.GetType(jobTypeName);
var jobObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, jobType) as HostedServiceBase;
if (jobObject.Name != "")
{
job.Name = jobObject.Name;
}
else
{
job.Name = Utilities.GetTypeName(job.JobType);
}
job.Frequency = jobObject.Frequency;
job.Interval = jobObject.Interval;
job.StartDate = jobObject.StartDate;
job.EndDate = jobObject.EndDate;
job.RetentionHistory = jobObject.RetentionHistory;
job.IsEnabled = jobObject.IsEnabled;
job.IsStarted = true;
job.IsExecuting = false;
job.NextExecution = null;
jobs.AddJob(job);
}
}
catch (Exception ex)
{
// can occur during the initial installation because the database has not yet been created
if (!ex.Message.Contains("No database provider has been configured for this DbContext"))
{
_filelogger.LogError(Utilities.LogMessage(this, $"An Error Occurred Starting Scheduled Job: {Name} - {ex}"));
}
}
}
_executingTask = ExecuteAsync(_cancellationTokenSource.Token);
@ -314,6 +318,11 @@ namespace Oqtane.Infrastructure
}
}
private bool IsInstalled(IConfigurationRoot config)
{
return !string.IsNullOrEmpty(config.GetConnectionString(SettingKeys.ConnectionStringKey));
}
public void Dispose()
{
_cancellationTokenSource.Cancel();

View File

@ -39,7 +39,7 @@ namespace Oqtane.Infrastructure
List<Site> sites = siteRepository.GetSites().ToList();
foreach (Site site in sites)
{
log += "Processing Site: " + site.Name + "<br />";
log += "<br />Processing Site: " + site.Name + "<br />";
int retention;
int count;
@ -118,11 +118,11 @@ namespace Oqtane.Infrastructure
try
{
var assemblies = installationManager.RegisterAssemblies();
log += assemblies.ToString() + " Assemblies Registered<br />";
log += "<br />" + assemblies.ToString() + " Assemblies Registered<br />";
}
catch (Exception ex)
{
log += $"Error Registering Assemblies - {ex.Message}<br />";
log += $"<br />Error Registering Assemblies - {ex.Message}<br />";
}
return log;

View File

@ -0,0 +1,42 @@
using System;
using System.Globalization;
using System.Reflection;
namespace Oqtane.Infrastructure
{
public class RightToLeftCulture
{
public static CultureInfo ResolveFormat(CultureInfo cultureInfo)
{
SetNumberFormatInfo(cultureInfo.NumberFormat);
SetCalenar(cultureInfo);
return cultureInfo;
}
private static void SetCalenar(CultureInfo cultureInfo)
{
var calendar = new RightToLeftCultureCalendar();
var fieldInfo = cultureInfo.GetType().GetField("_calendar", BindingFlags.NonPublic | BindingFlags.Instance);
if (fieldInfo != null)
{
fieldInfo.SetValue(cultureInfo, calendar);
}
var info = cultureInfo.DateTimeFormat.GetType().GetField("calendar", BindingFlags.NonPublic | BindingFlags.Instance);
if (info != null)
{
info.SetValue(cultureInfo.DateTimeFormat, calendar);
}
}
public static void SetNumberFormatInfo(NumberFormatInfo persianNumberFormatInfo)
{
persianNumberFormatInfo.NumberDecimalSeparator = ".";
persianNumberFormatInfo.DigitSubstitution = DigitShapes.NativeNational;
persianNumberFormatInfo.NumberNegativePattern = 0;
persianNumberFormatInfo.NegativeSign = "-";
}
}
}

View File

@ -0,0 +1,77 @@
using System;
namespace Oqtane.Infrastructure
{
public class RightToLeftCultureCalendar : System.Globalization.PersianCalendar
{
public override int GetYear(DateTime time)
{
try
{
return base.GetYear(time);
}
catch
{
// ignore
}
return time.Year;
}
public override int GetMonth(DateTime time)
{
try
{
return base.GetMonth(time);
}
catch
{
// ignore
}
return time.Month;
}
public override int GetDayOfMonth(DateTime time)
{
try
{
return base.GetDayOfMonth(time);
}
catch
{
// ignore
}
return time.Day;
}
public override int GetDayOfYear(DateTime time)
{
try
{
return base.GetDayOfYear(time);
}
catch
{
// ignore
}
return time.DayOfYear;
}
public override DayOfWeek GetDayOfWeek(DateTime time)
{
try
{
return base.GetDayOfWeek(time);
}
catch
{
// ignore
}
return time.DayOfWeek;
}
}
}

View File

@ -1,14 +1,22 @@
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
using Oqtane.Documentation;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.SiteTemplates
namespace Oqtane.Infrastructure.SiteTemplates
{
[PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")]
public class AdminSiteTemplate : ISiteTemplate
{
private readonly IStringLocalizer<AdminSiteTemplate> _localizer;
public AdminSiteTemplate(IStringLocalizer<AdminSiteTemplate> localizer)
{
_localizer = localizer;
}
public string Name
{
get { return "Admin Site Template"; }
@ -169,6 +177,66 @@ namespace Oqtane.SiteTemplates
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Privacy",
Parent = "",
Path = "privacy",
Icon = Icons.Eye,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Privacy Policy", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Settings = new List<Setting> {
new Setting { SettingName = "DynamicTokens", SettingValue = "true" }
},
Content = _localizer["Privacy"]
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Terms",
Parent = "",
Path = "terms",
Icon = Icons.List,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Terms of Use", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Settings = new List<Setting> {
new Setting { SettingName = "DynamicTokens", SettingValue = "true" }
},
Content = _localizer["Terms"]
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Not Found",

View File

@ -7,7 +7,7 @@ using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.SiteTemplates
namespace Oqtane.Infrastructure.SiteTemplates
{
[PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")]
public class DefaultSiteTemplate : ISiteTemplate

View File

@ -4,7 +4,7 @@ using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.SiteTemplates
namespace Oqtane.Infrastructure.SiteTemplates
{
[PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")]
public class EmptySiteTemplate : ISiteTemplate

View File

@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Oqtane.Infrastructure.SiteTemplates;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
@ -75,6 +77,9 @@ namespace Oqtane.Infrastructure
case "6.1.0":
Upgrade_6_1_0(tenant, scope);
break;
case "6.1.1":
Upgrade_6_1_1(tenant, scope);
break;
}
}
}
@ -457,6 +462,75 @@ namespace Oqtane.Infrastructure
RemoveAssemblies(tenant, assemblies, "6.1.0");
}
private void Upgrade_6_1_1(Tenant tenant, IServiceScope scope)
{
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<AdminSiteTemplate>>();
var pageTemplates = new List<PageTemplate>
{
new PageTemplate
{
Name = "Privacy",
Parent = "",
Path = "privacy",
Icon = Icons.Eye,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Privacy Policy", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Settings = new List<Setting> {
new Setting { SettingName = "DynamicTokens", SettingValue = "true" }
},
Content = localizer["Privacy"]
}
}
},
new PageTemplate
{
Name = "Terms",
Parent = "",
Path = "terms",
Icon = Icons.List,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Terms of Use", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Settings = new List<Setting> {
new Setting { SettingName = "DynamicTokens", SettingValue = "true" }
},
Content = localizer["Terms"]
}
}
}
};
AddPagesToSites(scope, tenant, pageTemplates);
}
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates)
{
var tenants = scope.ServiceProvider.GetRequiredService<ITenantManager>();

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -34,21 +34,21 @@
<EmbeddedResource Include="Scripts\MigrateTenant.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.1" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.1" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11-pre20241216174303" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.3" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.3" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />

View File

@ -50,7 +50,6 @@ namespace Oqtane.Pages
{
if (string.IsNullOrWhiteSpace(path))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return BrokenFile();
}

View File

@ -7,6 +7,7 @@ using System.Xml;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Enums;
using Oqtane.Infrastructure;
@ -19,6 +20,7 @@ using Oqtane.Shared;
namespace Oqtane.Pages
{
[AllowAnonymous]
[OutputCache(Duration = 300, Tags = [Constants.SitemapOutputCacheTag])]
public class SitemapModel : PageModel
{
private readonly IServiceProvider _serviceProvider;

View File

@ -22,6 +22,14 @@ namespace Oqtane.Repository
{
return _cache.GetOrCreate("jobs", entry =>
{
// remove any jobs which have been uninstalled
foreach (var job in _db.Job.ToList())
{
if (Type.GetType(job.JobType) == null)
{
DeleteJob(job.JobId);
}
}
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return _db.Job.ToList();
});

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
@ -27,7 +28,7 @@ namespace Oqtane.Repository
{
if (IsMaster(entityName))
{
return _master.Setting.Where(item => item.EntityName == entityName);
return _master.Setting.Where(item => item.EntityName == entityName).ToList();
}
else
{
@ -38,13 +39,28 @@ namespace Oqtane.Repository
public IEnumerable<Setting> GetSettings(string entityName, int entityId)
{
var settings = GetSettings(entityName);
var settings = GetSettings(entityName).ToList();
if (entityName == EntityNames.Site)
{
// site settings can be overridden by host settings
var hostsettings = GetSettings(EntityNames.Host);
foreach (var hostsetting in hostsettings)
{
if (settings.Any(item => item.SettingName == hostsetting.SettingName))
{
settings.First(item => item.SettingName == hostsetting.SettingName).SettingValue = hostsetting.SettingValue;
}
else
{
settings.Add(new Setting { SettingId = -1, EntityName = entityName, EntityId = entityId, SettingName = hostsetting.SettingName, SettingValue = hostsetting.SettingValue, IsPrivate = hostsetting.IsPrivate });
}
}
}
return settings.Where(item => item.EntityId == entityId);
}
public Setting AddSetting(Setting setting)
{
using var tenant = _tenantContextFactory.CreateDbContext();
if (IsMaster(setting.EntityName))
{
_master.Setting.Add(setting);
@ -52,6 +68,7 @@ namespace Oqtane.Repository
}
else
{
using var tenant = _tenantContextFactory.CreateDbContext();
tenant.Setting.Add(setting);
tenant.SaveChanges();
}
@ -61,7 +78,6 @@ namespace Oqtane.Repository
public Setting UpdateSetting(Setting setting)
{
using var tenant = _tenantContextFactory.CreateDbContext();
if (IsMaster(setting.EntityName))
{
_master.Entry(setting).State = EntityState.Modified;
@ -69,6 +85,7 @@ namespace Oqtane.Repository
}
else
{
using var tenant = _tenantContextFactory.CreateDbContext();
tenant.Entry(setting).State = EntityState.Modified;
tenant.SaveChanges();
}
@ -78,33 +95,32 @@ namespace Oqtane.Repository
public Setting GetSetting(string entityName, int settingId)
{
using var tenant = _tenantContextFactory.CreateDbContext();
if (IsMaster(entityName))
{
return _master.Setting.Find(settingId);
}
else
{
using var tenant = _tenantContextFactory.CreateDbContext();
return tenant.Setting.Find(settingId);
}
}
public Setting GetSetting(string entityName, int entityId, string settingName)
{
using var tenant = _tenantContextFactory.CreateDbContext();
if (IsMaster(entityName))
{
return _master.Setting.Where(item => item.EntityName == entityName && item.EntityId == entityId && item.SettingName == settingName).FirstOrDefault();
}
else
{
using var tenant = _tenantContextFactory.CreateDbContext();
return tenant.Setting.Where(item => item.EntityName == entityName && item.EntityId == entityId && item.SettingName == settingName).FirstOrDefault();
}
}
public void DeleteSetting(string entityName, int settingId)
{
using var tenant = _tenantContextFactory.CreateDbContext();
if (IsMaster(entityName))
{
Setting setting = _master.Setting.Find(settingId);
@ -113,6 +129,7 @@ namespace Oqtane.Repository
}
else
{
using var tenant = _tenantContextFactory.CreateDbContext();
Setting setting = tenant.Setting.Find(settingId);
tenant.Setting.Remove(setting);
tenant.SaveChanges();
@ -122,7 +139,6 @@ namespace Oqtane.Repository
public void DeleteSettings(string entityName, int entityId)
{
using var tenant = _tenantContextFactory.CreateDbContext();
if (IsMaster(entityName))
{
IEnumerable<Setting> settings = _master.Setting
@ -136,6 +152,7 @@ namespace Oqtane.Repository
}
else
{
using var tenant = _tenantContextFactory.CreateDbContext();
IEnumerable<Setting> settings = tenant.Setting
.Where(item => item.EntityName == entityName)
.Where(item => item.EntityId == entityId);

View File

@ -9,6 +9,7 @@ using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Modules.Admin.Modules;
using Oqtane.Shared;
using Module = Oqtane.Models.Module;
@ -25,6 +26,7 @@ namespace Oqtane.Repository
private readonly IPageModuleRepository _pageModuleRepository;
private readonly IModuleDefinitionRepository _moduleDefinitionRepository;
private readonly IThemeRepository _themeRepository;
private readonly ISettingRepository _settingRepository;
private readonly IServiceProvider _serviceProvider;
private readonly IConfigurationRoot _config;
private readonly IServerStateManager _serverState;
@ -32,8 +34,8 @@ namespace Oqtane.Repository
private static readonly object _lock = new object();
public SiteRepository(IDbContextFactory<TenantDBContext> factory, IRoleRepository roleRepository, IProfileRepository profileRepository, IFolderRepository folderRepository, IPageRepository pageRepository,
IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IThemeRepository themeRepository, IServiceProvider serviceProvider,
IConfigurationRoot config, IServerStateManager serverState, ILogManager logger)
IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IThemeRepository themeRepository, ISettingRepository settingRepository,
IServiceProvider serviceProvider, IConfigurationRoot config, IServerStateManager serverState, ILogManager logger)
{
_factory = factory;
_roleRepository = roleRepository;
@ -44,6 +46,7 @@ namespace Oqtane.Repository
_pageModuleRepository = pageModuleRepository;
_moduleDefinitionRepository = moduleDefinitionRepository;
_themeRepository = themeRepository;
_settingRepository = settingRepository;
_serviceProvider = serviceProvider;
_config = config;
_serverState = serverState;
@ -391,6 +394,7 @@ namespace Oqtane.Repository
{
_logger.Log(LogLevel.Information, "Site Template", LogFunction.Update, "Page Updated {Page}", page);
}
UpdateSettings(EntityNames.Page, page.PageId, pageTemplate.Settings);
}
}
else
@ -401,6 +405,7 @@ namespace Oqtane.Repository
{
_logger.Log(LogLevel.Information, "Site Template", LogFunction.Create, "Page Added {Page}", page);
}
UpdateSettings(EntityNames.Page, page.PageId, pageTemplate.Settings);
}
}
catch (Exception ex)
@ -457,6 +462,7 @@ namespace Oqtane.Repository
{
_logger.Log(LogLevel.Information, "Site Template", LogFunction.Update, "Page Module Updated {PageModule}", pageModule);
}
UpdateSettings(EntityNames.Module, pageModule.Module.ModuleId, pageTemplateModule.Settings);
}
else
{
@ -475,6 +481,7 @@ namespace Oqtane.Repository
{
_logger.Log(LogLevel.Information, "Site Template", LogFunction.Create, "Page Module Added {PageModule}", pageModule);
}
UpdateSettings(EntityNames.Module, pageModule.Module.ModuleId, pageTemplateModule.Settings);
}
}
@ -522,5 +529,25 @@ namespace Oqtane.Repository
}
}
}
private void UpdateSettings(string entityName, int entityId, List<Setting> templateSettings)
{
foreach (var templateSetting in templateSettings)
{
var setting = _settingRepository.GetSetting(entityName, entityId, templateSetting.SettingName);
if (setting == null)
{
templateSetting.EntityName = entityName;
templateSetting.EntityId = entityId;
_settingRepository.AddSetting(templateSetting);
}
else
{
setting.SettingValue = templateSetting.SettingValue;
setting.IsPrivate = templateSetting.IsPrivate;
_settingRepository.UpdateSetting(setting);
}
}
}
}
}

View File

@ -65,14 +65,14 @@ namespace Oqtane.Repository
// delete visitors in batches of 100 records
var count = 0;
var purgedate = DateTime.UtcNow.AddDays(-age);
var visitors = db.Visitor.Where(item => item.SiteId == siteId && item.Visits < 2 && item.VisitedOn < purgedate)
var visitors = db.Visitor.Where(item => item.SiteId == siteId && item.VisitedOn < purgedate)
.OrderBy(item => item.VisitedOn).Take(100).ToList();
while (visitors.Count > 0)
{
count += visitors.Count;
db.Visitor.RemoveRange(visitors);
db.SaveChanges();
visitors = db.Visitor.Where(item => item.SiteId == siteId && item.Visits < 2 && item.VisitedOn < purgedate)
visitors = db.Visitor.Where(item => item.SiteId == siteId && item.VisitedOn < purgedate)
.OrderBy(item => item.VisitedOn).Take(100).ToList();
}
return count;

View File

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Privacy" xml:space="preserve">
<value>&lt;p&gt;This privacy policy ("policy") will help you understand how [PageState:Site:Name] ("us", "we", "our") uses and protects the data you provide to us when you visit and use this website.&lt;/p&gt;
&lt;p&gt;We reserve the right to change this policy at any time. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page.&lt;/p&gt;
&lt;h2&gt;What User Data We Collect&lt;/h2&gt;
&lt;p&gt;When you visit this website, we may collect the following data: your IP address, your contact information and email address, other information such as interests and preferences.&lt;/p&gt;
&lt;h2&gt;Why We Collect Your Data&lt;/h2&gt;
&lt;p&gt;We are collecting your data for several reasons: to better understand your needs, to improve our products and services, to send you promotional emails containing the information we think you will find interesting, to customize our website according to your online behavior and personal preferences.&lt;/p&gt;
&lt;h2&gt;Safeguarding and Securing the Data&lt;/h2&gt;
&lt;p&gt;[PageState:Site:Name] is committed to securing your data and keeping it confidential. [PageState:Site:Name] has done everything in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online.&lt;/p&gt;
&lt;h2&gt;Our Cookie Policy&lt;/h2&gt;
&lt;p&gt;Once you agree to allow our website to use cookies, you also agree to allow us to use the data it collects regarding your online behavior (analyze web traffic, web pages you visit and spend the most time on, etc...).&lt;/p&gt;
&lt;p&gt;The data we collect by using cookies is used to customize our website to your needs.&lt;/p&gt;
&lt;p&gt;Please note that cookies don't allow us to gain access to your computer in any way. They are strictly used to monitor which pages you find useful and which you do not so that we can provide a better experience for you.&lt;/p&gt;
&lt;p&gt;If you want to disable or remove cookies, you can do so by accessing the settings of your internet browser.&lt;/p&gt;
&lt;h2&gt;Links to Other Websites&lt;/h2&gt;
&lt;p&gt;Our website contains links that lead to other websites. If you click on these links [PageState:Site:Name] is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of any website you navigate to from our website.&lt;/p&gt;
&lt;h2&gt;Restricting the Collection of your Personal Data&lt;/h2&gt;
&lt;p&gt;At some point, you might wish to restrict the use and collection of your personal data. If you previously agreed to share your information with us, feel free to contact us via email and we will be more than happy to change this for you.&lt;/p&gt;
&lt;p&gt;[PageState:Site:Name] will not lease, sell or distribute your personal information to any third parties, unless we have your permission. Your personal information will only be used when we need to send you promotional materials if you agree to this privacy policy.&lt;/p&gt;</value>
</data>
<data name="Terms" xml:space="preserve">
<value>&lt;p&gt;Please read these terms and conditions carefully before using this website operated by [PageState:Site:Name] ("us", "we", "our").&lt;/p&gt;
&lt;h2&gt;Conditions of Use&lt;/h2&gt;
&lt;p&gt;By using this website, you certify that you have read and reviewed this Agreement and that you agree to comply with its terms. If you do not want to be bound by the terms of this Agreement, you are advised to stop using the website accordingly. [PageState:Site:Name] only grants use and access of this website, its products, and its services to those who have accepted its terms.&lt;/p&gt;
&lt;h2&gt;Privacy Policy&lt;/h2&gt;
&lt;p&gt;Before you continue using our website, we advise you to read our &lt;a href="/privacy"&gt;privacy policy&lt;/a&gt; regarding our user data collection. It will help you better understand our practices.&lt;/p&gt;
&lt;h2&gt;Intellectual Property&lt;/h2&gt;
&lt;p&gt;You agree that all materials, products, and services provided on this website are the property of [PageState:Site:Name], its affiliates, directors, officers, employees, agents, suppliers, or licensors including all copyrights, trade secrets, trademarks, patents, and other intellectual property. You also agree that you will not reproduce or redistribute the [PageState:Site:Name]s intellectual property in any way, including electronic, digital, or new trademark registrations.&lt;/p&gt;
&lt;p&gt;You grant [PageState:Site:Name] a royalty-free and non-exclusive license to display, use, copy, transmit, and broadcast the content you upload and publish. For issues regarding intellectual property claims, you should contact us in order to come to an agreement.&lt;/p&gt;
&lt;h2&gt;User Accounts&lt;/h2&gt;
&lt;p&gt;As a user of this website, you may be asked to register with us and provide private information. You are responsible for ensuring the accuracy of this information, and you are responsible for maintaining the safety and security of your identifying information.&lt;/p&gt;
&lt;p&gt;You are also responsible for all activities that occur under your account or password. If you think there are any possible issues regarding the security of your account on the website, inform us immediately so we may address them accordingly.&lt;/p&gt;
&lt;p&gt;We reserve all rights to terminate accounts, edit or remove content and cancel orders at our sole discretion.&lt;/p&gt;
&lt;h2&gt;Applicable Law&lt;/h2&gt;
&lt;p&gt;By using this website, you agree that the laws of the jurisdiction associated to [PageState:Site:Name], without regard to principles of conflict laws, will govern these terms and conditions, or any dispute of any sort that might come between [PageState:Site:Name] and you, or its business partners and associates.&lt;/p&gt;
&lt;h2&gt;Disputes&lt;/h2&gt;
&lt;p&gt;Any dispute related in any way to your use of this website or to products you purchase from us shall be arbitrated by a court of law and you consent to exclusive jurisdiction and venue of such courts.&lt;/p&gt;
&lt;h2&gt;Indemnification&lt;/h2&gt;
&lt;p&gt;You agree to indemnify [PageState:Site:Name] and its affiliates and hold [PageState:Site:Name] harmless against legal claims and demands that may arise from your use or misuse of our services. We reserve the right to select our own legal counsel.&lt;/p&gt;
&lt;h2&gt;Limitation on Liability&lt;/h2&gt;
&lt;p&gt;[PageState:Site:Name] is not liable for any damages that may occur to you as a result of your misuse of our website. [PageState:Site:Name] reserves the right to edit, modify, and change this Agreement at any time. We shall let our users know of these changes through electronic mail. This Agreement is an understanding between [PageState:Site:Name] and the user, and this supersedes and replaces all prior agreements regarding the use of this website.&lt;/p&gt;
</value>
</data>
</root>

View File

@ -0,0 +1,120 @@
using System;
using System.Diagnostics.Contracts;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ServerCookieConsentService : ICookieConsentService
{
private readonly IHttpContextAccessor _accessor;
private readonly CookiePolicyOptions _cookiePolicyOptions;
public ServerCookieConsentService(IHttpContextAccessor accessor, IOptions<CookiePolicyOptions> cookiePolicyOptions)
{
_accessor = accessor;
_cookiePolicyOptions = cookiePolicyOptions.Value;
}
public Task<bool> IsActionedAsync()
{
var actioned = false;
if (_accessor.HttpContext != null)
{
var cookieValue = GetCookieValue("actioned");
actioned = cookieValue == Constants.CookieConsentActionCookieValue;
}
return Task.FromResult(actioned);
}
public Task<bool> CanTrackAsync(bool optOut)
{
var canTrack = true;
if (_accessor.HttpContext != null)
{
var cookieValue = GetCookieValue("consent");
var saved = cookieValue == Constants.CookieConsentCookieValue;
if (optOut)
{
canTrack = string.IsNullOrEmpty(cookieValue) || !saved;
}
else
{
canTrack = cookieValue == Constants.CookieConsentCookieValue;
}
}
return Task.FromResult(canTrack);
}
public Task<string> CreateActionedCookieAsync()
{
var cookieString = CreateCookieString(false, string.Empty);
return Task.FromResult(cookieString);
}
public Task<string> CreateConsentCookieAsync()
{
var cookieString = CreateCookieString(true, Constants.CookieConsentCookieValue);
return Task.FromResult(cookieString);
}
public Task<string> WithdrawConsentCookieAsync()
{
var cookieString = CreateCookieString(true, string.Empty);
return Task.FromResult(cookieString);
}
private string GetCookieValue(string type)
{
var cookieValue = string.Empty;
if (_accessor.HttpContext != null)
{
var value = _accessor.HttpContext.Request.Cookies[Constants.CookieConsentCookieName];
var index = type == "actioned" ? 1 : 0;
cookieValue = !string.IsNullOrEmpty(value) && value.Contains("|") ? value.Split('|')[index] : string.Empty;
}
return cookieValue;
}
private string CreateCookieString(bool saved, string savedValue)
{
var cookieString = string.Empty;
if (_accessor.HttpContext != null)
{
var savedCookie = saved ? savedValue : GetCookieValue("consent");
var actionedCookie = Constants.CookieConsentActionCookieValue;
var cookieValue = $"{savedCookie}|{actionedCookie}";
var options = _cookiePolicyOptions.ConsentCookie.Build(_accessor.HttpContext);
if (!_accessor.HttpContext.Response.HasStarted)
{
_accessor.HttpContext.Response.Cookies.Append(
Constants.CookieConsentCookieName,
cookieValue,
new CookieOptions()
{
Expires = options.Expires,
IsEssential = true,
SameSite = options.SameSite,
Secure = options.Secure
}
);
}
//get the cookie string from response header
cookieString = options.CreateCookieHeader(Constants.CookieConsentCookieName, Uri.EscapeDataString(cookieValue)).ToString();
}
return cookieString;
}
}
}

View File

@ -0,0 +1,41 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OutputCaching;
using Oqtane.Documentation;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Shared;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ServerOutputCacheService : IOutputCacheService
{
private readonly IOutputCacheStore _outputCacheStore;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
public ServerOutputCacheService(IOutputCacheStore outputCacheStore, ILogManager logger, IHttpContextAccessor accessor)
{
_outputCacheStore = outputCacheStore;
_logger = logger;
_accessor = accessor;
}
public async Task EvictByTag(string tag)
{
if (_accessor.HttpContext.User.IsInRole(RoleNames.Admin))
{
await _outputCacheStore.EvictByTagAsync(tag, default);
_logger.Log(LogLevel.Information, this, LogFunction.Other, "Evicted Output Cache for Tag {Tag}", tag);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Output Cache Eviction for {Tag}", tag);
}
}
}
}

View File

@ -142,6 +142,8 @@ namespace Oqtane
});
});
services.AddOutputCache();
services.AddMvc(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
@ -222,6 +224,7 @@ namespace Oqtane
app.UseJwtAuthorization();
app.UseRouting();
app.UseCors();
app.UseOutputCache();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();

View File

@ -2,10 +2,10 @@
"RenderMode": "Interactive",
"Runtime": "Server",
"Database": {
"DefaultDBType": ""
"DefaultDBType": "Oqtane.Database.Sqlite.SqliteDatabase, Oqtane.Database.Sqlite"
},
"ConnectionStrings": {
"DefaultConnection": ""
"DefaultConnection": "Data Source=Oqtane-202507152139.db;"
},
"Installation": {
"DefaultAlias": "",
@ -54,5 +54,9 @@
"LogLevel": {
"Default": "Information"
}
}
},
"InstallationId": "e0b3aaed-a33d-4756-bd08-a83403d0c437",
"InstallationVersion": "6.1.1",
"InstallationDate": "202507152140",
"PackageRegistryEmail": "ismail.hasimoglu@edu.szu.at"
}

View File

@ -13,11 +13,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -19,10 +19,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -30,7 +30,6 @@
.breadcrumbs {
background-color: #e6e6e6;
border-bottom: 1px solid #d6d5d5;
}
.top-row {
@ -120,12 +119,7 @@
.app-logo .navbar-brand {
color: white;
}
@media (max-width: 767.98px) {
.main .top-row {
display: none;
}
@media (max-width: 991.98px) {
.app-search {
border-radius: 6px;
}
@ -141,7 +135,7 @@
.app-search:active, .app-search:hover {
display: block;
position: fixed;
position: absolute;
color: #fff;
top: 0;
min-height: 60px;
@ -149,6 +143,7 @@
left: 0;
z-index: 999;
border-radius: 0;
background-color: #e6e6e6;
}
.app-search:active .app-form-inline, .app-search:hover .app-form-inline {
@ -169,6 +164,11 @@
padding-bottom: 6px;
}
}
@media (max-width: 767.98px) {
.main .top-row {
display: none;
}
}
@media (min-width: 768px) {
app {
@ -257,6 +257,7 @@
width: 100%;
left: 0;
z-index: 4;
border-bottom: 1px solid #d6d5d5;
}
.sidebar {

View File

@ -99,7 +99,49 @@ div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu
color: #ffffff;
z-index: 1000;
}
@media (max-width: 991.98px) {
.app-search {
border-radius: 6px;
}
.app-search input{
display: none !important;
}
.app-search input + button {
position: initial;
padding-top: 7px;
padding-bottom: 7px;
}
.app-search:active, .app-search:hover {
display: block;
position: fixed;
top: 0;
min-height: 96px;
width: 100%;
left: 0;
z-index: 999;
border-radius: 0;
}
.app-search:active .app-form-inline, .app-search:hover .app-form-inline {
margin: 10px auto;
position: relative;
display: block;
max-width: 80%;
}
.app-search:active .app-form-inline input, .app-search:hover .app-form-inline input {
width: 100%;
display: block !important;
}
.app-search:active .app-form-inline input + button, .app-search:hover .app-form-inline input + button {
position: absolute;
color: rgb(42, 159, 214);
padding-top: 6px;
padding-bottom: 6px;
}
}
@media (max-width: 767.98px) {
.app-menu {
@ -130,45 +172,7 @@ div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu
position: relative;
top: 60px;
}
.app-search {
border-radius: 6px;
}
.app-search input{
display: none !important;
}
.app-search input + button {
position: initial;
padding-top: 7px;
padding-bottom: 7px;
}
.app-search:active, .app-search:hover{
display: block;
position: fixed;
top: 0;
min-height: 60px;
width: 100%;
left: 0;
z-index: 999;
border-radius: 0;
}
.app-search:active .app-form-inline, .app-search:hover .app-form-inline {
margin: 10px auto;
position: relative;
display: block;
max-width: 80%;
}
.app-search:active .app-form-inline input, .app-search:hover .app-form-inline input {
width: 100%;
display: block !important;
}
.app-search:active .app-form-inline input + button, .app-search:hover .app-form-inline input + button {
position: absolute;
color: rgb(42, 159, 214);
padding-top: 6px;
padding-bottom: 6px;
}
}

View File

@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -272,3 +272,13 @@ app {
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}
/* cookie consent */
.gdpr-consent-bar .btn-show{
bottom: -3px;
left: 5px;
}
.gdpr-consent-bar .btn-hide{
top: 0;
right: 5px;
}

View File

@ -14,6 +14,9 @@ Oqtane.Interop = {
}
document.cookie = cookieString;
},
setCookieString: function (cookieString) {
document.cookie = cookieString;
},
getCookie: function (name) {
name = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);

View File

@ -1,8 +1,8 @@
const scriptKeys = new Set();
export function onUpdate() {
// determine if this is an enhanced navigation
let enhancedNavigation = scriptKeys.size !== 0;
// determine if this is an initial request
let initialRequest = scriptKeys.size === 0;
// iterate over all script elements in document
const scripts = document.getElementsByTagName('script');
@ -11,7 +11,7 @@ export function onUpdate() {
if (script.hasAttribute('data-reload')) {
let key = getKey(script);
if (enhancedNavigation) {
if (!initialRequest) {
// reload the script if data-reload is "always" or "true"... or if the script has not been loaded previously and data-reload is "once"
let dataReload = script.getAttribute('data-reload');
if ((dataReload === 'always' || dataReload === 'true') || (!scriptKeys.has(key) && dataReload == 'once')) {
@ -40,7 +40,7 @@ function getKey(script) {
function reloadScript(script) {
try {
if (isValid(script)) {
replaceScript(script);
injectScript(script);
}
} catch (error) {
console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, error);
@ -55,16 +55,18 @@ function isValid(script) {
return true;
}
function replaceScript(script) {
function injectScript(script) {
return new Promise((resolve, reject) => {
var newScript = document.createElement('script');
// replicate attributes and content
for (let i = 0; i < script.attributes.length; i++) {
if (script.attributes[i].name !== 'data-reload') {
newScript.setAttribute(script.attributes[i].name, script.attributes[i].value);
}
}
newScript.nonce = script.nonce; // must be referenced explicitly
newScript.innerHTML = script.innerHTML;
newScript.removeAttribute('data-reload');
// dynamically injected scripts cannot be async or deferred
newScript.async = false;
@ -73,10 +75,10 @@ function replaceScript(script) {
newScript.onload = () => resolve();
newScript.onerror = (error) => reject(error);
// remove existing script element
script.remove();
// replace with new script element to force reload in Blazor
// inject script element in head to force execution in Blazor
document.head.appendChild(newScript);
// remove data-reload attribute
script.removeAttribute('data-reload');
});
}

View File

@ -32,6 +32,15 @@ namespace Oqtane.Models
this.CrossOrigin = CrossOrigin;
}
public Script(string Src, string Integrity, string CrossOrigin, ResourceLoadBehavior LoadBehavior)
{
SetDefaults();
this.Url = Src;
this.Integrity = Integrity;
this.CrossOrigin = CrossOrigin;
this.LoadBehavior = LoadBehavior;
}
public Script(string Src, string Integrity, string CrossOrigin, ResourceLocation Location, ResourceLoadBehavior LoadBehavior, Dictionary<string, string> DataAttributes, string Type, string Bundle, string RenderMode)
{
SetDefaults();

View File

@ -35,6 +35,7 @@ namespace Oqtane.Models
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
};
Settings = new List<Setting>();
PageTemplateModules = new List<PageTemplateModule>();
// properties used by IModule
@ -60,6 +61,7 @@ namespace Oqtane.Models
public bool IsPersonalizable { get; set; }
public bool IsDeleted { get; set; }
public List<Permission> PermissionList { get; set; }
public List<Setting> Settings { get; set; }
public List<PageTemplateModule> PageTemplateModules { get; set; }
// properties used by IModule
@ -99,6 +101,7 @@ namespace Oqtane.Models
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
};
Settings = new List<Setting>();
Content = "";
}
@ -109,6 +112,7 @@ namespace Oqtane.Models
public string ContainerType { get; set; }
public bool IsDeleted { get; set; }
public List<Permission> PermissionList { get; set; }
public List<Setting> Settings { get; set; }
public string Content { get; set; }
[Obsolete("The ModulePermissions property is deprecated. Use PermissionList instead", false)]

View File

@ -32,6 +32,10 @@ namespace Oqtane.Models
/// <summary>
/// Reference to a <see cref="File"/> containing the users photo.
/// </summary>
public string Subject { get; set; }
public int GraduationYear { get; set; }
public int? PhotoFileId { get; set; }
/// <summary>

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -19,11 +19,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.1" />
<PackageReference Include="System.Text.Json" Version="9.0.3" />
</ItemGroup>
</Project>

View File

@ -4,8 +4,8 @@ namespace Oqtane.Shared
{
public class Constants
{
public static readonly string Version = "6.1.0";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0";
public static readonly string Version = "6.1.1";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";
@ -33,8 +33,8 @@ namespace Oqtane.Shared
public const string PageManagementModule = "Oqtane.Modules.Admin.Pages, Oqtane.Client";
public const string ErrorModule = "Oqtane.Modules.Admin.Error.{Action}, Oqtane.Client";
public const string AdminSiteTemplate = "Oqtane.SiteTemplates.AdminSiteTemplate, Oqtane.Server";
public const string DefaultSiteTemplate = "Oqtane.SiteTemplates.DefaultSiteTemplate, Oqtane.Server";
public const string AdminSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.AdminSiteTemplate, Oqtane.Server";
public const string DefaultSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.DefaultSiteTemplate, Oqtane.Server";
public static readonly string[] DefaultHostModuleTypes = new[] { "Upgrade", "Themes", "SystemInfo", "Sql", "Sites", "ModuleDefinitions", "Logs", "Jobs", "ModuleCreator" };
@ -46,7 +46,7 @@ namespace Oqtane.Shared
public const string DefaultSite = "Default Site";
public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico,webp";
public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,xml,rss,css";
public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,rss,css";
public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$";
public static readonly char[] InvalidFileNameChars =
@ -91,6 +91,11 @@ namespace Oqtane.Shared
public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css";
public const string BootstrapStylesheetIntegrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==";
public const string CookieConsentCookieName = "Oqtane.CookieConsent";
public const string CookieConsentCookieValue = "yes";
public const string CookieConsentActionCookieValue = "yes";
public const string SitemapOutputCacheTag = "Sitemap";
// Obsolete constants
const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Version>6.1.0</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -1,4 +1,4 @@
# Oqtane Framework
# Oqtane Framewo
![Oqtane](https://github.com/oqtane/framework/blob/master/oqtane.png?raw=true "Oqtane")
@ -12,15 +12,13 @@ Oqtane is being developed based on some fundamental principles which are outline
# Latest Release
[6.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1) was released on December 20, 2024 and is a maintenance release including 58 pull requests by 7 different contributors, pushing the total number of project commits all-time to over 6100. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[6.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1) was released on March 12, 2025 and is a maintenance release including 46 pull requests by 4 different contributors, pushing the total number of project commits all-time to over 6400. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json)
# Getting Started (Version 6.x)
# Getting Started (Version 6.1.1)
**Installing using source code from the Dev/Master branch:**
- Install **[.NET 9.0.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**.
- Install **[.NET 9.0.3 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**.
- Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
@ -86,9 +84,17 @@ Connect with other developers, get support, and share ideas by joining the Oqtan
# Roadmap
This project is open source, and therefore is a work in progress...
[6.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1) (Mar 12, 2025)
- [x] Stabilization improvements
- [x] Cookie Consent Banner & Privacy/Terms
[6.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0) (Feb 11, 2025)
- [x] Static Asset / Folder Asset Caching
- [x] JavaScript improvements in Blazor Static Server Rendering (SSR)
- [x] User Impersonation
[6.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1) (Dec 20, 2024)
- [x] Stabilization improvements
- [x] JavaScript improvements in Blazor Static Server Rendering (SSR)
[6.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0) (Nov 14, 2024)
- [x] Migration to .NET 9

View File

@ -1,5 +1,9 @@
# Security Policy
## Security Bulletins
All published security bulletins are available in the Oqtane [Security Center](https://www.oqtane.net/security). The Security Center allows you to select a specific version of the Oqtane Framework and view the associated security bulletins.
## Reporting a Vulnerability
We make every effort to ensure rapid and thorough analysis of reported issues and, where appropriate, provide workarounds and updated application releases to fix them. If you identify a potential security vulnerability please report it via support@oqtane.org.
@ -7,7 +11,7 @@ We make every effort to ensure rapid and thorough analysis of reported issues an
All submitted information is viewed only by members of the Oqtane Security Team, and will not be discussed outside the Team without the permission of the person/company who reported the issue. Each confirmed issue is assigned a severity level (critical, moderate, or low) corresponding to its potential impact on an Oqtane installation.
* **Critical** means the issue can be exploited by a remote attacker to gain access to data or functionality. All critical issue security bulletins include a recommended workaround or fix that should be applied as soon as possible.
* **Moderate** means the issue can compromise data or functionality on a portal/website only if some other condition is met (e.g. a particular module or a user within a particular role is required). Moderate issue security bulletins typically include recommended actions to resolve the issue.
* **Moderate** means the issue can compromise data or functionality on a site only if some other condition is met (e.g. a particular module or a user within a particular role is required). Moderate issue security bulletins typically include recommended actions to resolve the issue.
* **Low** means the issue is very difficult to exploit or has a limited potential impact.
Once an issue has been resolved via a public release of Oqtane, the release notes on GitHub are updated to reflect that security bulletins exist for the release. We strongly suggest using the "Watch" option on GitHub for "Releases" at a minimum to receive notifications of updated Oqtane releases.

Some files were not shown because too many files have changed in this diff Show More